@funnest/charts-ui 0.5.0 → 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
  }
@@ -1450,6 +1478,7 @@ function layoutRects(node, x2 = 0, y3 = 0, w3 = 20, h3 = 16, gap = 1.6) {
1450
1478
  }
1451
1479
 
1452
1480
  // src/components/layout-menu.tsx
1481
+ var LAYOUT_PANES = (id) => paneCount(LAYOUTS[id]);
1453
1482
  function LayoutGlyph({ id }) {
1454
1483
  return /* @__PURE__ */ a2("svg", { width: "20", height: "16", viewBox: "0 0 20 16", "aria-hidden": "true", children: layoutRects(LAYOUTS[id]).map((r3) => /* @__PURE__ */ a2("rect", { x: r3.x + 0.5, y: r3.y + 0.5, width: Math.max(0, r3.w - 1), height: Math.max(0, r3.h - 1), rx: "1.5", fill: "none", stroke: "currentColor", "stroke-width": "1.2" })) });
1455
1484
  }
@@ -1481,6 +1510,8 @@ function LayoutMenu({ ctx, multi }) {
1481
1510
  const t3 = STRINGS[s3.locale];
1482
1511
  const layout = usePopover();
1483
1512
  const sync = usePopover();
1513
+ const layoutMenuRef = useKeepInView(layout.open);
1514
+ const syncMenuRef = useKeepInView(sync.open);
1484
1515
  const anySync = Object.values(m3.sync).some(Boolean);
1485
1516
  return /* @__PURE__ */ a2($, { children: [
1486
1517
  /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
@@ -1488,7 +1519,7 @@ function LayoutMenu({ ctx, multi }) {
1488
1519
  "button",
1489
1520
  {
1490
1521
  class: "fnc-btn fnc-btn-wide fnc-btn-layout",
1491
- title: t3.layout,
1522
+ "data-tip": t3.layout,
1492
1523
  "aria-label": t3.layout,
1493
1524
  "aria-haspopup": "menu",
1494
1525
  "aria-expanded": layout.open,
@@ -1499,7 +1530,7 @@ function LayoutMenu({ ctx, multi }) {
1499
1530
  ]
1500
1531
  }
1501
1532
  ),
1502
- 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: [
1503
1534
  /* @__PURE__ */ a2("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1504
1535
  group.map((id) => /* @__PURE__ */ a2(
1505
1536
  "button",
@@ -1508,7 +1539,7 @@ function LayoutMenu({ ctx, multi }) {
1508
1539
  role: "menuitemradio",
1509
1540
  "aria-checked": id === m3.layout,
1510
1541
  "aria-label": `${t3.layout} ${id}`,
1511
- title: id,
1542
+ "data-layout": id,
1512
1543
  onClick: () => {
1513
1544
  multi.setLayout(id);
1514
1545
  layout.setOpen(false);
@@ -1518,12 +1549,12 @@ function LayoutMenu({ ctx, multi }) {
1518
1549
  ))
1519
1550
  ] })) })
1520
1551
  ] }),
