@funnest/charts-ui 0.5.1 → 0.5.2

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.
@@ -0,0 +1,6 @@
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).
4
+ * Attach the returned ref to the menu element.
5
+ */
6
+ export declare function useKeepInView<T extends HTMLElement>(open: boolean): import("preact").RefObject<T | null>;
package/dist/index.js CHANGED
@@ -825,7 +825,7 @@ function DrawingToolbar({ ctx }) {
825
825
  if (tool && ctx.store.get().drawbarOpen) ctx.store.set({ drawbarOpen: false });
826
826
  };
827
827
  return /* @__PURE__ */ a2($, { children: [
828
- s3.drawbarCollapsed && /* @__PURE__ */ a2("button", { class: "fnc-drawbar-tab", "aria-label": t3.expandDrawbar, title: t3.expandDrawbar, onClick: () => ctx.setDrawbarCollapsed(false), children: /* @__PURE__ */ a2(ChevronRightIcon, {}) }),
828
+ s3.drawbarCollapsed && /* @__PURE__ */ a2("button", { class: "fnc-drawbar-tab", "aria-label": t3.expandDrawbar, "data-tip": t3.expandDrawbar, onClick: () => ctx.setDrawbarCollapsed(false), children: /* @__PURE__ */ a2(ChevronRightIcon, {}) }),
829
829
  /* @__PURE__ */ a2(
830
830
  "div",
831
831
  {
@@ -840,7 +840,7 @@ function DrawingToolbar({ ctx }) {
840
840
  "button",
841
841
  {
842
842
  class: "fnc-btn",
843
- title: t3[label],
843
+ "data-tip": t3[label],
844
844
  "aria-label": t3[label],
845
845
  "aria-pressed": s3.drawingTool === tool,
846
846
  onClick: () => pick(tool),
@@ -852,7 +852,7 @@ function DrawingToolbar({ ctx }) {
852
852
  "button",
853
853
  {
854
854
  class: "fnc-btn",
855
- title: t3.magnet,
855
+ "data-tip": t3.magnet,
856
856
  "aria-label": t3.magnet,
857
857
  "aria-pressed": s3.magnet,
858
858
  onClick: () => ctx.chart.setMagnet(!s3.magnet),
@@ -863,7 +863,7 @@ function DrawingToolbar({ ctx }) {
863
863
  "button",
864
864
  {
865
865
  class: "fnc-btn",
866
- title: t3.removeAll,
866
+ "data-tip": t3.removeAll,
867
867
  "aria-label": t3.removeAll,
868
868
  disabled: s3.drawingCount === 0,
869
869
  onClick: () => ctx.chart.clearDrawings(),
@@ -875,7 +875,7 @@ function DrawingToolbar({ ctx }) {
875
875
  "button",
876
876
  {
877
877
  class: "fnc-btn fnc-drawbar-collapse",
878
- title: t3.collapseDrawbar,
878
+ "data-tip": t3.collapseDrawbar,
879
879
  "aria-label": t3.collapseDrawbar,
880
880
  onClick: () => ctx.setDrawbarCollapsed(true),
881
881
  children: /* @__PURE__ */ a2(ChevronLeftIcon, {})
@@ -898,17 +898,43 @@ function SelectionBar({ ctx }) {
898
898
  {
899
899
  class: "fnc-swatch",
900
900
  style: { background: color },
901
- title: t3.color,
901
+ "data-tip": t3.color,
902
902
  "aria-label": `${t3.color} ${color}`,
903
903
  "aria-pressed": (d3.color ?? SWATCHES[0]) === color,
904
904
  onClick: () => ctx.chart.updateDrawing(d3.id, { color })
905
905
  }
906
906
  )),
907
907
  /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
908
- /* @__PURE__ */ a2("button", { class: "fnc-btn", title: t3.remove, "aria-label": t3.remove, onClick: () => ctx.chart.removeDrawing(d3.id), children: /* @__PURE__ */ a2(TrashIcon, {}) })
908
+ /* @__PURE__ */ a2("button", { class: "fnc-btn", "data-tip": t3.remove, "aria-label": t3.remove, onClick: () => ctx.chart.removeDrawing(d3.id), children: /* @__PURE__ */ a2(TrashIcon, {}) })
909
909
  ] });
910
910
  }
911
911
 
912
+ // src/components/keep-in-view.ts
913
+ var MARGIN = 8;
914
+ function useKeepInView(open) {
915
+ const ref = T2(null);
916
+ F2(() => {
917
+ const el = ref.current;
918
+ if (!open || !el) return;
919
+ el.style.translate = "";
920
+ const fit = () => {
921
+ el.style.translate = "";
922
+ const r3 = el.getBoundingClientRect();
923
+ const root = el.closest(".fnc-root")?.getBoundingClientRect();
924
+ const minX = Math.max(0, root?.left ?? 0) + MARGIN;
925
+ const maxX = Math.min(document.documentElement.clientWidth, root?.right ?? Infinity) - MARGIN;
926
+ let dx = 0;
927
+ if (r3.right > maxX) dx = maxX - r3.right;
928
+ if (r3.left + dx < minX) dx = minX - r3.left;
929
+ if (dx) el.style.translate = `${Math.round(dx)}px 0`;
930
+ };
931
+ fit();
932
+ window.addEventListener("resize", fit);
933
+ return () => window.removeEventListener("resize", fit);
934
+ }, [open]);
935
+ return ref;
936
+ }
937
+
912
938
  // src/components/chart-type-menu.tsx
913
939
  var TYPES = [
914
940
  { type: "candles", label: "candles", icon: CandlesIcon },
@@ -920,6 +946,7 @@ function ChartTypeMenu({ ctx }) {
920
946
  const t3 = STRINGS[s3.locale];
921
947
  const [open, setOpen] = d2(false);
922
948
  const ref = T2(null);
949
+ const menuRef = useKeepInView(open);
923
950
  const current = TYPES.find((x2) => x2.type === s3.chartType) ?? TYPES[0];
924
951
  A2(() => {
925
952
  if (!open) return;
@@ -937,12 +964,12 @@ function ChartTypeMenu({ ctx }) {
937
964
  ctx.store.set({ chartType: type });
938
965
  setOpen(false);
939
966
  };
940
- return /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref, children: [
967
+ return /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap fnc-chart-type", ref, children: [
941
968
  /* @__PURE__ */ a2(
942
969
  "button",
943
970
  {
944
971
  class: "fnc-btn fnc-btn-wide",
945
- title: t3.chartType,
972
+ "data-tip": t3.chartType,
946
973
  "aria-label": t3.chartType,
947
974
  "aria-haspopup": "menu",
948
975
  "aria-expanded": open,
@@ -953,7 +980,7 @@ function ChartTypeMenu({ ctx }) {
953
980
  ]
954
981
  }
955
982
  ),
956
- open && /* @__PURE__ */ a2("div", { class: "fnc-menu", role: "menu", children: TYPES.map(({ type, label, icon: Icon2 }) => /* @__PURE__ */ a2(
983
+ open && /* @__PURE__ */ a2("div", { class: "fnc-menu", role: "menu", ref: menuRef, children: TYPES.map(({ type, label, icon: Icon2 }) => /* @__PURE__ */ a2(
957
984
  "button",
958
985
  {
959
986
  class: "fnc-menu-item",
@@ -1230,7 +1257,7 @@ function IndicatorSettings({ ctx, id }) {
1230
1257
  onInput: (e3) => updateLine(i3, { period: e3.currentTarget.value.replace(/\D/g, "") })
1231
1258
  }
1232
1259
  ),
1233
- /* @__PURE__ */ a2("button", { type: "button", class: "fnc-ind-btn", "aria-label": t3.removePeriod, title: t3.removePeriod, onClick: () => setLines(lines.filter((_3, j2) => j2 !== i3)), children: /* @__PURE__ */ a2(SmallCloseIcon, {}) })
1260
+ /* @__PURE__ */ a2("button", { type: "button", class: "fnc-ind-btn", "aria-label": t3.removePeriod, "data-tip": t3.removePeriod, onClick: () => setLines(lines.filter((_3, j2) => j2 !== i3)), children: /* @__PURE__ */ a2(SmallCloseIcon, {}) })
1234
1261
  ] })),
1235
1262
  /* @__PURE__ */ a2("button", { type: "button", class: "fnc-add-row", onClick: addLine, children: [
1236
1263
  "+ ",
@@ -1302,6 +1329,7 @@ function IntervalPicker({ ctx }) {
1302
1329
  const t3 = STRINGS[s3.locale];
1303
1330
  const [open, setOpen] = d2(false);
1304
1331
  const ref = T2(null);
1332
+ const menuRef = useKeepInView(open);
1305
1333
  A2(() => {
1306
1334
  if (!open) return;
1307
1335
  const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
@@ -1328,7 +1356,7 @@ function IntervalPicker({ ctx }) {
1328
1356
  class: "fnc-chip",
1329
1357
  "aria-pressed": r3 === s3.resolution,
1330
1358
  disabled: !isSupported(r3),
1331
- title: isSupported(r3) ? resolutionLabel(r3, s3.locale, "long") : t3.unsupportedInterval,
1359
+ "data-tip": isSupported(r3) ? resolutionLabel(r3, s3.locale, "long") : t3.unsupportedInterval,
1332
1360
  onClick: () => select(r3),
1333
1361
  children: resolutionLabel(r3, s3.locale)
1334
1362
  }
@@ -1337,7 +1365,7 @@ function IntervalPicker({ ctx }) {
1337
1365
  "button",
1338
1366
  {
1339
1367
  class: `fnc-btn fnc-btn-wide fnc-interval-trigger${favorites.includes(s3.resolution) ? "" : " fnc-interval-trigger-active"}`,
1340
- title: t3.moreIntervals,
1368
+ "data-tip": t3.moreIntervals,
1341
1369
  "aria-label": t3.moreIntervals,
1342
1370
  "aria-haspopup": "menu",
1343
1371
  "aria-expanded": open,
@@ -1348,7 +1376,7 @@ function IntervalPicker({ ctx }) {
1348
1376
  ]
1349
1377
  }
1350
1378
  ),
1351
- open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-interval-menu", role: "menu", children: ctx.intervalGroups.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-menu-group", role: "group", "aria-label": t3[GROUP_LABEL[group.id]], children: [
1379
+ open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-interval-menu", role: "menu", ref: menuRef, children: ctx.intervalGroups.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-menu-group", role: "group", "aria-label": t3[GROUP_LABEL[group.id]], children: [
1352
1380
  /* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: t3[GROUP_LABEL[group.id]] }),
1353
1381
  group.intervals.map((r3) => {
1354
1382
  const favorite = s3.favoriteIntervals.includes(r3);
@@ -1361,7 +1389,7 @@ function IntervalPicker({ ctx }) {
1361
1389
  role: "menuitemradio",
1362
1390
  "aria-checked": r3 === s3.resolution,
1363
1391
  disabled: !ok,
1364
- title: ok ? void 0 : t3.unsupportedInterval,
1392
+ "data-tip": ok ? void 0 : t3.unsupportedInterval,
1365
1393
  onClick: () => select(r3),
1366
1394
  children: resolutionLabel(r3, s3.locale, "long")
1367
1395
  }
@@ -1372,7 +1400,7 @@ function IntervalPicker({ ctx }) {
1372
1400
  class: "fnc-star",
1373
1401
  "aria-pressed": favorite,
1374
1402
  "aria-label": `${resolutionLabel(r3, s3.locale, "long")} ${favorite ? t3.removeFavorite : t3.addFavorite}`,
1375
- title: favorite ? t3.removeFavorite : t3.addFavorite,
1403
+ "data-tip": favorite ? t3.removeFavorite : t3.addFavorite,
1376
1404
  onClick: () => ctx.toggleFavoriteInterval(r3),
1377
1405
  children: /* @__PURE__ */ a2(StarIcon, { filled: favorite })
1378
1406
  }
@@ -1482,6 +1510,8 @@ function LayoutMenu({ ctx, multi }) {
1482
1510
  const t3 = STRINGS[s3.locale];
1483
1511
  const layout = usePopover();
1484
1512
  const sync = usePopover();
1513
+ const layoutMenuRef = useKeepInView(layout.open);
1514
+ const syncMenuRef = useKeepInView(sync.open);
1485
1515
  const anySync = Object.values(m3.sync).some(Boolean);
1486
1516
  return /* @__PURE__ */ a2($, { children: [
1487
1517
  /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
@@ -1489,7 +1519,7 @@ function LayoutMenu({ ctx, multi }) {
1489
1519
  "button",
1490
1520
  {
1491
1521
  class: "fnc-btn fnc-btn-wide fnc-btn-layout",
1492
- title: t3.layout,
1522
+ "data-tip": t3.layout,
1493
1523
  "aria-label": t3.layout,
1494
1524
  "aria-haspopup": "menu",
1495
1525
  "aria-expanded": layout.open,
@@ -1500,7 +1530,7 @@ function LayoutMenu({ ctx, multi }) {
1500
1530
  ]
1501
1531
  }
1502
1532
  ),
1503
- layout.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-layout-menu", role: "menu", "aria-label": t3.layout, children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-layout-row", children: [
1533
+ layout.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-layout-menu", role: "menu", "aria-label": t3.layout, ref: layoutMenuRef, children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-layout-row", children: [
1504
1534
  /* @__PURE__ */ a2("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1505
1535
  group.map((id) => /* @__PURE__ */ a2(
1506
1536
  "button",
@@ -1509,7 +1539,7 @@ function LayoutMenu({ ctx, multi }) {
1509
1539
  role: "menuitemradio",
1510
1540
  "aria-checked": id === m3.layout,
1511
1541
  "aria-label": `${t3.layout} ${id}`,
1512
- title: id,
1542
+ "data-layout": id,
1513
1543
  onClick: () => {
1514
1544
  multi.setLayout(id);
1515
1545
  layout.setOpen(false);
@@ -1524,7 +1554,7 @@ function LayoutMenu({ ctx, multi }) {
1524
1554
  "button",
1525
1555
  {
1526
1556
  class: "fnc-btn fnc-btn-sync",
1527
- title: t3.sync,
1557
+ "data-tip": t3.sync,
1528
1558
  "aria-label": t3.sync,
1529
1559
  "aria-haspopup": "menu",
1530
1560
  "aria-expanded": sync.open,
@@ -1533,7 +1563,7 @@ function LayoutMenu({ ctx, multi }) {
1533
1563
  children: /* @__PURE__ */ a2(LinkIcon, {})
1534
1564
  }
1535
1565
  ),
1536
- sync.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, children: [
1566
+ sync.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, ref: syncMenuRef, children: [
1537
1567
  /* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: [
1538
1568
  t3.sync,
1539
1569
  /* @__PURE__ */ a2("small", { children: t3.syncHint })
@@ -1712,7 +1742,7 @@ function SymbolSearch({ ctx }) {
1712
1742
  class: `fnc-sr-star${fav ? " is-on" : ""}`,
1713
1743
  "aria-pressed": fav,
1714
1744
  "aria-label": `${info.name} ${fav ? t3.removeFavorite : t3.addFavorite}`,
1715
- title: fav ? t3.removeFavorite : t3.addFavorite,
1745
+ "data-tip": fav ? t3.removeFavorite : t3.addFavorite,
1716
1746
  onClick: (e3) => {
1717
1747
  e3.stopPropagation();
1718
1748
  toggleFavorite(info);
@@ -1747,7 +1777,7 @@ function SymbolSearch({ ctx }) {
1747
1777
  {
1748
1778
  class: "fnc-ind-btn",
1749
1779
  "aria-label": `${info.name} ${t3.removeRecent}`,
1750
- title: t3.removeRecent,
1780
+ "data-tip": t3.removeRecent,
1751
1781
  onClick: (e3) => {
1752
1782
  e3.stopPropagation();
1753
1783
  save(RECENT_KEY, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
@@ -1785,7 +1815,7 @@ function SymbolSearch({ ctx }) {
1785
1815
  }
1786
1816
  ),
1787
1817
  query && /* @__PURE__ */ a2("button", { class: "fnc-ind-btn", "aria-label": t3.cancel, onClick: () => (setQuery(""), inputRef.current?.focus()), children: /* @__PURE__ */ a2(SmallCloseIcon, {}) }),
1788
- /* @__PURE__ */ a2("button", { class: "fnc-kbd fnc-kbd-btn fnc-hide-mobile", "aria-label": t3.close, title: t3.close, onClick: close, children: "ESC" })
1818
+ /* @__PURE__ */ a2("button", { class: "fnc-kbd fnc-kbd-btn fnc-hide-mobile", "aria-label": t3.close, "data-tip": t3.close, onClick: close, children: "ESC" })
1789
1819
  ] }),
1790
1820
  /* @__PURE__ */ a2("div", { class: "fnc-tabs", role: "tablist", children: ctx.searchCategories.map((c3) => /* @__PURE__ */ a2("button", { class: "fnc-tab", role: "tab", "aria-selected": c3.id === category, onClick: () => setCategory(c3.id), children: c3.label[locale] })) }),
1791
1821
  /* @__PURE__ */ a2("div", { class: "fnc-sr-body", children: [
@@ -1849,7 +1879,7 @@ function Toolbar({ ctx }) {
1849
1879
  {
1850
1880
  class: "fnc-symbol",
1851
1881
  disabled: !canSearch,
1852
- title: canSearch ? t3.searchSymbol : void 0,
1882
+ "data-tip": canSearch ? t3.searchSymbol : void 0,
1853
1883
  "aria-haspopup": "dialog",
1854
1884
  onClick: () => ctx.setSearchOpen(true),
1855
1885
  children: [
@@ -1868,8 +1898,8 @@ function Toolbar({ ctx }) {
1868
1898
  /* @__PURE__ */ a2(
1869
1899
  "button",
1870
1900
  {
1871
- class: "fnc-btn fnc-btn-wide fnc-btn-label",
1872
- title: t3.addIndicator,
1901
+ class: "fnc-btn fnc-btn-wide fnc-btn-label fnc-btn-indicators",
1902
+ "data-tip": t3.addIndicator,
1873
1903
  "aria-label": t3.addIndicator,
1874
1904
  "aria-haspopup": "dialog",
1875
1905
  onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
@@ -1886,7 +1916,7 @@ function Toolbar({ ctx }) {
1886
1916
  "button",
1887
1917
  {
1888
1918
  class: "fnc-btn fnc-btn-drawbar",
1889
- title: t3.drawingTools,
1919
+ "data-tip": t3.drawingTools,
1890
1920
  "aria-label": t3.drawingTools,
1891
1921
  "aria-pressed": s3.drawbarOpen || s3.drawingTool !== null,
1892
1922
  onClick: () => ctx.store.set({ drawbarOpen: !s3.drawbarOpen }),
@@ -1897,20 +1927,20 @@ function Toolbar({ ctx }) {
1897
1927
  /* @__PURE__ */ a2(
1898
1928
  "button",
1899
1929
  {
1900
- class: "fnc-btn",
1901
- title: t3.chartSettings,
1930
+ class: "fnc-btn fnc-btn-settings",
1931
+ "data-tip": t3.chartSettings,
1902
1932
  "aria-label": t3.chartSettings,
1903
1933
  "aria-haspopup": "dialog",
1904
1934
  onClick: () => ctx.store.set({ dialog: { kind: "chartSettings" } }),
1905
1935
  children: /* @__PURE__ */ a2(SettingsIcon, {})
1906
1936
  }
1907
1937
  ),
1908
- /* @__PURE__ */ a2("button", { class: "fnc-btn fnc-btn-screenshot", title: t3.screenshot, "aria-label": t3.screenshot, onClick: ctx.screenshot, children: /* @__PURE__ */ a2(CameraIcon, {}) }),
1938
+ /* @__PURE__ */ a2("button", { class: "fnc-btn fnc-btn-screenshot", "data-tip": t3.screenshot, "aria-label": t3.screenshot, onClick: ctx.screenshot, children: /* @__PURE__ */ a2(CameraIcon, {}) }),
1909
1939
  /* @__PURE__ */ a2(
1910
1940
  "button",
1911
1941
  {
1912
- class: "fnc-btn",
1913
- title: s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1942
+ class: "fnc-btn fnc-btn-fullscreen",
1943
+ "data-tip": s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1914
1944
  "aria-label": s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1915
1945
  onClick: ctx.toggleFullscreen,
1916
1946
  children: s3.fullscreen ? /* @__PURE__ */ a2(CollapseIcon, {}) : /* @__PURE__ */ a2(ExpandIcon, {})
@@ -1948,7 +1978,7 @@ function IndicatorItem({ ctx, item }) {
1948
1978
  "button",
1949
1979
  {
1950
1980
  class: "fnc-ind-btn fnc-ind-eye",
1951
- title: item.visible ? t3.hide : t3.show,
1981
+ "data-tip": item.visible ? t3.hide : t3.show,
1952
1982
  "aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
1953
1983
  "aria-pressed": !item.visible,
1954
1984
  onClick: (e3) => {
@@ -1959,13 +1989,13 @@ function IndicatorItem({ ctx, item }) {
1959
1989
  }
1960
1990
  ),
1961
1991
  /* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
1962
- item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, title: v3.label, children: v3.value })),
1992
+ item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, "data-tip": v3.label, children: v3.value })),
1963
1993
  /* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
1964
1994
  /* @__PURE__ */ a2(
1965
1995
  "button",
1966
1996
  {
1967
1997
  class: "fnc-ind-btn",
1968
- title: t3.indicatorSettings,
1998
+ "data-tip": t3.indicatorSettings,
1969
1999
  "aria-label": `${item.title} ${t3.indicatorSettings}`,
1970
2000
  onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1971
2001
  children: /* @__PURE__ */ a2(GearIcon, {})
@@ -1975,7 +2005,7 @@ function IndicatorItem({ ctx, item }) {
1975
2005
  "button",
1976
2006
  {
1977
2007
  class: "fnc-ind-btn",
1978
- title: t3.removeIndicator,
2008
+ "data-tip": t3.removeIndicator,
1979
2009
  "aria-label": `${item.title} ${t3.removeIndicator}`,
1980
2010
  onClick: () => ctx.chart.removeIndicator(item.id),
1981
2011
  children: /* @__PURE__ */ a2(SmallCloseIcon, {})
@@ -2074,7 +2104,7 @@ function Legend({ ctx }) {
2074
2104
  ] }),
2075
2105
  values,
2076
2106
  !collapsed && overlayItems.map((item) => /* @__PURE__ */ a2(IndicatorItem, { ctx, item })),
2077
- overlayItems.length > 0 && (collapsed ? /* @__PURE__ */ a2("button", { class: "fnc-ind-handle", "aria-expanded": "false", "aria-label": t3.expand, title: t3.expand, onClick: () => setCollapsed(false), children: /* @__PURE__ */ a2("span", {}) }) : /* @__PURE__ */ a2("button", { class: "fnc-ind-collapse", "aria-expanded": "true", "aria-label": t3.collapse, title: t3.collapse, onClick: () => setCollapsed(true), children: /* @__PURE__ */ a2(CollapseLeftIcon, {}) }))
2107
+ overlayItems.length > 0 && (collapsed ? /* @__PURE__ */ a2("button", { class: "fnc-ind-handle", "aria-expanded": "false", "aria-label": t3.expand, "data-tip": t3.expand, onClick: () => setCollapsed(false), children: /* @__PURE__ */ a2("span", {}) }) : /* @__PURE__ */ a2("button", { class: "fnc-ind-collapse", "aria-expanded": "true", "aria-label": t3.collapse, "data-tip": t3.collapse, onClick: () => setCollapsed(true), children: /* @__PURE__ */ a2(CollapseLeftIcon, {}) }))
2078
2108
  ] }),
2079
2109
  paneLegends.map(
2080
2110
  ({ pane, item }) => item && /* @__PURE__ */ a2("div", { class: "fnc-legend fnc-pane-legend", style: { top: `${pane.top + 4}px` }, children: /* @__PURE__ */ a2(IndicatorItem, { ctx, item }) })
@@ -2090,7 +2120,7 @@ function NavControls({ ctx }) {
2090
2120
  if (plotWidth <= 0) return null;
2091
2121
  const top = panesHeight - CONTROL_OFFSET;
2092
2122
  const page = () => Math.max(1, Math.round(plotWidth / ctx.chart.timeScale.barSpacing / 3));
2093
- const button = (label, onClick, icon) => /* @__PURE__ */ a2("button", { class: "fnc-btn", title: label, "aria-label": label, onClick, children: icon });
2123
+ const button = (label, onClick, icon) => /* @__PURE__ */ a2("button", { class: "fnc-btn", "data-tip": label, "aria-label": label, onClick, children: icon });
2094
2124
  return /* @__PURE__ */ a2($, { children: [
2095
2125
  showButtons && /* @__PURE__ */ a2("div", { class: "fnc-nav", style: { left: `${plotWidth / 2}px`, top: `${top}px` }, children: [
2096
2126
  button(t3.zoomOut, () => ctx.chart.zoom(1 / ZOOM_STEP), /* @__PURE__ */ a2(MinusIcon, {})),
@@ -2104,7 +2134,7 @@ function NavControls({ ctx }) {
2104
2134
  {
2105
2135
  class: "fnc-realtime",
2106
2136
  style: { left: `${plotWidth - 44}px`, top: `${top}px` },
2107
- title: t3.toRealtime,
2137
+ "data-tip": t3.toRealtime,
2108
2138
  "aria-label": t3.toRealtime,
2109
2139
  onClick: () => ctx.chart.scrollToRealtime(),
2110
2140
  children: /* @__PURE__ */ a2(ToRealtimeIcon, {})
@@ -2125,7 +2155,7 @@ function Attribution({ ctx }) {
2125
2155
  href: ATTRIBUTION_URL,
2126
2156
  target: "_blank",
2127
2157
  rel: "noopener",
2128
- title: label,
2158
+ "data-tip": label,
2129
2159
  "aria-label": label,
2130
2160
  style: { top: `${panesHeight - 36}px` },
2131
2161
  children: [
@@ -2150,7 +2180,7 @@ function PaneMaximize({ ctx, multi }) {
2150
2180
  {
2151
2181
  class: "fnc-btn fnc-pane-max",
2152
2182
  style: { left: `${s3.layout.plotWidth - 36}px` },
2153
- title: `${label} (Alt+Enter)`,
2183
+ "data-tip": `${label} (Alt+Enter)`,
2154
2184
  "aria-label": label,
2155
2185
  "aria-pressed": maximized,
2156
2186
  onClick: () => {
@@ -2696,6 +2726,38 @@ var CSS = (
2696
2726
  flex: none;
2697
2727
  }
2698
2728
 
2729
+ /* Tooltip (data-tip). Hosts can restyle it with --fnc-tip-bg / --fnc-tip-text. */
2730
+ .fnc-tip {
2731
+ position: fixed;
2732
+ z-index: 2147483001;
2733
+ max-width: 240px;
2734
+ padding: 6px 10px;
2735
+ border-radius: 8px;
2736
+ background: var(--fnc-tip-bg, #24242b);
2737
+ color: var(--fnc-tip-text, #f2f2f4);
2738
+ font-family: var(--fnc-font);
2739
+ font-size: 12px;
2740
+ font-weight: 500;
2741
+ line-height: 1.4;
2742
+ white-space: nowrap;
2743
+ box-shadow: 0 6px 20px rgb(0 0 0 / 0.28);
2744
+ pointer-events: none;
2745
+ opacity: 0;
2746
+ visibility: hidden;
2747
+ transition: opacity 120ms cubic-bezier(0.4, 0, 1, 1), transform 120ms cubic-bezier(0.4, 0, 1, 1), visibility 0s 120ms;
2748
+ }
2749
+ .fnc-tip[data-side='bottom'] { transform: translateY(-4px); }
2750
+ .fnc-tip[data-side='top'] { transform: translateY(4px); }
2751
+ .fnc-tip[data-side='right'] { transform: translateX(-4px); }
2752
+ .fnc-tip.is-on {
2753
+ opacity: 1;
2754
+ visibility: visible;
2755
+ transform: none;
2756
+ transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s;
2757
+ }
2758
+ @media (prefers-reduced-motion: reduce) { .fnc-tip, .fnc-tip.is-on { transform: none; } }
2759
+ @media (hover: none) { .fnc-tip { display: none; } }
2760
+
2699
2761
  .fnc-tools { display: flex; align-items: center; gap: 4px; flex: none; }
2700
2762
  /* Right-hand menus open toward the left so they stay on screen. */
2701
2763
  .fnc-tools .fnc-menu { left: auto; right: 0; }
@@ -3116,6 +3178,77 @@ function div(className, parent) {
3116
3178
  parent?.appendChild(el);
3117
3179
  return el;
3118
3180
  }
3181
+ function bindTooltips(root) {
3182
+ const tip = div("fnc-tip", root);
3183
+ tip.setAttribute("role", "tooltip");
3184
+ tip.id = `fnc-tip-${Math.random().toString(36).slice(2, 8)}`;
3185
+ let current = null;
3186
+ let timer;
3187
+ let hiddenAt = 0;
3188
+ const place = (el) => {
3189
+ const r3 = el.getBoundingClientRect();
3190
+ const w3 = tip.offsetWidth;
3191
+ const h3 = tip.offsetHeight;
3192
+ const vw = window.innerWidth;
3193
+ const vh = window.innerHeight;
3194
+ let side = el.closest(".fnc-drawbar") ? "right" : el.closest(".fnc-nav") ? "top" : "bottom";
3195
+ if (side === "bottom" && r3.bottom + h3 + 10 > vh) side = "top";
3196
+ let left = side === "right" ? r3.right + 8 : r3.left + r3.width / 2 - w3 / 2;
3197
+ const top = side === "right" ? r3.top + r3.height / 2 - h3 / 2 : side === "top" ? r3.top - h3 - 6 : r3.bottom + 6;
3198
+ left = Math.max(4, Math.min(vw - w3 - 4, left));
3199
+ tip.style.left = `${Math.round(left)}px`;
3200
+ tip.style.top = `${Math.round(top)}px`;
3201
+ tip.dataset.side = side;
3202
+ };
3203
+ const show = (el) => {
3204
+ const text = el.dataset.tip;
3205
+ if (!text || !el.isConnected) return;
3206
+ tip.textContent = text;
3207
+ el.setAttribute("aria-describedby", tip.id);
3208
+ place(el);
3209
+ tip.classList.add("is-on");
3210
+ };
3211
+ const hide = () => {
3212
+ clearTimeout(timer);
3213
+ if (tip.classList.contains("is-on")) hiddenAt = Date.now();
3214
+ tip.classList.remove("is-on");
3215
+ current?.removeAttribute("aria-describedby");
3216
+ current = null;
3217
+ };
3218
+ const target = (e3) => e3.target instanceof Element ? e3.target.closest("[data-tip]") : null;
3219
+ const onOver = (e3) => {
3220
+ if (e3.pointerType === "touch") return;
3221
+ const el = target(e3);
3222
+ if (el === current) return;
3223
+ hide();
3224
+ if (!el || !root.contains(el)) return;
3225
+ current = el;
3226
+ timer = setTimeout(() => show(el), Date.now() - hiddenAt < 300 ? 0 : 350);
3227
+ };
3228
+ const onFocus = (e3) => {
3229
+ const el = target(e3);
3230
+ if (!el || !e3.target.matches?.(":focus-visible")) return;
3231
+ hide();
3232
+ current = el;
3233
+ show(el);
3234
+ };
3235
+ root.addEventListener("pointerover", onOver);
3236
+ root.addEventListener("pointerleave", hide);
3237
+ root.addEventListener("pointerdown", hide, true);
3238
+ root.addEventListener("wheel", hide, { passive: true });
3239
+ root.addEventListener("focusin", onFocus);
3240
+ root.addEventListener("focusout", hide);
3241
+ return () => {
3242
+ hide();
3243
+ root.removeEventListener("pointerover", onOver);
3244
+ root.removeEventListener("pointerleave", hide);
3245
+ root.removeEventListener("pointerdown", hide, true);
3246
+ root.removeEventListener("wheel", hide);
3247
+ root.removeEventListener("focusin", onFocus);
3248
+ root.removeEventListener("focusout", hide);
3249
+ tip.remove();
3250
+ };
3251
+ }
3119
3252
  function createShell(container, options) {
3120
3253
  injectStyles(container.ownerDocument);
3121
3254
  const storage = createStorage(options.storageKey);
@@ -3142,6 +3275,7 @@ function createShell(container, options) {
3142
3275
  bottom: div("fnc-slot fnc-slot-bottom"),
3143
3276
  footer: div("fnc-slot fnc-slot-footer")
3144
3277
  };
3278
+ const unbindTips = bindTooltips(root);
3145
3279
  const body = div("fnc-body", root);
3146
3280
  const drawbarHost = options.drawingToolbar ? div("fnc-drawbar-host", body) : null;
3147
3281
  const main = div("fnc-main", body);
@@ -3207,6 +3341,7 @@ function createShell(container, options) {
3207
3341
  options.onFavoriteIntervalsChange?.(favoriteIntervals);
3208
3342
  },
3209
3343
  destroy: () => {
3344
+ unbindTips();
3210
3345
  document.removeEventListener("fullscreenchange", onFullscreenChange);
3211
3346
  darkQuery?.removeEventListener?.("change", onSchemeChange);
3212
3347
  if (isNativeFullscreen()) void document.exitFullscreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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.5.1"
45
+ "@funnest/charts": "0.5.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"