@funnest/charts-ui 0.6.13 → 0.6.14

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,7 @@ export interface WidgetContext {
70
79
  storage: WidgetStorage;
71
80
  intervalGroups: readonly IntervalGroup[];
72
81
  searchCategories: readonly SearchCategory[];
82
+ searchExchanges: readonly SearchExchange[];
73
83
  /** IANA zone used for the status-bar clock (KST for Korean users by default). */
74
84
  displayTimezone: string;
75
85
  showLogo: boolean;
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
@@ -1836,6 +1836,41 @@ function Highlight({ text, query }) {
1836
1836
  parts.push(text.slice(i3));
1837
1837
  return /* @__PURE__ */ a3($, { children: parts });
1838
1838
  }
1839
+ var sparkCache = /* @__PURE__ */ new Map();
1840
+ var SPARK_TTL = 12e4;
1841
+ var SPARK_DWELL_MS = 300;
1842
+ function LazySparkline({ ctx, info, up }) {
1843
+ const ref = T2(null);
1844
+ const [values, setValues] = d2(info.sparkline ?? null);
1845
+ A2(() => {
1846
+ if (info.sparkline || !ctx.datafeed.getSparkline || !ref.current) return;
1847
+ let alive = true;
1848
+ const load = () => {
1849
+ let hit = sparkCache.get(info.symbol);
1850
+ if (!hit || Date.now() - hit.at > SPARK_TTL) {
1851
+ hit = { at: Date.now(), values: ctx.datafeed.getSparkline(info.symbol).catch(() => []) };
1852
+ sparkCache.set(info.symbol, hit);
1853
+ }
1854
+ void hit.values.then((v3) => alive && setValues(v3));
1855
+ };
1856
+ let timer;
1857
+ const io = new IntersectionObserver((entries) => {
1858
+ clearTimeout(timer);
1859
+ if (!entries.some((e3) => e3.isIntersecting)) return;
1860
+ timer = setTimeout(() => {
1861
+ io.disconnect();
1862
+ load();
1863
+ }, SPARK_DWELL_MS);
1864
+ });
1865
+ io.observe(ref.current);
1866
+ return () => {
1867
+ alive = false;
1868
+ clearTimeout(timer);
1869
+ io.disconnect();
1870
+ };
1871
+ }, [info.symbol]);
1872
+ return /* @__PURE__ */ a3("span", { ref, class: "fnc-sr-spark-slot", children: values && /* @__PURE__ */ a3(Sparkline, { values, up }) });
1873
+ }
1839
1874
  function Sparkline({ values, up }) {
1840
1875
  if (values.length < 2) return null;
1841
1876
  const min = Math.min(...values);
@@ -1855,6 +1890,7 @@ function SymbolSearch({ ctx }) {
1855
1890
  const [exchange, setExchange] = d2(null);
1856
1891
  const [results, setResults] = d2(null);
1857
1892
  const [loading, setLoading] = d2(true);
1893
+ const [pending, setPending] = d2(true);
1858
1894
  const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1859
1895
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1860
1896
  const [active, setActive] = d2(0);
@@ -1864,8 +1900,19 @@ function SymbolSearch({ ctx }) {
1864
1900
  A2(() => {
1865
1901
  let alive = true;
1866
1902
  setLoading(true);
1903
+ setPending(true);
1867
1904
  const id = setTimeout(() => {
1868
- ctx.datafeed.searchSymbols?.(query.trim()).then((items) => alive && setResults(items)).catch(() => alive && setResults([])).finally(() => alive && setLoading(false));
1905
+ const feed = ctx.datafeed;
1906
+ const search = feed.searchSymbolsProgressive ? feed.searchSymbolsProgressive(query.trim(), (partial) => {
1907
+ if (!alive) return;
1908
+ setResults(partial);
1909
+ setLoading(false);
1910
+ }) : feed.searchSymbols?.(query.trim());
1911
+ search?.then((items) => alive && setResults(items)).catch(() => alive && setResults([])).finally(() => {
1912
+ if (!alive) return;
1913
+ setLoading(false);
1914
+ setPending(false);
1915
+ });
1869
1916
  }, DEBOUNCE_MS);
1870
1917
  return () => {
1871
1918
  alive = false;
@@ -1879,7 +1926,9 @@ function SymbolSearch({ ctx }) {
1879
1926
  const exchanges = w2(() => {
1880
1927
  const counts = /* @__PURE__ */ new Map();
1881
1928
  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]);
1929
+ const declared = ctx.searchExchanges.map((e3) => ({ ...e3, count: counts.get(e3.name) }));
1930
+ 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 }));
1931
+ return [...declared, ...others];
1883
1932
  }, [results, category]);
1884
1933
  const q2 = query.trim().toLowerCase();
1885
1934
  const favoriteRows = filter(favorites).filter((f4) => !q2 || f4.name.toLowerCase().includes(q2) || f4.symbol.toLowerCase().includes(q2));
@@ -1957,7 +2006,7 @@ function SymbolSearch({ ctx }) {
1957
2006
  cat && ` \xB7 ${cat.label[locale]}`
1958
2007
  ] })
1959
2008
  ] }),
