@funnest/charts-ui 0.6.13 → 0.6.15

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
@@ -60,6 +60,15 @@ export interface SearchCategory {
60
60
  label: Record<Locale, string>;
61
61
  match(info: SymbolInfo): boolean;
62
62
  }
63
+ /**
64
+ * An exchange listed in the symbol search sidebar from the start (before any results), with its
65
+ * logo. `name` is the `SymbolInfo.exchange` its symbols carry; `label` what to show.
66
+ */
67
+ export interface SearchExchange {
68
+ name: string;
69
+ label?: Record<Locale, string>;
70
+ logoUrl?: string;
71
+ }
63
72
  /** Everything components need; created once per widget. */
64
73
  export interface WidgetContext {
65
74
  /** The widget's root element (theme variables live here). */
@@ -70,6 +79,9 @@ export interface WidgetContext {
70
79
  storage: WidgetStorage;
71
80
  intervalGroups: readonly IntervalGroup[];
72
81
  searchCategories: readonly SearchCategory[];
82
+ searchExchanges: readonly SearchExchange[];
83
+ /** Rows per exchange before a "모두 보기" (0: all). */
84
+ searchPreviewPerExchange: number;
73
85
  /** IANA zone used for the status-bar clock (KST for Korean users by default). */
74
86
  displayTimezone: string;
75
87
  showLogo: boolean;
package/dist/i18n.d.ts CHANGED
@@ -124,6 +124,7 @@ export declare const STRINGS: {
124
124
  allSymbols: string;
125
125
  favorites: string;
126
126
  exchanges: string;
127
+ showAllIn: string;
127
128
  noFavorites: string;
128
129
  navigate: string;
129
130
  openAction: string;
@@ -268,6 +269,7 @@ export declare const STRINGS: {
268
269
  allSymbols: string;
269
270
  favorites: string;
270
271
  exchanges: string;
272
+ showAllIn: string;
271
273
  noFavorites: string;
272
274
  navigate: string;
273
275
  openAction: string;
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 } from './context';
11
+ export type { ContextMenuItem, ContextMenuItems, IntervalGroup, LayoutSync, SearchCategory, SearchExchange } from './context';
12
12
  export type { ColorScheme, TradingChart, TradingChartOptions, TradingChartSlots } from './trading-chart';
13
13
  export { resolutionLabel } from './i18n';
package/dist/index.js CHANGED
@@ -549,6 +549,7 @@ var STRINGS = {
549
549
  allSymbols: "\uC804\uCCB4",
550
550
  favorites: "\uC990\uACA8\uCC3E\uAE30",
551
551
  exchanges: "\uAC70\uB798\uC18C",
552
+ showAllIn: "{exchange} {n}\uAC1C \uBAA8\uB450 \uBCF4\uAE30 \u203A",
552
553
  noFavorites: "\uC990\uACA8\uCC3E\uAE30\uD55C \uC885\uBAA9\uC774 \uC5C6\uC5B4\uC694. \u2606\uB97C \uB20C\uB7EC \uCD94\uAC00\uD574 \uBCF4\uC138\uC694",
553
554
  navigate: "\uC774\uB3D9",
554
555
  openAction: "\uC5F4\uAE30",
@@ -693,6 +694,7 @@ var STRINGS = {
693
694
  allSymbols: "All",
694
695
  favorites: "Favorites",
695
696
  exchanges: "Exchanges",
697
+ showAllIn: "All {n} on {exchange} \u203A",
696
698
  noFavorites: "No favorites yet \u2014 tap \u2606 to add one",
697
699
  navigate: "navigate",
698
700
  openAction: "open",
@@ -1836,6 +1838,41 @@ function Highlight({ text, query }) {
1836
1838
  parts.push(text.slice(i3));
1837
1839
  return /* @__PURE__ */ a3($, { children: parts });
1838
1840
  }
1841
+ var sparkCache = /* @__PURE__ */ new Map();
1842
+ var SPARK_TTL = 12e4;
1843
+ var SPARK_DWELL_MS = 300;
1844
+ function LazySparkline({ ctx, info, up }) {
1845
+ const ref = T2(null);
1846
+ const [values, setValues] = d2(info.sparkline ?? null);
1847
+ A2(() => {
1848
+ if (info.sparkline || !ctx.datafeed.getSparkline || !ref.current) return;
1849
+ let alive = true;
1850
+ const load = () => {
1851
+ let hit = sparkCache.get(info.symbol);
1852
+ if (!hit || Date.now() - hit.at > SPARK_TTL) {
1853
+ hit = { at: Date.now(), values: ctx.datafeed.getSparkline(info.symbol).catch(() => []) };
1854
+ sparkCache.set(info.symbol, hit);
1855
+ }
1856
+ void hit.values.then((v3) => alive && setValues(v3));
1857
+ };
1858
+ let timer;
1859
+ const io = new IntersectionObserver((entries) => {
1860
+ clearTimeout(timer);
1861
+ if (!entries.some((e3) => e3.isIntersecting)) return;
1862
+ timer = setTimeout(() => {
1863
+ io.disconnect();
1864
+ load();
1865
+ }, SPARK_DWELL_MS);
1866
+ });
1867
+ io.observe(ref.current);
1868
+ return () => {
1869
+ alive = false;
1870
+ clearTimeout(timer);
1871
+ io.disconnect();
1872
+ };
1873
+ }, [info.symbol]);
1874
+ return /* @__PURE__ */ a3("span", { ref, class: "fnc-sr-spark-slot", children: values && /* @__PURE__ */ a3(Sparkline, { values, up }) });
1875
+ }
1839
1876
  function Sparkline({ values, up }) {
1840
1877
  if (values.length < 2) return null;
1841
1878
  const min = Math.min(...values);
@@ -1855,6 +1892,7 @@ function SymbolSearch({ ctx }) {
1855
1892
  const [exchange, setExchange] = d2(null);
1856
1893
  const [results, setResults] = d2(null);
1857
1894
  const [loading, setLoading] = d2(true);
1895
+ const [pending, setPending] = d2(true);
1858
1896
  const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1859
1897
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1860
1898
  const [active, setActive] = d2(0);
@@ -1864,8 +1902,19 @@ function SymbolSearch({ ctx }) {
1864
1902
  A2(() => {
1865
1903
  let alive = true;
1866
1904
  setLoading(true);
1905
+ setPending(true);
1867
1906
  const id = setTimeout(() => {
1868
- ctx.datafeed.searchSymbols?.(query.trim()).then((items) => alive && setResults(items)).catch(() => alive && setResults([])).finally(() => alive && setLoading(false));
1907
+ const feed = ctx.datafeed;
1908
+ const search = feed.searchSymbolsProgressive ? feed.searchSymbolsProgressive(query.trim(), (partial) => {
1909
+ if (!alive) return;
1910
+ setResults(partial);
1911
+ setLoading(false);
1912
+ }) : feed.searchSymbols?.(query.trim());
1913
+ search?.then((items) => alive && setResults(items)).catch(() => alive && setResults([])).finally(() => {
1914
+ if (!alive) return;
1915
+ setLoading(false);
1916
+ setPending(false);
1917
+ });
1869
1918
  }, DEBOUNCE_MS);
1870
1919
  return () => {
1871
1920
  alive = false;
@@ -1879,14 +1928,33 @@ function SymbolSearch({ ctx }) {
1879
1928
  const exchanges = w2(() => {
1880
1929
  const counts = /* @__PURE__ */ new Map();
1881
1930
  for (const r3 of results ?? []) if (matchCategory(r3)) counts.set(r3.exchange, (counts.get(r3.exchange) ?? 0) + 1);
1882
- return [...counts.entries()].sort((a4, b) => b[1] - a4[1]);
1931
+ const declared = ctx.searchExchanges.map((e3) => ({ ...e3, count: counts.get(e3.name) }));
1932
+ const others = [...counts.entries()].filter(([name]) => !ctx.searchExchanges.some((e3) => e3.name === name)).sort((a4, b) => b[1] - a4[1]).map(([name, count]) => ({ name, label: void 0, logoUrl: void 0, count }));
1933
+ return [...declared, ...others];
1883
1934
  }, [results, category]);
1884
1935
  const q2 = query.trim().toLowerCase();
1885
1936
  const favoriteRows = filter(favorites).filter((f4) => !q2 || f4.name.toLowerCase().includes(q2) || f4.symbol.toLowerCase().includes(q2));
1886
1937
  const showRecent = view.kind === "all" && !q2 && recent.length > 0;
1887
1938
  const recentRows = showRecent ? filter(recent) : [];
1888
- const resultRows = view.kind === "favorites" ? favoriteRows : filter(results ?? []);
1939
+ const allResults = view.kind === "favorites" ? favoriteRows : filter(results ?? []);
1940
+ const preview = ctx.searchPreviewPerExchange;
1941
+ const more = /* @__PURE__ */ new Map();
1942
+ let resultRows = allResults;
1943
+ if (preview > 0 && view.kind === "all" && !exchange) {
1944
+ const seen = /* @__PURE__ */ new Map();
1945
+ const total = /* @__PURE__ */ new Map();
1946
+ for (const r3 of allResults) total.set(r3.exchange, (total.get(r3.exchange) ?? 0) + 1);
1947
+ resultRows = [];
1948
+ for (const r3 of allResults) {
1949
+ const n2 = (seen.get(r3.exchange) ?? 0) + 1;
1950
+ seen.set(r3.exchange, n2);
1951
+ if (n2 > preview) continue;
1952
+ resultRows.push(r3);
1953
+ if (n2 === preview && (total.get(r3.exchange) ?? 0) > preview) more.set(resultRows.length - 1, { exchange: r3.exchange, count: total.get(r3.exchange) });
1954
+ }
1955
+ }
1889
1956
  const rows = [...recentRows, ...resultRows];
1957
+ const exchangeLabel = (name) => ctx.searchExchanges.find((e3) => e3.name === name)?.label?.[locale] ?? name;
1890
1958
  A2(() => setActive(0), [query, category, exchange, view, results]);
1891
1959
  A2(() => {
1892
1960
  listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
@@ -1957,7 +2025,7 @@ function SymbolSearch({ ctx }) {
1957
2025
  cat && ` \xB7 ${cat.label[locale]}`
1958
2026
  ] })
1959
2027
  ] }),
1960
- info.sparkline && /* @__PURE__ */ a3(Sparkline, { values: info.sparkline, up }),
2028
+ (info.sparkline || ctx.datafeed.getSparkline) && /* @__PURE__ */ a3(LazySparkline, { ctx, info, up }, info.symbol),
1961
2029
  info.price !== void 0 && /* @__PURE__ */ a3("span", { class: "fnc-sr-quote", children: [
1962
2030
  /* @__PURE__ */ a3("b", { children: formatPrice(info.price, info.pricePrecision) }),
1963
2031
  info.changePercent !== void 0 && /* @__PURE__ */ a3("span", { class: trend, children: [
@@ -1983,7 +2051,8 @@ function SymbolSearch({ ctx }) {
1983
2051
  }
1984
2052
  );
1985
2053
  };
1986
- const sideItem = (label, selected, onClick, extra) => /* @__PURE__ */ a3("button", { class: "fnc-sr-side-item", "aria-pressed": selected, onClick, children: [
2054
+ const sideItem = (label, selected, onClick, extra, logoUrl) => /* @__PURE__ */ a3("button", { class: "fnc-sr-side-item", "aria-pressed": selected, onClick, children: [
2055
+ logoUrl !== void 0 && (logoUrl ? /* @__PURE__ */ a3("img", { class: "fnc-sr-side-logo", src: logoUrl, alt: "", "aria-hidden": "true" }) : /* @__PURE__ */ a3("i", { class: "fnc-sr-side-logo", "aria-hidden": "true" })),
1987
2056
  /* @__PURE__ */ a3("span", { children: label }),
1988
2057
  extra
1989
2058
  ] });
@@ -2019,7 +2088,13 @@ function SymbolSearch({ ctx }) {
2019
2088
  sideItem(t3.favorites, view.kind === "favorites", () => (setView({ kind: "favorites" }), setExchange(null)), /* @__PURE__ */ a3("em", { children: favorites.length || "" })),
2020
2089
  exchanges.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-sr-side-title", children: t3.exchanges }),
2021
2090
  exchanges.map(
2022
- ([name, count]) => sideItem(name, exchange === name, () => (setView({ kind: "all" }), setExchange(exchange === name ? null : name)), /* @__PURE__ */ a3("em", { children: count }))
2091
+ (x2) => sideItem(
2092
+ x2.label?.[locale] ?? x2.name,
2093
+ exchange === x2.name,
2094
+ () => (setView({ kind: "all" }), setExchange(exchange === x2.name ? null : x2.name)),
2095
+ /* @__PURE__ */ a3("em", { children: x2.count ?? (pending ? "" : 0) }),
2096
+ ctx.searchExchanges.length ? x2.logoUrl ?? "" : void 0
2097
+ )
2023
2098
  )
2024
2099
  ] }),
2025
2100
  /* @__PURE__ */ a3("div", { class: "fnc-sr-list fnc-scroll", id: "fnc-search-results", role: "listbox", ref: listRef, children: [
@@ -2029,7 +2104,13 @@ function SymbolSearch({ ctx }) {
2029
2104
  ] }),
2030
2105
  recentRows.map((info, i3) => row2(info, i3, true)),
2031
2106
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
2032
- loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a3("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : resultRows.map((info, i3) => row2(info, recentRows.length + i3, false)),
2107
+ loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a3("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : resultRows.map((info, i3) => {
2108
+ const m3 = more.get(i3);
2109
+ return m3 ? /* @__PURE__ */ a3($, { children: [
2110
+ row2(info, recentRows.length + i3, false),
2111
+ /* @__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);
2113
+ }),
2033
2114
  !loading && rows.length === 0 && /* @__PURE__ */ a3("div", { class: "fnc-empty", children: view.kind === "favorites" && favorites.length === 0 ? t3.noFavorites : t3.noResults })
2034
2115
  ] })
2035
2116
  ] }),
@@ -3170,6 +3251,10 @@ var CSS = (
3170
3251
  }
3171
3252
  .fnc-sr-side-item span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3172
3253
  .fnc-sr-side-item em { font-style: normal; font-size: 12px; color: var(--fnc-text-2); }
3254
+ .fnc-sr-side-logo { flex: none; width: 18px; height: 18px; border-radius: 5px; object-fit: cover; background: var(--fnc-surface); }
3255
+ .fnc-sr-spark-slot { display: inline-flex; flex: none; width: 72px; height: 24px; }
3256
+ .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
+ .fnc-sr-more:hover { color: var(--fnc-text); }
3173
3258
  .fnc-sr-side-item:hover { background: var(--fnc-surface); }
3174
3259
  .fnc-sr-side-item[aria-pressed='true'] { background: var(--fnc-surface); font-weight: 700; }
3175
3260
  .fnc-chip-token {
@@ -4196,6 +4281,8 @@ function createPane(shell, config) {
4196
4281
  storage: shell.storage,
4197
4282
  intervalGroups: config.intervalGroups,
4198
4283
  searchCategories: config.searchCategories,
4284
+ searchExchanges: config.searchExchanges ?? [],
4285
+ searchPreviewPerExchange: config.searchPreviewPerExchange ?? 0,
4199
4286
  displayTimezone: config.displayTimezone,
4200
4287
  showLogo: config.symbolLogo,
4201
4288
  searchHotkey: config.searchHotkey,
@@ -4270,6 +4357,8 @@ function createTradingChart(container, options) {
4270
4357
  favoriteIntervals,
4271
4358
  onFavoriteIntervalsChange,
4272
4359
  searchCategories = DEFAULT_SEARCH_CATEGORIES,
4360
+ searchExchanges = [],
4361
+ searchPreviewPerExchange = 0,
4273
4362
  storageKey = "funnest-charts",
4274
4363
  searchHotkey = true,
4275
4364
  symbolLogo = true,
@@ -4299,6 +4388,8 @@ function createTradingChart(container, options) {
4299
4388
  chartType,
4300
4389
  intervalGroups,
4301
4390
  searchCategories,
4391
+ searchExchanges,
4392
+ searchPreviewPerExchange,
4302
4393
  symbolLogo,
4303
4394
  searchHotkey,
4304
4395
  drawingToolbar,
@@ -4368,6 +4459,8 @@ function createMultiChart(container, options) {
4368
4459
  favoriteIntervals,
4369
4460
  onFavoriteIntervalsChange,
4370
4461
  searchCategories = DEFAULT_SEARCH_CATEGORIES,
4462
+ searchExchanges = [],
4463
+ searchPreviewPerExchange = 0,
4371
4464
  storageKey = "funnest-charts",
4372
4465
  searchHotkey = true,
4373
4466
  symbolLogo = true,
@@ -4438,6 +4531,8 @@ function createMultiChart(container, options) {
4438
4531
  chartType,
4439
4532
  intervalGroups,
4440
4533
  searchCategories,
4534
+ searchExchanges,
4535
+ searchPreviewPerExchange,
4441
4536
  symbolLogo,
4442
4537
  searchHotkey,
4443
4538
  drawingToolbar,
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, UiState, WidgetContext } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, MultiControls, SearchCategory, SearchExchange, UiState, WidgetContext } from './context';
4
4
  import { type Shell } from './shell';
5
5
  import { Store } from './store';
6
6
  export interface PaneConfig {
@@ -11,6 +11,8 @@ export interface PaneConfig {
11
11
  chartType: ChartType;
12
12
  intervalGroups: readonly IntervalGroup[];
13
13
  searchCategories: readonly SearchCategory[];
14
+ searchExchanges?: readonly SearchExchange[];
15
+ searchPreviewPerExchange?: number;
14
16
  symbolLogo: boolean;
15
17
  drawingToolbar: boolean;
16
18
  attribution: boolean;
@@ -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 { ContextMenuItems, IntervalGroup, SearchCategory } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, SearchCategory, SearchExchange } 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년. */
@@ -22,6 +22,13 @@ export interface TradingChartOptions extends Omit<ChartOptions, 'datafeed'> {
22
22
  onFavoriteIntervalsChange?: (intervals: Resolution[]) => void;
23
23
  /** Symbol search tabs. */
24
24
  searchCategories?: readonly SearchCategory[];
25
+ /** Exchanges listed in the symbol search sidebar from the start, with logos (counts fill in as results come). */
26
+ searchExchanges?: readonly SearchExchange[];
27
+ /**
28
+ * Symbol search: rows per exchange shown at first, each followed by "N개 모두 보기" when there
29
+ * are more (picking the exchange lists them all). Default 0: everything at once.
30
+ */
31
+ searchPreviewPerExchange?: number;
25
32
  /**
26
33
  * localStorage key prefix for UI preferences (favorite intervals, recent searches).
27
34
  * Default 'funnest-charts'. Pass null to keep them in memory only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-ui",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
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.13"
45
+ "@funnest/charts": "0.6.15"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"