@forgecharts/sdk 1.3.9 → 1.3.11

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.
@@ -1,4 +1,4 @@
1
- import React11, { forwardRef, useRef, useState, useImperativeHandle, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
1
+ import React11, { forwardRef, useRef, useState, useImperativeHandle, useEffect, useCallback, createContext, useMemo, useContext, useReducer } from 'react';
2
2
  import { TextStyle, Application, Container, Graphics, Text, FillGradient } from 'pixi.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import ReactDOM from 'react-dom';
@@ -17841,7 +17841,143 @@ var logo_dark_default = "../logo_dark-B2KCSRPJ.png";
17841
17841
 
17842
17842
  // src/react/assets/logo_light.png
17843
17843
  var logo_light_default = "../logo_light-NAWNBY4G.png";
17844
+ var _ws = null;
17845
+ var _listeners = /* @__PURE__ */ new Set();
17846
+ var _subscribed = /* @__PURE__ */ new Set();
17847
+ var _reconnectTimer = null;
17848
+ var _authToken = null;
17844
17849
  function setWsAuthToken(token) {
17850
+ _authToken = token;
17851
+ }
17852
+ function _wsUrl() {
17853
+ const base = typeof window === "undefined" ? "ws://localhost:4001/ws" : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ws`;
17854
+ return _authToken ? `${base}?token=${encodeURIComponent(_authToken)}` : base;
17855
+ }
17856
+ function _send(msg) {
17857
+ if (_ws?.readyState === WebSocket.OPEN) {
17858
+ _ws.send(JSON.stringify(msg));
17859
+ }
17860
+ }
17861
+ function _ensureConnected() {
17862
+ if (_ws && (_ws.readyState === WebSocket.CONNECTING || _ws.readyState === WebSocket.OPEN)) return;
17863
+ if (_reconnectTimer) {
17864
+ clearTimeout(_reconnectTimer);
17865
+ _reconnectTimer = null;
17866
+ }
17867
+ _ws = new WebSocket(_wsUrl());
17868
+ _ws.onopen = () => {
17869
+ for (const sym of _subscribed) {
17870
+ _send({ type: "subscribe_quote", symbol: sym });
17871
+ }
17872
+ };
17873
+ _ws.onmessage = (event) => {
17874
+ try {
17875
+ const msg = JSON.parse(event.data);
17876
+ if (msg.type === "quote") {
17877
+ for (const listener of _listeners) {
17878
+ listener(msg.payload);
17879
+ }
17880
+ }
17881
+ } catch {
17882
+ }
17883
+ };
17884
+ _ws.onclose = () => {
17885
+ _ws = null;
17886
+ if (_subscribed.size > 0) {
17887
+ _reconnectTimer = setTimeout(_ensureConnected, 3e3);
17888
+ }
17889
+ };
17890
+ }
17891
+ function _addListener(fn) {
17892
+ _listeners.add(fn);
17893
+ }
17894
+ function _removeListener(fn) {
17895
+ _listeners.delete(fn);
17896
+ }
17897
+ function _subscribe(symbols) {
17898
+ _ensureConnected();
17899
+ for (const sym of symbols) {
17900
+ if (!_subscribed.has(sym)) {
17901
+ _subscribed.add(sym);
17902
+ _send({ type: "subscribe_quote", symbol: sym });
17903
+ }
17904
+ }
17905
+ }
17906
+ function _unsubscribe(symbols) {
17907
+ for (const sym of symbols) {
17908
+ _subscribed.delete(sym);
17909
+ _send({ type: "unsubscribe_quote", symbol: sym });
17910
+ }
17911
+ if (_subscribed.size === 0 && _ws) {
17912
+ _ws.close();
17913
+ _ws = null;
17914
+ }
17915
+ }
17916
+ function quoteReducer(state, action) {
17917
+ if (action.type === "tick") {
17918
+ const prices = new Map(state.prices);
17919
+ prices.set(action.symbol, action.last);
17920
+ const volumes = action.volume !== void 0 ? new Map(state.volumes).set(action.symbol, action.volume) : state.volumes;
17921
+ if (action.dir) {
17922
+ const dirs = new Map(state.dirs);
17923
+ dirs.set(action.symbol, action.dir);
17924
+ return { prices, dirs, volumes };
17925
+ }
17926
+ return { prices, dirs: state.dirs, volumes };
17927
+ }
17928
+ if (action.type === "clear_dir") {
17929
+ if (!state.dirs.has(action.symbol)) return state;
17930
+ const dirs = new Map(state.dirs);
17931
+ dirs.delete(action.symbol);
17932
+ return { ...state, dirs };
17933
+ }
17934
+ return state;
17935
+ }
17936
+ var FLASH_MS = 300;
17937
+ function useWatchlistQuotes(symbols, enabled = true) {
17938
+ const [state, dispatch] = useReducer(quoteReducer, void 0, () => ({
17939
+ prices: /* @__PURE__ */ new Map(),
17940
+ dirs: /* @__PURE__ */ new Map(),
17941
+ volumes: /* @__PURE__ */ new Map()
17942
+ }));
17943
+ const symbolsKey = symbols.slice().sort().join(",");
17944
+ const flashTimers = useRef(/* @__PURE__ */ new Map());
17945
+ const listenerRef = useRef(null);
17946
+ const onQuote = useCallback((payload) => {
17947
+ if (!_subscribed.has(payload.symbol)) return;
17948
+ const tickAction = { type: "tick", symbol: payload.symbol, last: payload.last, dir: payload.dir };
17949
+ if (payload.volume !== void 0) tickAction.volume = payload.volume;
17950
+ dispatch(tickAction);
17951
+ if (payload.dir) {
17952
+ const existing = flashTimers.current.get(payload.symbol);
17953
+ if (existing !== void 0) clearTimeout(existing);
17954
+ const t = setTimeout(() => {
17955
+ dispatch({ type: "clear_dir", symbol: payload.symbol });
17956
+ flashTimers.current.delete(payload.symbol);
17957
+ }, FLASH_MS);
17958
+ flashTimers.current.set(payload.symbol, t);
17959
+ }
17960
+ }, []);
17961
+ useEffect(() => {
17962
+ if (listenerRef.current) _removeListener(listenerRef.current);
17963
+ listenerRef.current = onQuote;
17964
+ _addListener(onQuote);
17965
+ return () => {
17966
+ if (listenerRef.current) _removeListener(listenerRef.current);
17967
+ listenerRef.current = null;
17968
+ for (const t of flashTimers.current.values()) clearTimeout(t);
17969
+ flashTimers.current.clear();
17970
+ };
17971
+ }, [onQuote]);
17972
+ useEffect(() => {
17973
+ if (!enabled) return;
17974
+ const syms = symbols.filter(Boolean);
17975
+ if (syms.length > 0) _subscribe(syms);
17976
+ return () => {
17977
+ if (syms.length > 0) _unsubscribe(syms);
17978
+ };
17979
+ }, [symbolsKey, enabled]);
17980
+ return { prices: state.prices, dirs: state.dirs, volumes: state.volumes };
17845
17981
  }
17846
17982
 
17847
17983
  // src/react/assets/Rithmic.png
@@ -29688,6 +29824,666 @@ function ScriptDrawer({ onClose, onAddIndicator, apiUrl, getAuthToken }) {
29688
29824
  ] })
29689
29825
  ] });
29690
29826
  }
29827
+
29828
+ // src/react/shell/watchlistApi.ts
29829
+ var _apiBase = "/api";
29830
+ var _memToken = null;
29831
+ var _getAuthToken = null;
29832
+ async function _resolveToken() {
29833
+ if (_getAuthToken) {
29834
+ try {
29835
+ return await _getAuthToken();
29836
+ } catch {
29837
+ }
29838
+ }
29839
+ if (_memToken) return _memToken;
29840
+ if (typeof localStorage !== "undefined") return localStorage.getItem("forgecharts-token");
29841
+ return null;
29842
+ }
29843
+ async function _authHeader() {
29844
+ const token = await _resolveToken();
29845
+ return token ? { Authorization: `Bearer ${token}` } : {};
29846
+ }
29847
+ async function apiFetch(method, path, body) {
29848
+ const bodyInit = body !== void 0 ? JSON.stringify(body) : null;
29849
+ const res = await fetch(`${_apiBase}${path}`, {
29850
+ method,
29851
+ headers: { "Content-Type": "application/json", ...await _authHeader() },
29852
+ ...bodyInit !== null ? { body: bodyInit } : {}
29853
+ });
29854
+ if (!res.ok) {
29855
+ const err = await res.json().catch(() => ({ error: res.statusText }));
29856
+ throw new Error(err.error ?? res.statusText);
29857
+ }
29858
+ if (res.status === 204) return void 0;
29859
+ return res.json();
29860
+ }
29861
+ var watchlistApi = {
29862
+ configure(cfg) {
29863
+ if (cfg.apiBase) _apiBase = cfg.apiBase;
29864
+ if (cfg.getAuthToken) _getAuthToken = cfg.getAuthToken;
29865
+ if (cfg.token) _memToken = cfg.token;
29866
+ },
29867
+ setToken(token) {
29868
+ _memToken = token;
29869
+ },
29870
+ getAll() {
29871
+ return apiFetch("GET", "/watchlist");
29872
+ },
29873
+ addItem(symbol, group_id) {
29874
+ return apiFetch("POST", "/watchlist/items", { symbol, group_id });
29875
+ },
29876
+ removeItem(id) {
29877
+ return apiFetch("DELETE", `/watchlist/items/${encodeURIComponent(id)}`);
29878
+ },
29879
+ moveItemToGroup(id, group_id) {
29880
+ return apiFetch("PATCH", `/watchlist/items/${encodeURIComponent(id)}`, { group_id });
29881
+ },
29882
+ addGroup(name) {
29883
+ return apiFetch("POST", "/watchlist/groups", { name });
29884
+ },
29885
+ removeGroup(id) {
29886
+ return apiFetch("DELETE", `/watchlist/groups/${encodeURIComponent(id)}`);
29887
+ },
29888
+ reorderItems(ids) {
29889
+ return apiFetch("PUT", "/watchlist/items/reorder", { ids });
29890
+ }
29891
+ };
29892
+ function fmtPrice2(v) {
29893
+ const abs = Math.abs(v);
29894
+ const dec = abs >= 1e3 ? 2 : abs >= 1 ? 2 : abs >= 1e-4 ? 4 : 6;
29895
+ return v.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec });
29896
+ }
29897
+ function fmtVolume(v) {
29898
+ if (v >= 1e9) return (v / 1e9).toFixed(2) + "B";
29899
+ if (v >= 1e6) return (v / 1e6).toFixed(2) + "M";
29900
+ if (v >= 1e3) return (v / 1e3).toFixed(1) + "K";
29901
+ return v.toLocaleString("en-US");
29902
+ }
29903
+ function SymbolRow({
29904
+ item,
29905
+ quote,
29906
+ flash,
29907
+ dragOver,
29908
+ highlighted,
29909
+ logoUrl,
29910
+ onSelect,
29911
+ onRemove,
29912
+ onContextMenu,
29913
+ onDragStart,
29914
+ onDragOver,
29915
+ onDrop,
29916
+ onDragEnd
29917
+ }) {
29918
+ const liveColor = flash === "up" ? "var(--wl-up)" : flash === "down" ? "var(--wl-down)" : quote ? quote.change >= 0 ? "var(--wl-up)" : "var(--wl-down)" : "var(--text-muted)";
29919
+ const changeColor = quote ? quote.change >= 0 ? "var(--wl-up)" : "var(--wl-down)" : "var(--text-muted)";
29920
+ return /* @__PURE__ */ jsxs(
29921
+ "div",
29922
+ {
29923
+ className: `wl-row${dragOver ? " wl-drag-over" : ""}${highlighted ? " agent-highlight" : ""}`,
29924
+ draggable: true,
29925
+ onDragStart,
29926
+ onDragOver,
29927
+ onDrop,
29928
+ onDragEnd,
29929
+ onClick: onSelect,
29930
+ onContextMenu,
29931
+ title: item.symbol,
29932
+ children: [
29933
+ /* @__PURE__ */ jsx("div", { className: "wl-drag-handle", title: "Drag to reorder", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 8 12", width: "8", height: "12", fill: "currentColor", children: [
29934
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "2", height: "2", rx: "1" }),
29935
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "5", width: "2", height: "2", rx: "1" }),
29936
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "10", width: "2", height: "2", rx: "1" }),
29937
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "0", width: "2", height: "2", rx: "1" }),
29938
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "2", height: "2", rx: "1" }),
29939
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "10", width: "2", height: "2", rx: "1" })
29940
+ ] }) }),
29941
+ item.symbol.includes(":") && /* @__PURE__ */ jsx(
29942
+ "img",
29943
+ {
29944
+ className: "wl-logo",
29945
+ src: logoUrl ?? `/api/logos/${encodeURIComponent(item.symbol.split(":")[0].toUpperCase())}`,
29946
+ alt: "",
29947
+ onError: (e) => {
29948
+ const img = e.currentTarget;
29949
+ if (logoUrl && !img.src.includes("/api/logos/")) {
29950
+ img.src = `/api/logos/${encodeURIComponent(item.symbol.split(":")[0].toUpperCase())}`;
29951
+ } else {
29952
+ img.style.display = "none";
29953
+ }
29954
+ }
29955
+ }
29956
+ ),
29957
+ /* @__PURE__ */ jsx("span", { className: "wl-symbol", children: item.symbol }),
29958
+ quote ? /* @__PURE__ */ jsxs(Fragment, { children: [
29959
+ /* @__PURE__ */ jsx(
29960
+ "span",
29961
+ {
29962
+ className: `wl-last${flash ? ` wl-flash-${flash}` : ""}`,
29963
+ style: { color: liveColor },
29964
+ children: fmtPrice2(quote.last)
29965
+ }
29966
+ ),
29967
+ /* @__PURE__ */ jsxs("span", { className: "wl-change", style: { color: changeColor }, children: [
29968
+ quote.change >= 0 ? "+" : "",
29969
+ fmtPrice2(quote.change)
29970
+ ] }),
29971
+ /* @__PURE__ */ jsxs("span", { className: "wl-pct", style: { color: changeColor }, children: [
29972
+ quote.change >= 0 ? "+" : "",
29973
+ quote.changePct.toFixed(2),
29974
+ "%"
29975
+ ] }),
29976
+ /* @__PURE__ */ jsx("span", { className: "wl-vol", children: quote.volume !== void 0 ? fmtVolume(quote.volume) : "\u2014" })
29977
+ ] }) : /* @__PURE__ */ jsx("span", { className: "wl-no-data", children: "\u2014" }),
29978
+ /* @__PURE__ */ jsx(
29979
+ "button",
29980
+ {
29981
+ className: "wl-remove-btn",
29982
+ title: "Remove",
29983
+ onClick: (e) => {
29984
+ e.stopPropagation();
29985
+ onRemove();
29986
+ },
29987
+ children: "\u2715"
29988
+ }
29989
+ )
29990
+ ]
29991
+ }
29992
+ );
29993
+ }
29994
+ function WatchlistDrawer({ onClose, onSelectSymbol, symbolResolver, apiUrl, getAuthToken, liveQuotes = true }) {
29995
+ const api = watchlistApi;
29996
+ useEffect(() => {
29997
+ if (apiUrl !== void 0 || getAuthToken) {
29998
+ watchlistApi.configure({
29999
+ ...apiUrl !== void 0 ? { apiBase: `${apiUrl}/api` } : {},
30000
+ ...getAuthToken ? { getAuthToken } : {}
30001
+ });
30002
+ }
30003
+ }, [apiUrl, getAuthToken]);
30004
+ const [groups, setGroups] = useState([]);
30005
+ const [items, setItems] = useState([]);
30006
+ const [loading, setLoading] = useState(true);
30007
+ const [searchOpen, setSearchOpen] = useState(false);
30008
+ const [addError, setAddError] = useState("");
30009
+ const [ctxMenu, setCtxMenu] = useState(null);
30010
+ const [groupDialog, setGroupDialog] = useState(null);
30011
+ const [groupName, setGroupName] = useState("");
30012
+ const [collapsed, setCollapsed] = useState(/* @__PURE__ */ new Set());
30013
+ const [logoMap, setLogoMap] = useState(/* @__PURE__ */ new Map());
30014
+ const [drawerWidth, setDrawerWidth] = useState(360);
30015
+ const [colWidths, setColWidths] = useState({ sym: 120, last: 70, chg: 70, pct: 60, vol: 52 });
30016
+ const [dragItemId, setDragItemId] = useState(null);
30017
+ const [dropTargetId, setDropTargetId] = useState(null);
30018
+ const [highlightedSymbol, setHighlightedSymbol] = useState(null);
30019
+ const ctxRef = useRef(null);
30020
+ const drawerRef = useRef(null);
30021
+ const dragState = useRef(null);
30022
+ const colDragRef = useRef(null);
30023
+ useAgentUIEvent("watchlist:highlight", ({ symbol }) => {
30024
+ setHighlightedSymbol(symbol);
30025
+ setTimeout(() => setHighlightedSymbol(null), 1800);
30026
+ });
30027
+ const onColResizeMouseDown = (e, left, right) => {
30028
+ e.preventDefault();
30029
+ e.stopPropagation();
30030
+ colDragRef.current = {
30031
+ left,
30032
+ right,
30033
+ startX: e.clientX,
30034
+ startLW: colWidths[left],
30035
+ startRW: right ? colWidths[right] : 0
30036
+ };
30037
+ const onMove = (ev) => {
30038
+ if (!colDragRef.current) return;
30039
+ const { left: l, right: r, startX, startLW, startRW } = colDragRef.current;
30040
+ const delta = ev.clientX - startX;
30041
+ const maxGrow = r ? startRW - 36 : Infinity;
30042
+ const maxShrink = startLW - 36;
30043
+ const clamped = Math.min(maxGrow, Math.max(-maxShrink, delta));
30044
+ setColWidths((prev) => {
30045
+ const next = { ...prev, [l]: startLW + clamped };
30046
+ if (r) next[r] = startRW - clamped;
30047
+ return next;
30048
+ });
30049
+ };
30050
+ const onUp = () => {
30051
+ colDragRef.current = null;
30052
+ window.removeEventListener("mousemove", onMove);
30053
+ window.removeEventListener("mouseup", onUp);
30054
+ };
30055
+ window.addEventListener("mousemove", onMove);
30056
+ window.addEventListener("mouseup", onUp);
30057
+ };
30058
+ const onResizeMouseDown = (e) => {
30059
+ e.preventDefault();
30060
+ dragState.current = { startX: e.clientX, startW: drawerWidth };
30061
+ const onMove = (ev) => {
30062
+ if (!dragState.current) return;
30063
+ const delta = dragState.current.startX - ev.clientX;
30064
+ const next = Math.min(600, Math.max(240, dragState.current.startW + delta));
30065
+ setDrawerWidth(next);
30066
+ };
30067
+ const onUp = () => {
30068
+ dragState.current = null;
30069
+ window.removeEventListener("mousemove", onMove);
30070
+ window.removeEventListener("mouseup", onUp);
30071
+ };
30072
+ window.addEventListener("mousemove", onMove);
30073
+ window.addEventListener("mouseup", onUp);
30074
+ };
30075
+ const reload = useCallback(async () => {
30076
+ try {
30077
+ const data = await api.getAll();
30078
+ setGroups(data.groups);
30079
+ setItems(data.items);
30080
+ } catch {
30081
+ } finally {
30082
+ setLoading(false);
30083
+ }
30084
+ }, []);
30085
+ useEffect(() => {
30086
+ reload();
30087
+ }, [reload]);
30088
+ useEffect(() => {
30089
+ if (items.length === 0 || liveQuotes === false) return;
30090
+ const syms = [...new Set(items.map((i) => i.symbol))].join(",");
30091
+ fetch(`/api/reference/symbols/logos?symbols=${encodeURIComponent(syms)}`).then((r) => r.ok ? r.json() : {}).then((map) => setLogoMap(new Map(Object.entries(map)))).catch(() => {
30092
+ });
30093
+ }, [items, liveQuotes]);
30094
+ const symbols = items.map((i) => i.symbol);
30095
+ const { prices: livePrices, dirs: liveDirs, volumes: liveVolumes } = useWatchlistQuotes(symbols, liveQuotes);
30096
+ const sessionOpenRef = useRef(/* @__PURE__ */ new Map());
30097
+ const quotes = useRef(/* @__PURE__ */ new Map()).current;
30098
+ quotes.clear();
30099
+ for (const [sym, last] of livePrices) {
30100
+ if (!sessionOpenRef.current.has(sym)) {
30101
+ sessionOpenRef.current.set(sym, last);
30102
+ }
30103
+ const open = sessionOpenRef.current.get(sym);
30104
+ const change = last - open;
30105
+ const q = {
30106
+ last,
30107
+ change,
30108
+ changePct: open !== 0 ? change / open * 100 : 0
30109
+ };
30110
+ const vol = liveVolumes.get(sym);
30111
+ if (vol !== void 0) q.volume = vol;
30112
+ quotes.set(sym, q);
30113
+ }
30114
+ const prevItemsRef = useRef([]);
30115
+ if (prevItemsRef.current !== items) {
30116
+ const prevSyms = new Set(prevItemsRef.current.map((i) => i.symbol));
30117
+ const nextSyms = new Set(items.map((i) => i.symbol));
30118
+ for (const s of prevSyms) {
30119
+ if (!nextSyms.has(s)) sessionOpenRef.current.delete(s);
30120
+ }
30121
+ prevItemsRef.current = items;
30122
+ }
30123
+ useEffect(() => {
30124
+ if (!ctxMenu) return;
30125
+ const handler = (e) => {
30126
+ if (ctxRef.current && !ctxRef.current.contains(e.target)) setCtxMenu(null);
30127
+ };
30128
+ document.addEventListener("mousedown", handler);
30129
+ return () => document.removeEventListener("mousedown", handler);
30130
+ }, [ctxMenu]);
30131
+ const handleAddFromSearch = useCallback(async (symbol) => {
30132
+ setSearchOpen(false);
30133
+ setAddError("");
30134
+ try {
30135
+ await api.addItem(symbol);
30136
+ await reload();
30137
+ } catch (err) {
30138
+ setAddError(err instanceof Error ? err.message : "Failed to add");
30139
+ }
30140
+ }, [reload]);
30141
+ const handleRemove = async (id) => {
30142
+ await api.removeItem(id);
30143
+ await reload();
30144
+ };
30145
+ const openCtxMenu = (e, item) => {
30146
+ e.preventDefault();
30147
+ setCtxMenu({ x: e.clientX, y: e.clientY, itemId: item.id, symbol: item.symbol });
30148
+ };
30149
+ const openGroupDialog = (itemId) => {
30150
+ setCtxMenu(null);
30151
+ setGroupName("");
30152
+ setGroupDialog({ itemId });
30153
+ };
30154
+ const handleCreateGroup = async () => {
30155
+ const name = groupName.trim();
30156
+ if (!name || !groupDialog) return;
30157
+ const group = await api.addGroup(name);
30158
+ await api.moveItemToGroup(groupDialog.itemId, group.id);
30159
+ setGroupDialog(null);
30160
+ await reload();
30161
+ };
30162
+ const handleMoveToExisting = async (itemId, groupId) => {
30163
+ setCtxMenu(null);
30164
+ await api.moveItemToGroup(itemId, groupId);
30165
+ await reload();
30166
+ };
30167
+ const handleUngroup = async (itemId) => {
30168
+ setCtxMenu(null);
30169
+ await api.moveItemToGroup(itemId, null);
30170
+ await reload();
30171
+ };
30172
+ const handleDragStart = (e, itemId) => {
30173
+ setDragItemId(itemId);
30174
+ e.dataTransfer.effectAllowed = "move";
30175
+ };
30176
+ const handleDragOver = (e, targetId) => {
30177
+ e.preventDefault();
30178
+ e.dataTransfer.dropEffect = "move";
30179
+ if (targetId !== dragItemId) setDropTargetId(targetId);
30180
+ };
30181
+ const handleDrop = async (e, targetId) => {
30182
+ e.preventDefault();
30183
+ if (!dragItemId || dragItemId === targetId) return;
30184
+ setDragItemId(null);
30185
+ setDropTargetId(null);
30186
+ const dragItem = items.find((i) => i.id === dragItemId);
30187
+ const targetItem = items.find((i) => i.id === targetId);
30188
+ if (!dragItem || !targetItem) return;
30189
+ const sameGroup = dragItem.group_id === targetItem.group_id;
30190
+ if (!sameGroup) {
30191
+ try {
30192
+ await api.moveItemToGroup(dragItem.id, targetItem.group_id);
30193
+ } catch {
30194
+ await reload();
30195
+ return;
30196
+ }
30197
+ const updated = items.map(
30198
+ (i) => i.id === dragItem.id ? { ...i, group_id: targetItem.group_id } : i
30199
+ );
30200
+ const targetGroup = updated.filter((i) => i.group_id === targetItem.group_id);
30201
+ const fromIdx = targetGroup.findIndex((i) => i.id === dragItem.id);
30202
+ const toIdx = targetGroup.findIndex((i) => i.id === targetId);
30203
+ if (fromIdx !== -1 && toIdx !== -1 && fromIdx !== toIdx) {
30204
+ const [moved] = targetGroup.splice(fromIdx, 1);
30205
+ targetGroup.splice(toIdx, 0, moved);
30206
+ }
30207
+ const rest = updated.filter((i) => i.group_id !== targetItem.group_id);
30208
+ const newItems = [...rest, ...targetGroup];
30209
+ setItems(newItems);
30210
+ try {
30211
+ await api.reorderItems(newItems.map((i) => i.id));
30212
+ } catch {
30213
+ await reload();
30214
+ }
30215
+ } else {
30216
+ const groupItems = items.filter((i) => i.group_id === dragItem.group_id);
30217
+ const fromIdx = groupItems.findIndex((i) => i.id === dragItemId);
30218
+ const toIdx = groupItems.findIndex((i) => i.id === targetId);
30219
+ if (fromIdx === -1 || toIdx === -1) return;
30220
+ const reordered = [...groupItems];
30221
+ const [moved] = reordered.splice(fromIdx, 1);
30222
+ reordered.splice(toIdx, 0, moved);
30223
+ const rest = items.filter((i) => i.group_id !== dragItem.group_id);
30224
+ const newItems = [...rest, ...reordered];
30225
+ setItems(newItems);
30226
+ try {
30227
+ await api.reorderItems(newItems.map((i) => i.id));
30228
+ } catch {
30229
+ await reload();
30230
+ }
30231
+ }
30232
+ };
30233
+ const handleDragEnd = () => {
30234
+ setDragItemId(null);
30235
+ setDropTargetId(null);
30236
+ };
30237
+ const handleRemoveGroup = async (groupId) => {
30238
+ await api.removeGroup(groupId);
30239
+ await reload();
30240
+ };
30241
+ const toggleCollapse = (groupId) => {
30242
+ setCollapsed((prev) => {
30243
+ const next = new Set(prev);
30244
+ next.has(groupId) ? next.delete(groupId) : next.add(groupId);
30245
+ return next;
30246
+ });
30247
+ };
30248
+ const ungrouped = items.filter((i) => i.group_id === null);
30249
+ return /* @__PURE__ */ jsxs(
30250
+ "div",
30251
+ {
30252
+ ref: drawerRef,
30253
+ className: "wl-drawer",
30254
+ style: {
30255
+ width: drawerWidth,
30256
+ "--wl-sym": `${colWidths.sym}px`,
30257
+ "--wl-last": `${colWidths.last}px`,
30258
+ "--wl-chg": `${colWidths.chg}px`,
30259
+ "--wl-pct": `${colWidths.pct}px`,
30260
+ "--wl-vol": `${colWidths.vol}px`
30261
+ },
30262
+ children: [
30263
+ /* @__PURE__ */ jsx("div", { className: "wl-resize-handle", onMouseDown: onResizeMouseDown }),
30264
+ /* @__PURE__ */ jsxs("div", { className: "wl-header", children: [
30265
+ /* @__PURE__ */ jsxs("span", { className: "wl-header-title", children: [
30266
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
30267
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
30268
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "7", width: "9", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
30269
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "11", width: "10", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" })
30270
+ ] }),
30271
+ "Watchlist"
30272
+ ] }),
30273
+ /* @__PURE__ */ jsx("button", { className: "wl-close-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", strokeWidth: "1.8", children: [
30274
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "12", y2: "12" }),
30275
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "2", y2: "12" })
30276
+ ] }) })
30277
+ ] }),
30278
+ /* @__PURE__ */ jsxs("div", { className: "wl-add-row", children: [
30279
+ /* @__PURE__ */ jsx(
30280
+ "input",
30281
+ {
30282
+ readOnly: true,
30283
+ className: "wl-add-input",
30284
+ placeholder: "Add symbol e.g. BINANCE:BTCUSDT",
30285
+ onClick: () => setSearchOpen(true),
30286
+ onFocus: () => setSearchOpen(true),
30287
+ style: { cursor: "pointer" }
30288
+ }
30289
+ ),
30290
+ /* @__PURE__ */ jsx("button", { className: "wl-add-btn", onClick: () => setSearchOpen(true), title: "Add", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", strokeWidth: "2", children: [
30291
+ /* @__PURE__ */ jsx("line", { x1: "7", y1: "1", x2: "7", y2: "13" }),
30292
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "7", x2: "13", y2: "7" })
30293
+ ] }) })
30294
+ ] }),
30295
+ addError && /* @__PURE__ */ jsx("div", { className: "wl-add-error", children: addError }),
30296
+ /* @__PURE__ */ jsxs("div", { className: "wl-col-headers", children: [
30297
+ /* @__PURE__ */ jsx("span", {}),
30298
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-symbol", children: [
30299
+ "Symbol",
30300
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "sym", "last") })
30301
+ ] }),
30302
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-last", children: [
30303
+ "Last",
30304
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "last", "chg") })
30305
+ ] }),
30306
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-chg", children: [
30307
+ "Chg",
30308
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "chg", "pct") })
30309
+ ] }),
30310
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-pct", children: [
30311
+ "Chg%",
30312
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "pct", "vol") })
30313
+ ] }),
30314
+ /* @__PURE__ */ jsx("span", { className: "wl-col-vol", children: "Vol" })
30315
+ ] }),
30316
+ /* @__PURE__ */ jsxs("div", { className: "wl-list", children: [
30317
+ loading && /* @__PURE__ */ jsx("div", { className: "wl-empty", children: "Loading\u2026" }),
30318
+ groups.map((group) => {
30319
+ const groupItems = items.filter((i) => i.group_id === group.id);
30320
+ const isCollapsed = collapsed.has(group.id);
30321
+ return /* @__PURE__ */ jsxs("div", { className: "wl-group", children: [
30322
+ /* @__PURE__ */ jsxs("div", { className: "wl-group-header", children: [
30323
+ /* @__PURE__ */ jsx(
30324
+ "button",
30325
+ {
30326
+ className: "wl-group-toggle",
30327
+ onClick: () => toggleCollapse(group.id),
30328
+ title: isCollapsed ? "Expand" : "Collapse",
30329
+ children: /* @__PURE__ */ jsx(
30330
+ "svg",
30331
+ {
30332
+ viewBox: "0 0 10 10",
30333
+ width: "8",
30334
+ height: "8",
30335
+ stroke: "currentColor",
30336
+ fill: "none",
30337
+ strokeWidth: "1.5",
30338
+ style: { transform: isCollapsed ? "rotate(-90deg)" : "none", transition: "transform 0.15s" },
30339
+ children: /* @__PURE__ */ jsx("polyline", { points: "2,3 5,7 8,3" })
30340
+ }
30341
+ )
30342
+ }
30343
+ ),
30344
+ /* @__PURE__ */ jsx("span", { className: "wl-group-name", children: group.name }),
30345
+ /* @__PURE__ */ jsx("span", { className: "wl-group-count", children: groupItems.length }),
30346
+ /* @__PURE__ */ jsx(
30347
+ "button",
30348
+ {
30349
+ className: "wl-remove-btn wl-group-del",
30350
+ title: "Remove group (items become ungrouped)",
30351
+ onClick: () => handleRemoveGroup(group.id),
30352
+ children: "\u2715"
30353
+ }
30354
+ )
30355
+ ] }),
30356
+ !isCollapsed && groupItems.map((item) => /* @__PURE__ */ jsx(
30357
+ SymbolRow,
30358
+ {
30359
+ item,
30360
+ quote: quotes.get(item.symbol),
30361
+ flash: liveDirs.get(item.symbol),
30362
+ dragOver: dropTargetId === item.id,
30363
+ highlighted: highlightedSymbol === item.symbol,
30364
+ ...logoMap.get(item.symbol) !== void 0 && { logoUrl: logoMap.get(item.symbol) },
30365
+ onSelect: () => onSelectSymbol(item.symbol),
30366
+ onRemove: () => handleRemove(item.id),
30367
+ onContextMenu: (e) => openCtxMenu(e, item),
30368
+ onDragStart: (e) => handleDragStart(e, item.id),
30369
+ onDragOver: (e) => handleDragOver(e, item.id),
30370
+ onDrop: (e) => handleDrop(e, item.id),
30371
+ onDragEnd: handleDragEnd
30372
+ },
30373
+ item.id
30374
+ ))
30375
+ ] }, group.id);
30376
+ }),
30377
+ ungrouped.length > 0 && groups.length > 0 && /* @__PURE__ */ jsx("div", { className: "wl-group-label-ungrouped", children: "Other" }),
30378
+ ungrouped.map((item) => /* @__PURE__ */ jsx(
30379
+ SymbolRow,
30380
+ {
30381
+ item,
30382
+ quote: quotes.get(item.symbol),
30383
+ flash: liveDirs.get(item.symbol),
30384
+ dragOver: dropTargetId === item.id,
30385
+ highlighted: highlightedSymbol === item.symbol,
30386
+ ...logoMap.get(item.symbol) !== void 0 && { logoUrl: logoMap.get(item.symbol) },
30387
+ onSelect: () => onSelectSymbol(item.symbol),
30388
+ onRemove: () => handleRemove(item.id),
30389
+ onContextMenu: (e) => openCtxMenu(e, item),
30390
+ onDragStart: (e) => handleDragStart(e, item.id),
30391
+ onDragOver: (e) => handleDragOver(e, item.id),
30392
+ onDrop: (e) => handleDrop(e, item.id),
30393
+ onDragEnd: handleDragEnd
30394
+ },
30395
+ item.id
30396
+ )),
30397
+ !loading && items.length === 0 && /* @__PURE__ */ jsxs("div", { className: "wl-empty", children: [
30398
+ "No symbols yet.",
30399
+ /* @__PURE__ */ jsx("br", {}),
30400
+ "Type a symbol above and press Enter."
30401
+ ] })
30402
+ ] }),
30403
+ ctxMenu && /* @__PURE__ */ jsxs(
30404
+ "div",
30405
+ {
30406
+ ref: ctxRef,
30407
+ className: "wl-ctx-menu",
30408
+ style: { top: ctxMenu.y, left: ctxMenu.x },
30409
+ children: [
30410
+ groups.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
30411
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-section", children: "Move to group" }),
30412
+ groups.map((g) => /* @__PURE__ */ jsx(
30413
+ "button",
30414
+ {
30415
+ className: "wl-ctx-item",
30416
+ onClick: () => handleMoveToExisting(ctxMenu.itemId, g.id),
30417
+ children: g.name
30418
+ },
30419
+ g.id
30420
+ )),
30421
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-divider" })
30422
+ ] }),
30423
+ /* @__PURE__ */ jsx(
30424
+ "button",
30425
+ {
30426
+ className: "wl-ctx-item",
30427
+ onClick: () => openGroupDialog(ctxMenu.itemId),
30428
+ children: "New group\u2026"
30429
+ }
30430
+ ),
30431
+ items.find((i) => i.id === ctxMenu.itemId)?.group_id && /* @__PURE__ */ jsx(
30432
+ "button",
30433
+ {
30434
+ className: "wl-ctx-item",
30435
+ onClick: () => handleUngroup(ctxMenu.itemId),
30436
+ children: "Remove from group"
30437
+ }
30438
+ ),
30439
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-divider" }),
30440
+ /* @__PURE__ */ jsx(
30441
+ "button",
30442
+ {
30443
+ className: "wl-ctx-item wl-ctx-item--danger",
30444
+ onClick: () => {
30445
+ setCtxMenu(null);
30446
+ handleRemove(ctxMenu.itemId);
30447
+ },
30448
+ children: "Remove symbol"
30449
+ }
30450
+ )
30451
+ ]
30452
+ }
30453
+ ),
30454
+ groupDialog && /* @__PURE__ */ jsx("div", { className: "wl-dialog-backdrop", children: /* @__PURE__ */ jsxs("div", { className: "wl-dialog", children: [
30455
+ /* @__PURE__ */ jsx("div", { className: "wl-dialog-title", children: "Add to group" }),
30456
+ /* @__PURE__ */ jsx(
30457
+ "input",
30458
+ {
30459
+ className: "wl-dialog-input",
30460
+ placeholder: "Group name",
30461
+ value: groupName,
30462
+ onChange: (e) => setGroupName(e.target.value),
30463
+ onKeyDown: (e) => {
30464
+ if (e.key === "Enter") handleCreateGroup();
30465
+ },
30466
+ autoFocus: true
30467
+ }
30468
+ ),
30469
+ /* @__PURE__ */ jsxs("div", { className: "wl-dialog-actions", children: [
30470
+ /* @__PURE__ */ jsx("button", { className: "wl-dialog-cancel", onClick: () => setGroupDialog(null), children: "Cancel" }),
30471
+ /* @__PURE__ */ jsx("button", { className: "wl-dialog-confirm", onClick: handleCreateGroup, disabled: !groupName.trim(), children: "Save" })
30472
+ ] })
30473
+ ] }) }),
30474
+ searchOpen && /* @__PURE__ */ jsx(
30475
+ SymbolSearchDialog,
30476
+ {
30477
+ current: "",
30478
+ onSelect: handleAddFromSearch,
30479
+ onClose: () => setSearchOpen(false),
30480
+ symbolResolver
30481
+ }
30482
+ )
30483
+ ]
30484
+ }
30485
+ );
30486
+ }
29691
30487
  function ChartWorkspace({
29692
30488
  // TabBar
29693
30489
  tabs,
@@ -29737,6 +30533,7 @@ function ChartWorkspace({
29737
30533
  // RightToolbar
29738
30534
  watchlistOpen,
29739
30535
  onToggleWatchlist,
30536
+ builtinWatchlistDrawer,
29740
30537
  orderEntryOpen,
29741
30538
  onToggleOrderEntry,
29742
30539
  showOrderEntry,
@@ -29764,6 +30561,7 @@ function ChartWorkspace({
29764
30561
  scriptDrawerOpen,
29765
30562
  onToggleScriptDrawer,
29766
30563
  builtinScriptDrawer,
30564
+ hostApiUrl,
29767
30565
  scriptApiUrl,
29768
30566
  // Trading panel
29769
30567
  tradingPanel,
@@ -29800,6 +30598,10 @@ function ChartWorkspace({
29800
30598
  const effOnToggleScriptDrawer = onToggleScriptDrawer ?? (() => setAutoScriptOpen((o) => !o));
29801
30599
  const effScriptDrawerOpen = onToggleScriptDrawer ? scriptDrawerOpen ?? false : autoScriptOpen;
29802
30600
  const closeScriptDrawer = onToggleScriptDrawer ?? (() => setAutoScriptOpen(false));
30601
+ const [autoWatchlistOpen, setAutoWatchlistOpen] = React11.useState(false);
30602
+ const effOnToggleWatchlist = onToggleWatchlist ?? (() => setAutoWatchlistOpen((o) => !o));
30603
+ const effWatchlistOpen = onToggleWatchlist ? watchlistOpen ?? false : autoWatchlistOpen;
30604
+ const closeWatchlist = onToggleWatchlist ?? (() => setAutoWatchlistOpen(false));
29803
30605
  const gate = (hostProp, licensed) => hostProp !== false && licensed;
29804
30606
  const effShowTradeButton = gate(showTradeButton, capabilities.orderEntry) && effOnToggleTradeDrawer !== void 0;
29805
30607
  const effShowOrderEntry = gate(showOrderEntry, capabilities.orderEntry) && effOnToggleOrderEntry !== void 0;
@@ -29968,7 +30770,18 @@ function ChartWorkspace({
29968
30770
  {
29969
30771
  onClose: closeScriptDrawer,
29970
30772
  onAddIndicator,
29971
- ...scriptApiUrl !== void 0 ? { apiUrl: scriptApiUrl } : {},
30773
+ ...(hostApiUrl ?? scriptApiUrl) !== void 0 ? { apiUrl: hostApiUrl ?? scriptApiUrl } : {},
30774
+ ...getAuthToken !== void 0 ? { getAuthToken } : {}
30775
+ }
30776
+ ),
30777
+ builtinWatchlistDrawer !== false && effWatchlistOpen && capabilities.watchlists && /* @__PURE__ */ jsx(
30778
+ WatchlistDrawer,
30779
+ {
30780
+ onClose: closeWatchlist,
30781
+ onSelectSymbol: onSymbolChange,
30782
+ symbolResolver,
30783
+ liveQuotes: false,
30784
+ ...hostApiUrl !== void 0 ? { apiUrl: hostApiUrl } : {},
29972
30785
  ...getAuthToken !== void 0 ? { getAuthToken } : {}
29973
30786
  }
29974
30787
  ),
@@ -29977,8 +30790,8 @@ function ChartWorkspace({
29977
30790
  /* @__PURE__ */ jsx(
29978
30791
  RightToolbar,
29979
30792
  {
29980
- watchlistOpen,
29981
- onToggleWatchlist,
30793
+ watchlistOpen: effWatchlistOpen,
30794
+ onToggleWatchlist: effOnToggleWatchlist,
29982
30795
  orderEntryOpen: effOrderEntryOpen,
29983
30796
  onToggleOrderEntry: effOnToggleOrderEntry ?? (() => {
29984
30797
  }),