1521
- /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
1552
+ LAYOUT_PANES(m3.layout) > 1 && /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
1522
1553
  /* @__PURE__ */ a2(
1523
1554
  "button",
1524
1555
  {
1525
1556
  class: "fnc-btn fnc-btn-sync",
1526
- title: t3.sync,
1557
+ "data-tip": t3.sync,
1527
1558
  "aria-label": t3.sync,
1528
1559
  "aria-haspopup": "menu",
1529
1560
  "aria-expanded": sync.open,
@@ -1532,7 +1563,7 @@ function LayoutMenu({ ctx, multi }) {
1532
1563
  children: /* @__PURE__ */ a2(LinkIcon, {})
1533
1564
  }
1534
1565
  ),
1535
- 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: [
1536
1567
  /* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: [
1537
1568
  t3.sync,
1538
1569
  /* @__PURE__ */ a2("small", { children: t3.syncHint })
@@ -1711,7 +1742,7 @@ function SymbolSearch({ ctx }) {
1711
1742
  class: `fnc-sr-star${fav ? " is-on" : ""}`,
1712
1743
  "aria-pressed": fav,
1713
1744
  "aria-label": `${info.name} ${fav ? t3.removeFavorite : t3.addFavorite}`,
1714
- title: fav ? t3.removeFavorite : t3.addFavorite,
1745
+ "data-tip": fav ? t3.removeFavorite : t3.addFavorite,
1715
1746
  onClick: (e3) => {
1716
1747
  e3.stopPropagation();
1717
1748
  toggleFavorite(info);
@@ -1746,7 +1777,7 @@ function SymbolSearch({ ctx }) {
1746
1777
  {
1747
1778
  class: "fnc-ind-btn",
1748
1779
  "aria-label": `${info.name} ${t3.removeRecent}`,
1749
- title: t3.removeRecent,
1780
+ "data-tip": t3.removeRecent,
1750
1781
  onClick: (e3) => {
1751
1782
  e3.stopPropagation();
1752
1783
  save(RECENT_KEY, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
@@ -1784,7 +1815,7 @@ function SymbolSearch({ ctx }) {
1784
1815
  }
1785
1816
  ),
1786
1817
  query && /* @__PURE__ */ a2("button", { class: "fnc-ind-btn", "aria-label": t3.cancel, onClick: () => (setQuery(""), inputRef.current?.focus()), children: /* @__PURE__ */ a2(SmallCloseIcon, {}) }),
1787
- /* @__PURE__ */ a2("kbd", { class: "fnc-kbd fnc-hide-mobile", 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" })
1788
1819
  ] }),
1789
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] })) }),
1790
1821
  /* @__PURE__ */ a2("div", { class: "fnc-sr-body", children: [
@@ -1819,7 +1850,7 @@ function SymbolSearch({ ctx }) {
1819
1850
  " ",
1820
1851
  t3.openAction
1821
1852
  ] }),
1822
- /* @__PURE__ */ a2("span", { children: [
1853
+ /* @__PURE__ */ a2("button", { class: "fnc-sr-foot-btn", onClick: close, children: [
1823
1854
  /* @__PURE__ */ a2("kbd", { class: "fnc-kbd", children: "ESC" }),
1824
1855
  " ",
1825
1856
  t3.close
@@ -1848,7 +1879,7 @@ function Toolbar({ ctx }) {
1848
1879
  {
1849
1880
  class: "fnc-symbol",
1850
1881
  disabled: !canSearch,
1851
- title: canSearch ? t3.searchSymbol : void 0,
1882
+ "data-tip": canSearch ? t3.searchSymbol : void 0,
1852
1883
  "aria-haspopup": "dialog",
1853
1884
  onClick: () => ctx.setSearchOpen(true),
1854
1885
  children: [
@@ -1861,29 +1892,31 @@ function Toolbar({ ctx }) {
1861
1892
  /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1862
1893
  /* @__PURE__ */ a2(IntervalPicker, { ctx }),
1863
1894
  /* @__PURE__ */ a2(Slot, { el: ctx.slots.toolbar }),
1864
- /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1865
- /* @__PURE__ */ a2(ChartTypeMenu, { ctx }),
1866
- /* @__PURE__ */ a2(
1867
- "button",
1868
- {
1869
- class: "fnc-btn fnc-btn-wide fnc-btn-label",
1870
- title: t3.addIndicator,
1871
- "aria-label": t3.addIndicator,
1872
- "aria-haspopup": "dialog",
1873
- onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
1874
- children: [
1875
- /* @__PURE__ */ a2(IndicatorIcon, {}),
1876
- /* @__PURE__ */ a2("span", { children: t3.indicators })
1877
- ]
1878
- }
1879
- ),
1880
- ctx.multi && /* @__PURE__ */ a2(LayoutMenu, { ctx, multi: ctx.multi }),
1881
1895
  /* @__PURE__ */ a2("div", { class: "fnc-spacer" }),
1896
+ /* @__PURE__ */ a2("div", { class: "fnc-tools", children: [
1897
+ /* @__PURE__ */ a2(ChartTypeMenu, { ctx }),
1898
+ /* @__PURE__ */ a2(
1899
+ "button",
1900
+ {
1901
+ class: "fnc-btn fnc-btn-wide fnc-btn-label fnc-btn-indicators",
1902
+ "data-tip": t3.addIndicator,
1903
+ "aria-label": t3.addIndicator,
1904
+ "aria-haspopup": "dialog",
1905
+ onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
1906
+ children: [
1907
+ /* @__PURE__ */ a2(IndicatorIcon, {}),
1908
+ /* @__PURE__ */ a2("span", { children: t3.indicators })
1909
+ ]
1910
+ }
1911
+ ),
1912
+ ctx.multi && /* @__PURE__ */ a2(LayoutMenu, { ctx, multi: ctx.multi })
1913
+ ] }),
1914
+ /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1882
1915
  ctx.drawingToolbar && /* @__PURE__ */ a2(
1883
1916
  "button",
1884
1917
  {
1885
1918
  class: "fnc-btn fnc-btn-drawbar",
1886
- title: t3.drawingTools,
1919
+ "data-tip": t3.drawingTools,
1887
1920
  "aria-label": t3.drawingTools,
1888
1921
  "aria-pressed": s3.drawbarOpen || s3.drawingTool !== null,
1889
1922
  onClick: () => ctx.store.set({ drawbarOpen: !s3.drawbarOpen }),
@@ -1894,20 +1927,20 @@ function Toolbar({ ctx }) {
1894
1927
  /* @__PURE__ */ a2(
1895
1928
  "button",
1896
1929
  {
1897
- class: "fnc-btn",
1898
- title: t3.chartSettings,
1930
+ class: "fnc-btn fnc-btn-settings",
1931
+ "data-tip": t3.chartSettings,
1899
1932
  "aria-label": t3.chartSettings,
1900
1933
  "aria-haspopup": "dialog",
1901
1934
  onClick: () => ctx.store.set({ dialog: { kind: "chartSettings" } }),
1902
1935
  children: /* @__PURE__ */ a2(SettingsIcon, {})
1903
1936
  }
1904
1937
  ),
1905
- /* @__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, {}) }),
1906
1939
  /* @__PURE__ */ a2(
1907
1940
  "button",
1908
1941
  {
1909
- class: "fnc-btn",
1910
- title: s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1942
+ class: "fnc-btn fnc-btn-fullscreen",
1943
+ "data-tip": s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1911
1944
  "aria-label": s3.fullscreen ? t3.exitFullscreen : t3.fullscreen,
1912
1945
  onClick: ctx.toggleFullscreen,
1913
1946
  children: s3.fullscreen ? /* @__PURE__ */ a2(CollapseIcon, {}) : /* @__PURE__ */ a2(ExpandIcon, {})
@@ -1945,7 +1978,7 @@ function IndicatorItem({ ctx, item }) {
1945
1978
  "button",
1946
1979
  {
1947
1980
  class: "fnc-ind-btn fnc-ind-eye",
1948
- title: item.visible ? t3.hide : t3.show,
1981
+ "data-tip": item.visible ? t3.hide : t3.show,
1949
1982
  "aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
1950
1983
  "aria-pressed": !item.visible,
1951
1984
  onClick: (e3) => {
@@ -1956,13 +1989,13 @@ function IndicatorItem({ ctx, item }) {
1956
1989
  }
1957
1990
  ),
1958
1991
  /* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
1959
- 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 })),
1960
1993
  /* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
1961
1994
  /* @__PURE__ */ a2(
1962
1995
  "button",
1963
1996
  {
1964
1997
  class: "fnc-ind-btn",
1965
- title: t3.indicatorSettings,
1998
+ "data-tip": t3.indicatorSettings,
1966
1999
  "aria-label": `${item.title} ${t3.indicatorSettings}`,
1967
2000
  onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1968
2001
  children: /* @__PURE__ */ a2(GearIcon, {})
@@ -1972,7 +2005,7 @@ function IndicatorItem({ ctx, item }) {
1972
2005
  "button",
1973
2006
  {
1974
2007
  class: "fnc-ind-btn",
1975
- title: t3.removeIndicator,
2008
+ "data-tip": t3.removeIndicator,
1976
2009
  "aria-label": `${item.title} ${t3.removeIndicator}`,
1977
2010
  onClick: () => ctx.chart.removeIndicator(item.id),
1978
2011
  children: /* @__PURE__ */ a2(SmallCloseIcon, {})
@@ -2071,7 +2104,7 @@ function Legend({ ctx }) {
2071
2104
  ] }),
2072
2105
  values,
2073
2106
  !collapsed && overlayItems.map((item) => /* @__PURE__ */ a2(IndicatorItem, { ctx, item })),
2074
- 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, {}) }))
2075
2108
  ] }),
2076
2109
  paneLegends.map(
2077
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 }) })
@@ -2087,7 +2120,7 @@ function NavControls({ ctx }) {
2087
2120
  if (plotWidth <= 0) return null;
2088
2121
  const top = panesHeight - CONTROL_OFFSET;
2089
2122
  const page = () => Math.max(1, Math.round(plotWidth / ctx.chart.timeScale.barSpacing / 3));
2090
- 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 });
2091
2124
  return /* @__PURE__ */ a2($, { children: [
2092
2125
  showButtons && /* @__PURE__ */ a2("div", { class: "fnc-nav", style: { left: `${plotWidth / 2}px`, top: `${top}px` }, children: [
2093
2126
  button(t3.zoomOut, () => ctx.chart.zoom(1 / ZOOM_STEP), /* @__PURE__ */ a2(MinusIcon, {})),
@@ -2101,7 +2134,7 @@ function NavControls({ ctx }) {
2101
2134
  {
2102
2135
  class: "fnc-realtime",
2103
2136
  style: { left: `${plotWidth - 44}px`, top: `${top}px` },
2104
- title: t3.toRealtime,
2137
+ "data-tip": t3.toRealtime,
2105
2138
  "aria-label": t3.toRealtime,
2106
2139
  onClick: () => ctx.chart.scrollToRealtime(),
2107
2140
  children: /* @__PURE__ */ a2(ToRealtimeIcon, {})
@@ -2122,7 +2155,7 @@ function Attribution({ ctx }) {
2122
2155
  href: ATTRIBUTION_URL,
2123
2156
  target: "_blank",
2124
2157
  rel: "noopener",
2125
- title: label,
2158
+ "data-tip": label,
2126
2159
  "aria-label": label,
2127
2160
  style: { top: `${panesHeight - 36}px` },
2128
2161
  children: [
@@ -2147,7 +2180,7 @@ function PaneMaximize({ ctx, multi }) {
2147
2180
  {
2148
2181
  class: "fnc-btn fnc-pane-max",
2149
2182
  style: { left: `${s3.layout.plotWidth - 36}px` },
2150
- title: `${label} (Alt+Enter)`,
2183
+ "data-tip": `${label} (Alt+Enter)`,
2151
2184
  "aria-label": label,
2152
2185
  "aria-pressed": maximized,
2153
2186
  onClick: () => {
@@ -2693,6 +2726,45 @@ var CSS = (
2693
2726
  flex: none;
2694
2727
  }
2695
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
+
2761
+ .fnc-tools { display: flex; align-items: center; gap: 4px; flex: none; }
2762
+ /* Right-hand menus open toward the left so they stay on screen. */
2763
+ .fnc-tools .fnc-menu { left: auto; right: 0; }
2764
+ .fnc-kbd-btn { cursor: pointer; }
2765
+ .fnc-kbd-btn:hover, .fnc-sr-foot-btn:hover { color: var(--fnc-text); }
2766
+ .fnc-sr-foot-btn { display: inline-flex; align-items: center; gap: 4px; font-size: inherit; color: inherit; cursor: pointer; }
2767
+
2696
2768
  .fnc-up { color: var(--fnc-up); }
2697
2769
  .fnc-down { color: var(--fnc-down); }
2698
2770
  .fnc-flat { color: var(--fnc-text); }
@@ -2943,7 +3015,7 @@ var CSS = (
2943
3015
  content: '';
2944
3016
  position: absolute;
2945
3017
  inset: 0;
2946
- border: 1px solid var(--fnc-accent);
3018
+ border: 1px solid var(--fnc-text-2);
2947
3019
  pointer-events: none;
2948
3020
  z-index: 3;
2949
3021
  }
@@ -2953,7 +3025,7 @@ var CSS = (
2953
3025
  .fnc-splitter-row::after { inset: 0 -4px; }
2954
3026
  .fnc-splitter-col { height: 1px; cursor: row-resize; }
2955
3027
  .fnc-splitter-col::after { inset: -4px 0; }
2956
- .fnc-splitter:hover { background: var(--fnc-accent); }
3028
+ .fnc-splitter:hover { background: var(--fnc-text-2); }
2957
3029
  .fnc-resizing-x, .fnc-resizing-x * { cursor: col-resize !important; user-select: none; }
2958
3030
  .fnc-resizing-y, .fnc-resizing-y * { cursor: row-resize !important; user-select: none; }
2959
3031
  .fnc-parking { display: none; }
@@ -3004,7 +3076,7 @@ var CSS = (
3004
3076
  background: var(--fnc-surface);
3005
3077
  font-size: 11px;
3006
3078
  }
3007
- .fnc-pane-tab[aria-selected='true'] .fnc-pane-tab-num { background: var(--fnc-accent); color: #fff; }
3079
+ .fnc-pane-tab[aria-selected='true'] .fnc-pane-tab-num { background: var(--fnc-text-2); color: var(--fnc-bg); }
3008
3080
  .fnc-layout-menu { min-width: 0; gap: 2px; padding: 8px; }
3009
3081
  .fnc-layout-row { display: flex; align-items: center; gap: 2px; }
3010
3082
  .fnc-layout-count { width: 16px; flex: none; font-size: 12px; color: var(--fnc-text-2); text-align: center; }
@@ -3017,7 +3089,7 @@ var CSS = (
3017
3089
  color: var(--fnc-text-2);
3018
3090
  }
3019
3091
  .fnc-layout-opt:hover { background: var(--fnc-surface); color: var(--fnc-text); }
3020
- .fnc-layout-opt[aria-checked='true'] { background: var(--fnc-surface); color: var(--fnc-accent); }
3092
+ .fnc-layout-opt[aria-checked='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3021
3093
  .fnc-sync-menu { min-width: 220px; }
3022
3094
  .fnc-menu-title { display: flex; flex-direction: column; gap: 2px; padding: 6px 10px 8px; font-size: 13px; font-weight: 600; color: var(--fnc-text); }
3023
3095
  .fnc-menu-title small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
@@ -3025,9 +3097,8 @@ var CSS = (
3025
3097
  .fnc-sync-item[aria-checked='true'] { background: none; color: var(--fnc-text); }
3026
3098
  .fnc-sync-item:hover { background: var(--fnc-surface); }
3027
3099
  .fnc-sync-item .fnc-switch { display: inline-block; transform: scale(0.8); transform-origin: right center; }
3028
- .fnc-switch[data-on='true'] { background: var(--fnc-accent); }
3100
+ .fnc-switch[data-on='true'] { background: var(--fnc-text-2); }
3029
3101
  .fnc-switch[data-on='true']::after { transform: translateX(16px); }
3030
- .fnc-btn-sync[aria-pressed='true'] { color: var(--fnc-accent); }
3031
3102
 
3032
3103
  /* \u2500\u2500 Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3033
3104
  @media (max-width: 640px) {
@@ -3107,6 +3178,77 @@ function div(className, parent) {
3107
3178
  parent?.appendChild(el);
3108
3179
  return el;
3109
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
+ }
3110
3252
  function createShell(container, options) {
3111
3253
  injectStyles(container.ownerDocument);
3112
3254
  const storage = createStorage(options.storageKey);
@@ -3133,6 +3275,7 @@ function createShell(container, options) {
3133
3275
  bottom: div("fnc-slot fnc-slot-bottom"),
3134
3276
  footer: div("fnc-slot fnc-slot-footer")
3135
3277
  };
3278
+ const unbindTips = bindTooltips(root);
3136
3279
  const body = div("fnc-body", root);
3137
3280
  const drawbarHost = options.drawingToolbar ? div("fnc-drawbar-host", body) : null;
3138
3281
  const main = div("fnc-main", body);
@@ -3198,6 +3341,7 @@ function createShell(container, options) {
3198
3341
  options.onFavoriteIntervalsChange?.(favoriteIntervals);
3199
3342
  },
3200
3343
  destroy: () => {
3344
+ unbindTips();
3201
3345
  document.removeEventListener("fullscreenchange", onFullscreenChange);
3202
3346
  darkQuery?.removeEventListener?.("change", onSchemeChange);
3203
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.0",
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.0"
45
+ "@funnest/charts": "0.5.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"