1960
- info.sparkline && /* @__PURE__ */ a3(Sparkline, { values: info.sparkline, up }),
2009
+ (info.sparkline || ctx.datafeed.getSparkline) && /* @__PURE__ */ a3(LazySparkline, { ctx, info, up }, info.symbol),
1961
2010
  info.price !== void 0 && /* @__PURE__ */ a3("span", { class: "fnc-sr-quote", children: [
1962
2011
  /* @__PURE__ */ a3("b", { children: formatPrice(info.price, info.pricePrecision) }),
1963
2012
  info.changePercent !== void 0 && /* @__PURE__ */ a3("span", { class: trend, children: [
@@ -1983,7 +2032,8 @@ function SymbolSearch({ ctx }) {
1983
2032
  }
1984
2033
  );
1985
2034
  };
1986
- const sideItem = (label, selected, onClick, extra) => /* @__PURE__ */ a3("button", { class: "fnc-sr-side-item", "aria-pressed": selected, onClick, children: [
2035
+ const sideItem = (label, selected, onClick, extra, logoUrl) => /* @__PURE__ */ a3("button", { class: "fnc-sr-side-item", "aria-pressed": selected, onClick, children: [
2036
+ 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
2037
  /* @__PURE__ */ a3("span", { children: label }),
1988
2038
  extra
1989
2039
  ] });
@@ -2019,7 +2069,13 @@ function SymbolSearch({ ctx }) {
2019
2069
  sideItem(t3.favorites, view.kind === "favorites", () => (setView({ kind: "favorites" }), setExchange(null)), /* @__PURE__ */ a3("em", { children: favorites.length || "" })),
2020
2070
  exchanges.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-sr-side-title", children: t3.exchanges }),
2021
2071
  exchanges.map(
2022
- ([name, count]) => sideItem(name, exchange === name, () => (setView({ kind: "all" }), setExchange(exchange === name ? null : name)), /* @__PURE__ */ a3("em", { children: count }))
2072
+ (x2) => sideItem(
2073
+ x2.label?.[locale] ?? x2.name,
2074
+ exchange === x2.name,
2075
+ () => (setView({ kind: "all" }), setExchange(exchange === x2.name ? null : x2.name)),
2076
+ /* @__PURE__ */ a3("em", { children: x2.count ?? (pending ? "" : 0) }),
2077
+ ctx.searchExchanges.length ? x2.logoUrl ?? "" : void 0
2078
+ )
2023
2079
  )
2024
2080
  ] }),
2025
2081
  /* @__PURE__ */ a3("div", { class: "fnc-sr-list fnc-scroll", id: "fnc-search-results", role: "listbox", ref: listRef, children: [
@@ -3170,6 +3226,8 @@ var CSS = (
3170
3226
  }
3171
3227
  .fnc-sr-side-item span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3172
3228
  .fnc-sr-side-item em { font-style: normal; font-size: 12px; color: var(--fnc-text-2); }
3229
+ .fnc-sr-side-logo { flex: none; width: 18px; height: 18px; border-radius: 5px; object-fit: cover; background: var(--fnc-surface); }
3230
+ .fnc-sr-spark-slot { display: inline-flex; flex: none; width: 72px; height: 24px; }
3173
3231
  .fnc-sr-side-item:hover { background: var(--fnc-surface); }
3174
3232
  .fnc-sr-side-item[aria-pressed='true'] { background: var(--fnc-surface); font-weight: 700; }
3175
3233
  .fnc-chip-token {
@@ -4196,6 +4254,7 @@ function createPane(shell, config) {
4196
4254
  storage: shell.storage,
4197
4255
  intervalGroups: config.intervalGroups,
4198
4256
  searchCategories: config.searchCategories,
4257
+ searchExchanges: config.searchExchanges ?? [],
4199
4258
  displayTimezone: config.displayTimezone,
4200
4259
  showLogo: config.symbolLogo,
4201
4260
  searchHotkey: config.searchHotkey,
@@ -4270,6 +4329,7 @@ function createTradingChart(container, options) {
4270
4329
  favoriteIntervals,
4271
4330
  onFavoriteIntervalsChange,
4272
4331
  searchCategories = DEFAULT_SEARCH_CATEGORIES,
4332
+ searchExchanges = [],
4273
4333
  storageKey = "funnest-charts",
4274
4334
  searchHotkey = true,
4275
4335
  symbolLogo = true,
@@ -4299,6 +4359,7 @@ function createTradingChart(container, options) {
4299
4359
  chartType,
4300
4360
  intervalGroups,
4301
4361
  searchCategories,
4362
+ searchExchanges,
4302
4363
  symbolLogo,
4303
4364
  searchHotkey,
4304
4365
  drawingToolbar,
@@ -4368,6 +4429,7 @@ function createMultiChart(container, options) {
4368
4429
  favoriteIntervals,
4369
4430
  onFavoriteIntervalsChange,
4370
4431
  searchCategories = DEFAULT_SEARCH_CATEGORIES,
4432
+ searchExchanges = [],
4371
4433
  storageKey = "funnest-charts",
4372
4434
  searchHotkey = true,
4373
4435
  symbolLogo = true,
@@ -4438,6 +4500,7 @@ function createMultiChart(container, options) {
4438
4500
  chartType,
4439
4501
  intervalGroups,
4440
4502
  searchCategories,
4503
+ searchExchanges,
4441
4504
  symbolLogo,
4442
4505
  searchHotkey,
4443
4506
  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,7 @@ export interface PaneConfig {
11
11
  chartType: ChartType;
12
12
  intervalGroups: readonly IntervalGroup[];
13
13
  searchCategories: readonly SearchCategory[];
14
+ searchExchanges?: readonly SearchExchange[];
14
15
  symbolLogo: boolean;
15
16
  drawingToolbar: boolean;
16
17
  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,8 @@ 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[];
25
27
  /**
26
28
  * localStorage key prefix for UI preferences (favorite intervals, recent searches).
27
29
  * 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.14",
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.14"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"