@coderyo/ui-shell 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -73,7 +73,11 @@ function saveIndicatorConfig(symbol, interval, config) {
73
73
  }
74
74
 
75
75
  // src/settings-panel.ts
76
- import { DEFAULT_INDICATOR_CONFIG } from "@coderyo/indicators";
76
+ import {
77
+ DEFAULT_INDICATOR_CONFIG,
78
+ disableIndicatorLayer,
79
+ listActiveIndicatorLayers
80
+ } from "@coderyo/indicators";
77
81
  import { t } from "@coderyo/i18n";
78
82
  function mountSettingsPanel(parent, opts = {}) {
79
83
  let open = false;
@@ -88,12 +92,27 @@ function mountSettingsPanel(parent, opts = {}) {
88
92
  btn.title = t("settings.title", "\u8A2D\u5B9A");
89
93
  btn.textContent = "\u2699";
90
94
  btn.style.cssText = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:14px;";
91
- const panel = document.createElement("div");
92
- panel.style.cssText = "display:none;position:absolute;right:0;top:100%;margin-top:4px;width:280px;max-height:70vh;overflow:auto;padding:0;background:#161b22;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #01040988;z-index:30;";
95
+ const overlay = document.createElement("div");
96
+ overlay.style.cssText = "display:none;position:fixed;inset:0;z-index:2000;background:#01040999;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;";
97
+ const dialog = document.createElement("div");
98
+ dialog.setAttribute("role", "dialog");
99
+ dialog.setAttribute("aria-modal", "true");
100
+ dialog.style.cssText = "width:min(420px,100%);max-height:min(85vh,640px);display:flex;flex-direction:column;background:#161b22;border:1px solid #30363d;border-radius:8px;box-shadow:0 16px 48px #010409cc;overflow:hidden;";
101
+ const header = document.createElement("div");
102
+ header.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #30363d;";
103
+ const title = document.createElement("span");
104
+ title.textContent = t("settings.title", "\u8A2D\u5B9A");
105
+ title.style.cssText = "font-size:14px;font-weight:600;color:#e6edf3;";
106
+ const closeBtn = document.createElement("button");
107
+ closeBtn.type = "button";
108
+ closeBtn.textContent = "\xD7";
109
+ closeBtn.title = t("settings.close", "\u95DC\u9589");
110
+ closeBtn.style.cssText = "background:transparent;border:none;color:#8b949e;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;";
111
+ header.append(title, closeBtn);
93
112
  const tabs = document.createElement("div");
94
- tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;";
113
+ tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;flex-shrink:0;";
95
114
  const content = document.createElement("div");
96
- content.style.cssText = "padding:10px 12px;font-size:12px;";
115
+ content.style.cssText = "padding:10px 12px;font-size:12px;overflow:auto;flex:1;min-height:0;";
97
116
  const tabIds = [
98
117
  ["chart", t("settings.tab.chart", "\u5716\u8868")],
99
118
  ["drawing", t("settings.tab.drawing", "\u7E6A\u5716")],
@@ -125,16 +144,16 @@ function mountSettingsPanel(parent, opts = {}) {
125
144
  row.append(input, document.createTextNode(label));
126
145
  return row;
127
146
  };
128
- const actionButton = (label, onClick) => {
129
- const btn2 = document.createElement("button");
130
- btn2.type = "button";
131
- btn2.textContent = label;
132
- btn2.style.cssText = "display:block;width:100%;margin-top:10px;padding:6px 10px;border-radius:4px;border:1px solid #f85149;background:#21262d;color:#f85149;cursor:pointer;font-size:12px;";
133
- btn2.onclick = (e) => {
147
+ const actionButton = (label, onClick, danger = true) => {
148
+ const b = document.createElement("button");
149
+ b.type = "button";
150
+ b.textContent = label;
151
+ b.style.cssText = `display:block;width:100%;margin-top:10px;padding:6px 10px;border-radius:4px;border:1px solid ${danger ? "#f85149" : "#30363d"};background:#21262d;color:${danger ? "#f85149" : "#e6edf3"};cursor:pointer;font-size:12px;`;
152
+ b.onclick = (e) => {
134
153
  e.stopPropagation();
135
154
  onClick();
136
155
  };
137
- return btn2;
156
+ return b;
138
157
  };
139
158
  const numberField = (label, value, onChange) => {
140
159
  const row = document.createElement("label");
@@ -148,6 +167,49 @@ function mountSettingsPanel(parent, opts = {}) {
148
167
  row.appendChild(input);
149
168
  return row;
150
169
  };
170
+ const renderLayerList = () => {
171
+ const layers = listActiveIndicatorLayers(indicatorConfig);
172
+ const section = document.createElement("div");
173
+ section.style.marginBottom = "12px";
174
+ const heading = document.createElement("div");
175
+ heading.textContent = t("settings.ind.layers", "\u6307\u6A19\u5716\u5C64");
176
+ heading.style.cssText = "font-weight:600;color:#e6edf3;margin-bottom:6px;font-size:12px;";
177
+ section.appendChild(heading);
178
+ if (layers.length === 0) {
179
+ const empty = document.createElement("div");
180
+ empty.textContent = t("settings.ind.layersEmpty", "\u76EE\u524D\u6C92\u6709\u6307\u6A19");
181
+ empty.style.cssText = "color:#8b949e;font-size:11px;";
182
+ section.appendChild(empty);
183
+ return section;
184
+ }
185
+ for (const layer of layers) {
186
+ const row = document.createElement("div");
187
+ row.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;margin-bottom:4px;border-radius:4px;background:#0d1117;border:1px solid #30363d;";
188
+ const meta = document.createElement("div");
189
+ meta.style.cssText = "min-width:0;";
190
+ const name = document.createElement("div");
191
+ name.textContent = layer.label;
192
+ name.style.cssText = "color:#e6edf3;font-size:12px;";
193
+ const target = document.createElement("div");
194
+ target.textContent = layer.target === "main" ? t("settings.ind.layerMain", "\u4E3B\u5716") : t("settings.ind.layerPane", "\u526F\u5716");
195
+ target.style.cssText = "color:#8b949e;font-size:10px;margin-top:2px;";
196
+ meta.append(name, target);
197
+ const remove = document.createElement("button");
198
+ remove.type = "button";
199
+ remove.textContent = t("settings.ind.layerRemove", "\u79FB\u9664");
200
+ remove.title = t("settings.ind.layerRemove", "\u79FB\u9664");
201
+ remove.style.cssText = "flex-shrink:0;padding:2px 8px;border-radius:4px;border:1px solid #f85149;background:transparent;color:#f85149;cursor:pointer;font-size:11px;";
202
+ remove.onclick = (e) => {
203
+ e.stopPropagation();
204
+ indicatorConfig = disableIndicatorLayer(indicatorConfig, layer.id);
205
+ opts.onIndicatorConfigChange?.(indicatorConfig);
206
+ renderContent();
207
+ };
208
+ row.append(meta, remove);
209
+ section.appendChild(row);
210
+ }
211
+ return section;
212
+ };
151
213
  const renderContent = () => {
152
214
  content.replaceChildren();
153
215
  if (tab === "chart") {
@@ -166,31 +228,43 @@ function mountSettingsPanel(parent, opts = {}) {
166
228
  opts.onReturnToCursorChange?.(v);
167
229
  })
168
230
  );
169
- if (opts.onClearAllDrawings) {
170
- content.appendChild(
171
- actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
172
- opts.onClearAllDrawings?.();
173
- renderContent();
174
- })
175
- );
176
- }
231
+ content.appendChild(
232
+ actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
233
+ opts.onClearAllDrawings?.();
234
+ renderContent();
235
+ })
236
+ );
177
237
  } else {
238
+ content.appendChild(renderLayerList());
239
+ content.appendChild(
240
+ checkbox(t("settings.ind.volume", "\u6210\u4EA4\u91CF\u526F\u5716"), indicatorConfig.showVolume, (v) => {
241
+ indicatorConfig = { ...indicatorConfig, showVolume: v };
242
+ opts.onIndicatorConfigChange?.(indicatorConfig);
243
+ renderContent();
244
+ })
245
+ );
246
+ const divider = document.createElement("div");
247
+ divider.style.cssText = "height:1px;background:#30363d;margin:10px 0;";
248
+ content.appendChild(divider);
178
249
  content.appendChild(
179
250
  checkbox(t("settings.ind.macd", "MACD \u7A97\u683C"), indicatorConfig.showMacd, (v) => {
180
251
  indicatorConfig = { ...indicatorConfig, showMacd: v };
181
252
  opts.onIndicatorConfigChange?.(indicatorConfig);
253
+ renderContent();
182
254
  })
183
255
  );
184
256
  content.appendChild(
185
257
  checkbox(t("settings.ind.rsi", "RSI \u7A97\u683C"), indicatorConfig.showRsi, (v) => {
186
258
  indicatorConfig = { ...indicatorConfig, showRsi: v };
187
259
  opts.onIndicatorConfigChange?.(indicatorConfig);
260
+ renderContent();
188
261
  })
189
262
  );
190
263
  content.appendChild(
191
264
  checkbox(t("settings.ind.kdj", "KDJ \u7A97\u683C"), indicatorConfig.showKdj, (v) => {
192
265
  indicatorConfig = { ...indicatorConfig, showKdj: v };
193
266
  opts.onIndicatorConfigChange?.(indicatorConfig);
267
+ renderContent();
194
268
  })
195
269
  );
196
270
  const src = document.createElement("label");
@@ -215,6 +289,7 @@ function mountSettingsPanel(parent, opts = {}) {
215
289
  checkbox(t("settings.ind.ema", "EMA \u758A\u52A0"), indicatorConfig.showEma, (v) => {
216
290
  indicatorConfig = { ...indicatorConfig, showEma: v };
217
291
  opts.onIndicatorConfigChange?.(indicatorConfig);
292
+ renderContent();
218
293
  })
219
294
  );
220
295
  content.appendChild(
@@ -227,6 +302,7 @@ function mountSettingsPanel(parent, opts = {}) {
227
302
  checkbox(t("settings.ind.boll", "BOLL \u901A\u9053"), indicatorConfig.showBoll, (v) => {
228
303
  indicatorConfig = { ...indicatorConfig, showBoll: v };
229
304
  opts.onIndicatorConfigChange?.(indicatorConfig);
305
+ renderContent();
230
306
  })
231
307
  );
232
308
  content.appendChild(
@@ -265,37 +341,50 @@ function mountSettingsPanel(parent, opts = {}) {
265
341
  opts.onIndicatorConfigChange?.(indicatorConfig);
266
342
  })
267
343
  );
268
- if (opts.onClearAllIndicators) {
269
- content.appendChild(
270
- actionButton(t("settings.ind.clearAll", "\u6E05\u7A7A\u6240\u6709\u6307\u6A19"), () => {
271
- opts.onClearAllIndicators?.();
272
- if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
273
- renderContent();
274
- })
275
- );
276
- }
344
+ content.appendChild(
345
+ actionButton(t("settings.ind.clearAll", "\u6E05\u7A7A\u6240\u6709\u6307\u6A19"), () => {
346
+ opts.onClearAllIndicators?.();
347
+ if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
348
+ renderContent();
349
+ })
350
+ );
277
351
  }
278
352
  };
279
- renderTabs();
280
- renderContent();
281
- panel.append(tabs, content);
282
- btn.onclick = (e) => {
283
- e.stopPropagation();
284
- open = !open;
353
+ const setOpen = (next) => {
354
+ open = next;
355
+ overlay.style.display = open ? "flex" : "none";
285
356
  if (open) {
286
357
  if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
287
358
  renderContent();
288
359
  }
289
- panel.style.display = open ? "block" : "none";
290
360
  };
291
- const close = () => {
292
- open = false;
293
- panel.style.display = "none";
361
+ renderTabs();
362
+ renderContent();
363
+ dialog.append(header, tabs, content);
364
+ overlay.appendChild(dialog);
365
+ document.body.appendChild(overlay);
366
+ btn.onclick = (e) => {
367
+ e.stopPropagation();
368
+ setOpen(!open);
294
369
  };
295
- document.addEventListener("click", close);
296
- panel.onclick = (e) => e.stopPropagation();
297
- wrap.append(btn, panel);
370
+ closeBtn.onclick = (e) => {
371
+ e.stopPropagation();
372
+ setOpen(false);
373
+ };
374
+ overlay.onclick = () => setOpen(false);
375
+ dialog.onclick = (e) => e.stopPropagation();
376
+ const onKey = (e) => {
377
+ if (e.key === "Escape" && open) setOpen(false);
378
+ };
379
+ document.addEventListener("keydown", onKey);
380
+ wrap.append(btn);
298
381
  parent.appendChild(wrap);
382
+ const origRemove = wrap.remove.bind(wrap);
383
+ wrap.remove = () => {
384
+ document.removeEventListener("keydown", onKey);
385
+ overlay.remove();
386
+ origRemove();
387
+ };
299
388
  return wrap;
300
389
  }
301
390
 
@@ -713,6 +802,9 @@ function createI18nProvider(defaultLocale = "zh-TW") {
713
802
  };
714
803
  }
715
804
 
805
+ // src/chart-layout.ts
806
+ import { t as t7 } from "@coderyo/i18n";
807
+
716
808
  // src/context-menu.ts
717
809
  import { t as t3 } from "@coderyo/i18n";
718
810
  function attachChartContextMenu(chartHost, opts = {}) {
@@ -778,7 +870,7 @@ function mountCrosshairLegend(chartHost, opts = {}) {
778
870
  title.style.cssText = "color:#8b949e;margin-bottom:2px;";
779
871
  const body = document.createElement("div");
780
872
  box.append(title, body);
781
- chartHost.appendChild(box);
873
+ if (chartHost) chartHost.appendChild(box);
782
874
  const fmt2 = (n) => n == null ? "\u2014" : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
783
875
  const render = (payload) => {
784
876
  const parts = [opts.symbol, opts.interval].filter(Boolean);
@@ -811,7 +903,7 @@ import { t as t4 } from "@coderyo/i18n";
811
903
  function mountDrawingPropertiesPanel(parent, opts = {}) {
812
904
  const panel = document.createElement("aside");
813
905
  panel.className = "tv-drawing-props";
814
- panel.style.cssText = "display:none;width:220px;flex-shrink:0;border-left:1px solid #30363d;background:#161b22;padding:10px 12px;font-size:12px;color:#e6edf3;overflow:auto;";
906
+ panel.style.cssText = "display:none;width:100%;max-width:200px;flex-shrink:0;border-left:1px solid #30363d;background:#161b22;padding:10px 12px;font-size:12px;color:#e6edf3;overflow:auto;box-sizing:border-box;";
815
907
  const title = document.createElement("div");
816
908
  title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
817
909
  title.style.cssText = "font-weight:600;margin-bottom:10px;";
@@ -889,6 +981,414 @@ function mountIndicatorPaneHost(parent) {
889
981
  return host;
890
982
  }
891
983
 
984
+ // src/layout-schema.ts
985
+ var LAYOUT_SCHEMA_VERSION = 1;
986
+ var DEFAULT_LAYOUT_SCHEMA = {
987
+ version: 1,
988
+ columns: 12,
989
+ rows: 12,
990
+ widgets: [
991
+ { id: "topBar", col: 0, row: 0, colSpan: 12, rowSpan: 1 },
992
+ { id: "leftToolbar", col: 0, row: 1, colSpan: 1, rowSpan: 10 },
993
+ { id: "chartHost", col: 1, row: 1, colSpan: 9, rowSpan: 6 },
994
+ { id: "indicatorHost", col: 1, row: 7, colSpan: 9, rowSpan: 2 },
995
+ { id: "statusBar", col: 1, row: 9, colSpan: 9, rowSpan: 1 },
996
+ { id: "propertiesPanel", col: 10, row: 1, colSpan: 2, rowSpan: 9 },
997
+ { id: "bottomToolbar", col: 0, row: 11, colSpan: 12, rowSpan: 1 }
998
+ ]
999
+ };
1000
+ function layoutStorageKey(layoutId) {
1001
+ return `tradview:layout:v${LAYOUT_SCHEMA_VERSION}:${layoutId}`;
1002
+ }
1003
+ function resolveLayoutSchema(partial) {
1004
+ if (!partial) return cloneLayoutSchema(DEFAULT_LAYOUT_SCHEMA);
1005
+ return normalizeLayoutSchema(partial);
1006
+ }
1007
+ function cloneLayoutSchema(schema) {
1008
+ return {
1009
+ version: 1,
1010
+ columns: schema.columns,
1011
+ rows: schema.rows,
1012
+ widgets: schema.widgets.map((w) => ({ ...w }))
1013
+ };
1014
+ }
1015
+ function normalizeLayoutSchema(input) {
1016
+ const columns = Math.max(4, Math.min(24, input.columns || 12));
1017
+ const rows = Math.max(4, Math.min(24, input.rows || 12));
1018
+ const widgets = [];
1019
+ const seen = /* @__PURE__ */ new Set();
1020
+ for (const w of input.widgets ?? []) {
1021
+ if (!isLayoutWidgetId(w.id) || seen.has(w.id)) continue;
1022
+ seen.add(w.id);
1023
+ widgets.push(clampPlacement(w, columns, rows));
1024
+ }
1025
+ for (const d of DEFAULT_LAYOUT_SCHEMA.widgets) {
1026
+ if (!seen.has(d.id)) widgets.push({ ...d });
1027
+ }
1028
+ return { version: 1, columns, rows, widgets };
1029
+ }
1030
+ function clampPlacement(w, columns, rows) {
1031
+ const colSpan = Math.max(1, Math.min(columns, w.colSpan || 1));
1032
+ const rowSpan = Math.max(1, Math.min(rows, w.rowSpan || 1));
1033
+ const col = Math.max(0, Math.min(columns - colSpan, w.col || 0));
1034
+ const row = Math.max(0, Math.min(rows - rowSpan, w.row || 0));
1035
+ return { id: w.id, col, row, colSpan, rowSpan };
1036
+ }
1037
+ function isLayoutWidgetId(id) {
1038
+ return id === "topBar" || id === "leftToolbar" || id === "bottomToolbar" || id === "chartHost" || id === "indicatorHost" || id === "statusBar" || id === "propertiesPanel";
1039
+ }
1040
+ function loadLayoutSchema(layoutId) {
1041
+ try {
1042
+ const raw = localStorage.getItem(layoutStorageKey(layoutId));
1043
+ if (!raw) return null;
1044
+ return normalizeLayoutSchema(JSON.parse(raw));
1045
+ } catch {
1046
+ return null;
1047
+ }
1048
+ }
1049
+ function saveLayoutSchema(layoutId, schema) {
1050
+ try {
1051
+ localStorage.setItem(layoutStorageKey(layoutId), JSON.stringify(normalizeLayoutSchema(schema)));
1052
+ } catch {
1053
+ }
1054
+ }
1055
+ function getWidgetPlacement(schema, id) {
1056
+ return schema.widgets.find((w) => w.id === id);
1057
+ }
1058
+
1059
+ // src/layout-engine.ts
1060
+ function createLayoutGrid(opts) {
1061
+ let schema = cloneLayoutSchema(opts.schema);
1062
+ const cells = /* @__PURE__ */ new Map();
1063
+ const root = document.createElement("div");
1064
+ root.className = "tv-layout-root";
1065
+ root.style.cssText = "display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;width:100%;height:100%;box-sizing:border-box;";
1066
+ const grid = document.createElement("div");
1067
+ grid.className = "tv-layout-grid";
1068
+ grid.style.cssText = "display:grid;flex:1;min-height:0;min-width:0;width:100%;position:relative;gap:0;";
1069
+ root.appendChild(grid);
1070
+ for (const w of schema.widgets) {
1071
+ const cell = document.createElement("div");
1072
+ cell.className = `tv-layout-cell tv-layout-cell--${w.id}`;
1073
+ cell.dataset.widgetId = w.id;
1074
+ cell.style.cssText = "min-width:0;min-height:0;overflow:hidden;position:relative;box-sizing:border-box;";
1075
+ const el = opts.widgets[w.id];
1076
+ if (el) {
1077
+ el.style.width = "100%";
1078
+ el.style.height = "100%";
1079
+ el.style.minWidth = "0";
1080
+ el.style.minHeight = "0";
1081
+ if (w.id === "chartHost") el.style.position = "relative";
1082
+ cell.appendChild(el);
1083
+ }
1084
+ cells.set(w.id, cell);
1085
+ grid.appendChild(cell);
1086
+ }
1087
+ let editorCtl = null;
1088
+ const applySchema = (next, features) => {
1089
+ schema = cloneLayoutSchema(next);
1090
+ applyGridTemplate(grid, schema, features);
1091
+ for (const w of schema.widgets) {
1092
+ const cell = cells.get(w.id);
1093
+ if (!cell) continue;
1094
+ applyPlacementStyle(cell, w);
1095
+ const visible = widgetVisible(w.id, features);
1096
+ cell.style.display = visible ? "" : "none";
1097
+ cell.style.pointerEvents = visible ? "" : "none";
1098
+ }
1099
+ editorCtl?.refresh(schema);
1100
+ };
1101
+ const enableEditor = (enabled) => {
1102
+ if (enabled) {
1103
+ if (!editorCtl) {
1104
+ editorCtl = new LayoutEditorController(grid, cells, schema, (s) => {
1105
+ schema = s;
1106
+ opts.onSchemaChange?.(s);
1107
+ });
1108
+ }
1109
+ editorCtl.enable(true);
1110
+ root.classList.add("tv-layout--edit");
1111
+ } else {
1112
+ editorCtl?.enable(false);
1113
+ root.classList.remove("tv-layout--edit");
1114
+ }
1115
+ };
1116
+ applySchema(schema, opts.features);
1117
+ if (opts.editor) enableEditor(true);
1118
+ return {
1119
+ root,
1120
+ grid,
1121
+ cells,
1122
+ applySchema,
1123
+ enableEditor,
1124
+ getSchema: () => cloneLayoutSchema(schema),
1125
+ setSchema: (s) => {
1126
+ schema = cloneLayoutSchema(s);
1127
+ applySchema(schema, opts.features);
1128
+ },
1129
+ destroyEditor: () => {
1130
+ editorCtl?.destroy();
1131
+ editorCtl = null;
1132
+ }
1133
+ };
1134
+ }
1135
+ function widgetVisible(id, f) {
1136
+ switch (id) {
1137
+ case "topBar":
1138
+ return f.showTopBar;
1139
+ case "leftToolbar":
1140
+ return f.showLeftToolbar;
1141
+ case "bottomToolbar":
1142
+ return f.showBottomToolbar;
1143
+ case "statusBar":
1144
+ return f.showStatusBar;
1145
+ case "propertiesPanel":
1146
+ return f.showPropertiesPanel;
1147
+ case "chartHost":
1148
+ case "indicatorHost":
1149
+ return true;
1150
+ default:
1151
+ return true;
1152
+ }
1153
+ }
1154
+ var LEFT_TOOLBAR_COL_PX = 44;
1155
+ function applyGridTemplate(grid, schema, features) {
1156
+ const narrowToolbar = features?.showLeftToolbar ?? false;
1157
+ if (narrowToolbar && schema.columns >= 2) {
1158
+ grid.style.gridTemplateColumns = `${LEFT_TOOLBAR_COL_PX}px repeat(${schema.columns - 1}, minmax(0, 1fr))`;
1159
+ } else {
1160
+ grid.style.gridTemplateColumns = `repeat(${schema.columns}, minmax(0, 1fr))`;
1161
+ }
1162
+ grid.style.gridTemplateRows = `repeat(${schema.rows}, minmax(0, 1fr))`;
1163
+ }
1164
+ function applyPlacementStyle(cell, w) {
1165
+ cell.style.gridColumn = `${w.col + 1} / span ${w.colSpan}`;
1166
+ cell.style.gridRow = `${w.row + 1} / span ${w.rowSpan}`;
1167
+ }
1168
+ var LayoutEditorController = class {
1169
+ constructor(grid, cells, schema, onChange) {
1170
+ this.grid = grid;
1171
+ this.cells = cells;
1172
+ this.schema = schema;
1173
+ this.onChange = onChange;
1174
+ }
1175
+ grid;
1176
+ cells;
1177
+ schema;
1178
+ onChange;
1179
+ enabled = false;
1180
+ drag = null;
1181
+ resize = null;
1182
+ cleanups = [];
1183
+ refresh(schema) {
1184
+ this.schema = cloneLayoutSchema(schema);
1185
+ for (const w of this.schema.widgets) {
1186
+ const cell = this.cells.get(w.id);
1187
+ if (cell) applyPlacementStyle(cell, w);
1188
+ }
1189
+ }
1190
+ enable(on) {
1191
+ if (this.enabled === on) return;
1192
+ this.enabled = on;
1193
+ if (on) this.bind();
1194
+ else this.unbind();
1195
+ }
1196
+ destroy() {
1197
+ this.enable(false);
1198
+ }
1199
+ bind() {
1200
+ for (const [id, cell] of this.cells) {
1201
+ const handle = document.createElement("div");
1202
+ handle.className = "tv-layout-drag-handle";
1203
+ handle.title = `Drag ${id}`;
1204
+ handle.style.cssText = "position:absolute;top:2px;left:2px;z-index:20;padding:2px 6px;font-size:10px;background:#388bfd;color:#fff;border-radius:3px;cursor:grab;user-select:none;opacity:0.85;";
1205
+ handle.textContent = id;
1206
+ const resize = document.createElement("div");
1207
+ resize.className = "tv-layout-resize-handle";
1208
+ resize.title = "Resize";
1209
+ resize.style.cssText = "position:absolute;right:2px;bottom:2px;width:12px;height:12px;z-index:20;background:#388bfd;border-radius:2px;cursor:nwse-resize;opacity:0.85;";
1210
+ cell.style.outline = "1px dashed #388bfd";
1211
+ cell.appendChild(handle);
1212
+ cell.appendChild(resize);
1213
+ const onDragDown = (ev) => {
1214
+ ev.preventDefault();
1215
+ const p = getPlacement(this.schema, id);
1216
+ if (!p) return;
1217
+ this.drag = { id, startX: ev.clientX, startY: ev.clientY, origin: { ...p } };
1218
+ handle.setPointerCapture(ev.pointerId);
1219
+ };
1220
+ const onDragMove = (ev) => {
1221
+ if (!this.drag || this.drag.id !== id) return;
1222
+ const delta = pixelDeltaToGrid(this.grid, this.schema, ev.clientX - this.drag.startX, ev.clientY - this.drag.startY);
1223
+ const next = clampPlacement2(
1224
+ {
1225
+ ...this.drag.origin,
1226
+ col: this.drag.origin.col + delta.dCol,
1227
+ row: this.drag.origin.row + delta.dRow
1228
+ },
1229
+ this.schema
1230
+ );
1231
+ updatePlacement(this.schema, next);
1232
+ applyPlacementStyle(cell, next);
1233
+ };
1234
+ const onDragUp = (ev) => {
1235
+ if (!this.drag || this.drag.id !== id) return;
1236
+ this.drag = null;
1237
+ handle.releasePointerCapture(ev.pointerId);
1238
+ this.onChange(cloneLayoutSchema(this.schema));
1239
+ };
1240
+ const onResizeDown = (ev) => {
1241
+ ev.preventDefault();
1242
+ ev.stopPropagation();
1243
+ const p = getPlacement(this.schema, id);
1244
+ if (!p) return;
1245
+ this.resize = { id, startX: ev.clientX, startY: ev.clientY, origin: { ...p } };
1246
+ resize.setPointerCapture(ev.pointerId);
1247
+ };
1248
+ const onResizeMove = (ev) => {
1249
+ if (!this.resize || this.resize.id !== id) return;
1250
+ const delta = pixelDeltaToGrid(this.grid, this.schema, ev.clientX - this.resize.startX, ev.clientY - this.resize.startY);
1251
+ const next = clampPlacement2(
1252
+ {
1253
+ ...this.resize.origin,
1254
+ colSpan: this.resize.origin.colSpan + delta.dCol,
1255
+ rowSpan: this.resize.origin.rowSpan + delta.dRow
1256
+ },
1257
+ this.schema
1258
+ );
1259
+ updatePlacement(this.schema, next);
1260
+ applyPlacementStyle(cell, next);
1261
+ };
1262
+ const onResizeUp = (ev) => {
1263
+ if (!this.resize || this.resize.id !== id) return;
1264
+ this.resize = null;
1265
+ resize.releasePointerCapture(ev.pointerId);
1266
+ this.onChange(cloneLayoutSchema(this.schema));
1267
+ };
1268
+ handle.addEventListener("pointerdown", onDragDown);
1269
+ handle.addEventListener("pointermove", onDragMove);
1270
+ handle.addEventListener("pointerup", onDragUp);
1271
+ resize.addEventListener("pointerdown", onResizeDown);
1272
+ resize.addEventListener("pointermove", onResizeMove);
1273
+ resize.addEventListener("pointerup", onResizeUp);
1274
+ this.cleanups.push(() => {
1275
+ handle.remove();
1276
+ resize.remove();
1277
+ cell.style.outline = "";
1278
+ });
1279
+ }
1280
+ }
1281
+ unbind() {
1282
+ for (const fn of this.cleanups) fn();
1283
+ this.cleanups.length = 0;
1284
+ this.drag = null;
1285
+ this.resize = null;
1286
+ }
1287
+ };
1288
+ function getPlacement(schema, id) {
1289
+ return schema.widgets.find((w) => w.id === id);
1290
+ }
1291
+ function updatePlacement(schema, p) {
1292
+ const i = schema.widgets.findIndex((w) => w.id === p.id);
1293
+ if (i >= 0) schema.widgets[i] = p;
1294
+ }
1295
+ function clampPlacement2(p, schema) {
1296
+ const colSpan = Math.max(1, Math.min(schema.columns - p.col, p.colSpan));
1297
+ const rowSpan = Math.max(1, Math.min(schema.rows - p.row, p.rowSpan));
1298
+ const col = Math.max(0, Math.min(schema.columns - colSpan, p.col));
1299
+ const row = Math.max(0, Math.min(schema.rows - rowSpan, p.row));
1300
+ return { ...p, col, row, colSpan, rowSpan };
1301
+ }
1302
+ function pixelDeltaToGrid(grid, schema, dx, dy) {
1303
+ const rect = grid.getBoundingClientRect();
1304
+ const cellW = rect.width / schema.columns;
1305
+ const cellH = rect.height / schema.rows;
1306
+ return {
1307
+ dCol: cellW > 0 ? Math.round(dx / cellW) : 0,
1308
+ dRow: cellH > 0 ? Math.round(dy / cellH) : 0
1309
+ };
1310
+ }
1311
+
1312
+ // src/layer/compositor-shell.ts
1313
+ function createCompositorShell(opts) {
1314
+ const cells = /* @__PURE__ */ new Map();
1315
+ const root = document.createElement("div");
1316
+ root.className = "tv-layout-root tv-layout-root--compositor";
1317
+ root.style.cssText = "display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;width:100%;height:100%;box-sizing:border-box;position:relative;";
1318
+ const grid = document.createElement("div");
1319
+ grid.className = "tv-compositor-shell-grid";
1320
+ grid.style.cssText = "position:absolute;inset:0;overflow:hidden;visibility:hidden;pointer-events:none;z-index:0;";
1321
+ root.appendChild(grid);
1322
+ const widgetIds = [
1323
+ "topBar",
1324
+ "leftToolbar",
1325
+ "bottomToolbar",
1326
+ "chartHost",
1327
+ "indicatorHost",
1328
+ "statusBar",
1329
+ "propertiesPanel"
1330
+ ];
1331
+ for (const id of widgetIds) {
1332
+ const cell = document.createElement("div");
1333
+ cell.className = `tv-layout-cell tv-layout-cell--${id}`;
1334
+ cell.dataset.widgetId = id;
1335
+ cell.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;box-sizing:border-box;";
1336
+ const el = opts.widgets[id];
1337
+ if (el) {
1338
+ el.style.width = "100%";
1339
+ el.style.height = "100%";
1340
+ el.style.minWidth = "0";
1341
+ el.style.minHeight = "0";
1342
+ if (id === "chartHost") el.style.position = "relative";
1343
+ cell.appendChild(el);
1344
+ }
1345
+ cells.set(id, cell);
1346
+ grid.appendChild(cell);
1347
+ }
1348
+ return { root, grid, cells };
1349
+ }
1350
+ function applyCompositorShellFeatures(_cells, _features) {
1351
+ }
1352
+
1353
+ // src/layer/compositor-shell-sync.ts
1354
+ var SHELL_FEATURE_KEYS = [
1355
+ "showTopBar",
1356
+ "showLeftToolbar",
1357
+ "showBottomToolbar",
1358
+ "showStatusBar",
1359
+ "showPropertiesPanel",
1360
+ "showCrosshairLegend"
1361
+ ];
1362
+ function shellLayerMatchesFeature(layer, feature) {
1363
+ switch (feature) {
1364
+ case "showTopBar":
1365
+ return layer.type === "shell.topBar" || layer.widgetKey === "topBar";
1366
+ case "showLeftToolbar":
1367
+ return layer.type === "shell.leftToolbar" || layer.widgetKey === "leftToolbar";
1368
+ case "showBottomToolbar":
1369
+ return layer.type === "shell.bottomToolbar" || layer.widgetKey === "bottomToolbar";
1370
+ case "showStatusBar":
1371
+ return layer.type === "shell.statusBar" || layer.widgetKey === "statusBar";
1372
+ case "showPropertiesPanel":
1373
+ return layer.type === "shell.propertiesPanel" || layer.widgetKey === "propertiesPanel";
1374
+ case "showCrosshairLegend":
1375
+ return layer.type === "overlay.crosshairLegend" || layer.widgetKey === "crosshairLegend";
1376
+ default:
1377
+ return false;
1378
+ }
1379
+ }
1380
+ function syncCompositorShellVisibilityFromFeatures(controller, features) {
1381
+ const pageLayers = controller.getLayersForActivePage();
1382
+ for (const feature of SHELL_FEATURE_KEYS) {
1383
+ const visible = features[feature];
1384
+ for (const layer of pageLayers) {
1385
+ if (shellLayerMatchesFeature(layer, feature)) {
1386
+ controller.setLayerVisible(layer.id, visible);
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+
892
1392
  // src/layout-features.ts
893
1393
  var DEFAULT_LAYOUT_FEATURES = {
894
1394
  showTopBar: false,
@@ -1072,7 +1572,7 @@ var DRAWING_TOOLS = [
1072
1572
  ];
1073
1573
  var MOBILE_MQ = "(max-width: 768px)";
1074
1574
  function mountToolButtons(parent, activeTool, onSelect, layout) {
1075
- const btnStyle = layout === "column" ? "width:36px;height:32px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:14px;" : "min-width:40px;height:36px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:14px;padding:0 8px;";
1575
+ const btnStyle = layout === "column" ? "width:32px;height:28px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:13px;padding:0;" : "min-width:36px;height:32px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:13px;padding:0 6px;";
1076
1576
  const activeStyle = btnStyle.replace("#21262d", "#388bfd").replace("#e6edf3", "#fff");
1077
1577
  const buttons = /* @__PURE__ */ new Map();
1078
1578
  for (const tool of DRAWING_TOOLS) {
@@ -1095,21 +1595,26 @@ function mountToolButtons(parent, activeTool, onSelect, layout) {
1095
1595
  }
1096
1596
  function mountChartLayout(root, opts = {}) {
1097
1597
  let layoutFeatures = resolveLayoutFeatures(opts);
1598
+ const layoutId = opts.layoutId ?? "default";
1599
+ root.replaceChildren();
1098
1600
  root.style.display = "flex";
1099
1601
  root.style.flexDirection = "column";
1100
- root.style.height = "100%";
1101
- root.style.width = "100%";
1602
+ root.style.height = root.style.height || "100%";
1603
+ root.style.width = root.style.width || "100%";
1102
1604
  root.style.minWidth = "0";
1103
1605
  root.style.boxSizing = "border-box";
1104
- root.style.overflow = "visible";
1105
- const themeProvider = opts.themeProvider ?? createThemeProvider();
1106
- const i18nProvider = opts.i18n ?? createI18nProvider();
1107
- themeProvider.subscribe((theme) => {
1606
+ root.style.overflow = "hidden";
1607
+ const useAutoTheme = opts.autoThemeProvider !== false;
1608
+ const useAutoI18n = opts.autoI18n !== false;
1609
+ const themeProvider = opts.themeProvider ?? (useAutoTheme ? createThemeProvider() : void 0);
1610
+ const i18nProvider = opts.i18n ?? (useAutoI18n ? createI18nProvider() : void 0);
1611
+ themeProvider?.subscribe((theme) => {
1108
1612
  root.style.background = theme === "dark" ? "#0d1117" : "#f6f8fa";
1109
1613
  });
1110
1614
  let activeTool = opts.activeDrawingTool ?? "cursor";
1111
1615
  let setActiveDesktop = null;
1112
1616
  let setActiveMobile = null;
1617
+ let lastSelectedDrawing = null;
1113
1618
  const setActiveDrawingTool = (tool) => {
1114
1619
  activeTool = tool;
1115
1620
  setActiveDesktop?.(tool);
@@ -1119,69 +1624,173 @@ function mountChartLayout(root, opts = {}) {
1119
1624
  setActiveDrawingTool(tool);
1120
1625
  opts.onDrawingToolSelect?.(tool);
1121
1626
  };
1122
- const headerSlot = document.createElement("div");
1123
- headerSlot.className = "tv-layout-header";
1124
- headerSlot.style.cssText = "display:none;flex-shrink:0;width:100%;min-width:0;overflow:visible;position:relative;z-index:30;";
1125
- const body = document.createElement("div");
1126
- body.className = "tv-layout-body";
1127
- body.style.cssText = "display:flex;flex:1;min-height:0;min-width:0;overflow:hidden;position:relative;z-index:0;";
1128
- const leftAside = document.createElement("aside");
1129
- leftAside.style.cssText = "width:48px;border-right:1px solid #30363d;background:#161b22;display:flex;flex-direction:column;align-items:center;padding:8px 4px;gap:8px;flex-shrink:0;";
1130
- const bottomBar = document.createElement("div");
1131
- bottomBar.style.cssText = "display:none;flex-shrink:0;gap:6px;padding:6px 8px;border-top:1px solid #30363d;background:#161b22;overflow-x:auto;";
1132
- const chartColumn = document.createElement("div");
1133
- chartColumn.style.cssText = "display:flex;flex-direction:column;flex:1;min-height:0;";
1627
+ const topBarHost = document.createElement("div");
1628
+ topBarHost.className = "tv-layout-header";
1629
+ topBarHost.style.cssText = "width:100%;min-width:0;overflow:visible;position:relative;z-index:30;";
1630
+ const leftToolbar = document.createElement("aside");
1631
+ leftToolbar.style.cssText = "width:100%;height:100%;max-width:44px;border-right:1px solid #30363d;background:#161b22;display:flex;flex-direction:column;align-items:center;padding:4px 2px;gap:4px;box-sizing:border-box;overflow:hidden;";
1632
+ const bottomToolbar = document.createElement("div");
1633
+ bottomToolbar.style.cssText = "width:100%;height:100%;gap:6px;padding:6px 8px;border-top:1px solid #30363d;background:#161b22;overflow-x:auto;box-sizing:border-box;display:flex;flex-direction:row;";
1634
+ const chartMain = document.createElement("div");
1635
+ chartMain.className = "tv-chart-main-mount";
1636
+ chartMain.style.cssText = "width:100%;height:100%;min-height:120px;position:relative;overflow:hidden;box-sizing:border-box;";
1637
+ const chartVolume = document.createElement("div");
1638
+ chartVolume.className = "tv-chart-volume-mount";
1639
+ chartVolume.style.cssText = "width:100%;height:100%;min-height:48px;position:relative;overflow:hidden;box-sizing:border-box;";
1134
1640
  const chartHost = document.createElement("div");
1135
- chartHost.style.cssText = "flex:1;min-height:0;width:100%;height:100%;position:relative;overflow:hidden;";
1136
- chartColumn.appendChild(chartHost);
1137
- const indicatorHost = mountIndicatorPaneHost(chartColumn);
1138
- const statusBar = mountStatusBar(chartColumn, opts.statusBar ?? {});
1139
- body.appendChild(chartColumn);
1140
- const propertiesPanel = mountDrawingPropertiesPanel(body, {
1641
+ chartHost.className = "tv-chart-grid-anchor";
1642
+ chartHost.style.cssText = "width:100%;height:100%;min-height:0;position:relative;overflow:hidden;box-sizing:border-box;";
1643
+ const indicatorMount = document.createElement("div");
1644
+ indicatorMount.style.cssText = "width:100%;height:100%;min-height:0;display:flex;flex-direction:column;";
1645
+ const indicatorHost = mountIndicatorPaneHost(indicatorMount);
1646
+ indicatorHost.style.flex = "1";
1647
+ const statusMount = document.createElement("div");
1648
+ statusMount.style.cssText = "width:100%;height:100%;";
1649
+ const statusBar = mountStatusBar(statusMount, opts.statusBar ?? {});
1650
+ const propertiesMount = document.createElement("div");
1651
+ propertiesMount.style.cssText = "width:100%;height:100%;min-height:0;";
1652
+ const propertiesPanel = mountDrawingPropertiesPanel(propertiesMount, {
1141
1653
  onStyleChange: opts.onDrawingStyleChange
1142
1654
  });
1143
- opts.onDrawingSelectionBind?.(propertiesPanel.bind);
1144
- root.appendChild(headerSlot);
1145
- root.appendChild(body);
1146
- root.appendChild(bottomBar);
1147
- let topBar = headerSlot;
1655
+ const layerCompositorManaged = opts.layerCompositorManaged === true;
1656
+ const drawingOverlay = chartMain;
1657
+ let loaded = null;
1658
+ let layoutSchema = resolveLayoutSchema(null);
1659
+ if (!layerCompositorManaged) {
1660
+ loaded = opts.layout !== void 0 && opts.layout !== null ? resolveLayoutSchema(opts.layout) : opts.layoutPersist ? loadLayoutSchema(layoutId) : null;
1661
+ layoutSchema = resolveLayoutSchema(loaded);
1662
+ }
1663
+ const persistLayout = (schema) => {
1664
+ if (layerCompositorManaged) return;
1665
+ layoutSchema = resolveLayoutSchema(schema);
1666
+ if (opts.layoutPersist) saveLayoutSchema(layoutId, layoutSchema);
1667
+ opts.onLayoutChange?.(layoutSchema);
1668
+ };
1669
+ let layoutShell;
1670
+ if (layerCompositorManaged) {
1671
+ layoutShell = createCompositorShell({
1672
+ widgets: {
1673
+ topBar: topBarHost,
1674
+ leftToolbar,
1675
+ bottomToolbar,
1676
+ chartHost,
1677
+ indicatorHost: indicatorMount,
1678
+ statusBar: statusMount,
1679
+ propertiesPanel: propertiesMount
1680
+ }
1681
+ });
1682
+ } else {
1683
+ layoutShell = createLayoutGrid({
1684
+ schema: layoutSchema,
1685
+ features: layoutFeatures,
1686
+ editor: opts.layoutEditor ?? false,
1687
+ onSchemaChange: persistLayout,
1688
+ widgets: {
1689
+ topBar: topBarHost,
1690
+ leftToolbar,
1691
+ bottomToolbar,
1692
+ chartHost,
1693
+ indicatorHost: indicatorMount,
1694
+ statusBar: statusMount,
1695
+ propertiesPanel: propertiesMount
1696
+ }
1697
+ });
1698
+ }
1699
+ root.appendChild(layoutShell.root);
1700
+ let topBar = topBarHost;
1148
1701
  let setActiveInterval = () => {
1149
1702
  };
1150
- const crosshairLegend = mountCrosshairLegend(chartHost, { symbol: opts.initialSymbol });
1703
+ const crosshairLegend = mountCrosshairLegend(
1704
+ layerCompositorManaged ? void 0 : chartMain,
1705
+ { symbol: opts.initialSymbol }
1706
+ );
1151
1707
  let detachContextMenu = () => {
1152
1708
  };
1153
1709
  let shortcutsBound = false;
1710
+ let disposeMobileMq = null;
1711
+ const mountToolbarActions = (parent, compact) => {
1712
+ const settings = opts.settings;
1713
+ if (!settings?.onClearAllDrawings && !settings?.onClearAllIndicators) return;
1714
+ const spacer = document.createElement("div");
1715
+ spacer.style.flex = compact ? "1" : "0";
1716
+ spacer.style.minHeight = compact ? "4px" : "0";
1717
+ parent.appendChild(spacer);
1718
+ const mk = (label, fn) => {
1719
+ const b = document.createElement("button");
1720
+ b.type = "button";
1721
+ b.title = label;
1722
+ b.textContent = compact ? label === t7("toolbar.clearDrawings", "\u6E05\u9664\u7E6A\u5716") ? "\u2327" : "\u2205" : label;
1723
+ b.style.cssText = compact ? "width:32px;height:24px;font-size:10px;background:#21262d;color:#f85149;border:1px solid #30363d;border-radius:4px;cursor:pointer;" : "min-width:36px;height:28px;font-size:10px;background:#21262d;color:#f85149;border:1px solid #30363d;border-radius:4px;cursor:pointer;padding:0 4px;";
1724
+ b.onclick = () => fn?.();
1725
+ return b;
1726
+ };
1727
+ if (settings.onClearAllDrawings) {
1728
+ parent.appendChild(mk(t7("toolbar.clearDrawings", "\u6E05\u9664\u7E6A\u5716"), settings.onClearAllDrawings));
1729
+ }
1730
+ if (settings.onClearAllIndicators) {
1731
+ parent.appendChild(mk(t7("toolbar.clearIndicators", "\u6E05\u9664\u6307\u6A19"), settings.onClearAllIndicators));
1732
+ }
1733
+ };
1154
1734
  const mountLeftToolbar = () => {
1155
- if (leftAside.parentElement) return;
1156
- const desktopTools = mountToolButtons(leftAside, activeTool, onToolSelect, "column");
1735
+ leftToolbar.replaceChildren();
1736
+ const desktopTools = mountToolButtons(leftToolbar, activeTool, onToolSelect, "column");
1157
1737
  setActiveDesktop = desktopTools.setActive;
1158
- body.insertBefore(leftAside, chartColumn);
1159
- bottomBar.style.flexDirection = "row";
1160
- const mobileTools = mountToolButtons(bottomBar, activeTool, onToolSelect, "row");
1738
+ mountToolbarActions(leftToolbar, true);
1739
+ bottomToolbar.replaceChildren();
1740
+ const mobileTools = mountToolButtons(bottomToolbar, activeTool, onToolSelect, "row");
1161
1741
  setActiveMobile = mobileTools.setActive;
1162
- const mq = window.matchMedia(MOBILE_MQ);
1163
- const applyLayout = () => {
1164
- const mobile = mq.matches;
1165
- leftAside.style.display = mobile ? "none" : "flex";
1166
- const showBottom = layoutFeatures.showBottomToolbar !== false;
1167
- bottomBar.style.display = mobile && showBottom ? "flex" : "none";
1168
- };
1169
- mq.addEventListener("change", applyLayout);
1170
- applyLayout();
1742
+ mountToolbarActions(bottomToolbar, false);
1171
1743
  };
1172
- const unmountLeftToolbar = () => {
1173
- leftAside.remove();
1174
- bottomBar.innerHTML = "";
1175
- setActiveDesktop = null;
1176
- setActiveMobile = null;
1744
+ const chartAreaColStart = () => layoutFeatures.showLeftToolbar ? 2 : 1;
1745
+ const applyPropertiesPanelLayout = (drawing) => {
1746
+ const showPanel = layoutFeatures.showPropertiesPanel && drawing != null;
1747
+ propertiesPanel.el.style.display = showPanel ? "block" : "none";
1748
+ if (layerCompositorManaged) return;
1749
+ const propsCell = layoutShell.cells.get("propertiesPanel");
1750
+ const chartCell = layoutShell.cells.get("chartHost");
1751
+ const indCell = layoutShell.cells.get("indicatorHost");
1752
+ const statusCell = layoutShell.cells.get("statusBar");
1753
+ if (!propsCell || !chartCell) return;
1754
+ const cw = getWidgetPlacement(layoutSchema, "chartHost");
1755
+ const iw = getWidgetPlacement(layoutSchema, "indicatorHost");
1756
+ const sw = getWidgetPlacement(layoutSchema, "statusBar");
1757
+ const pw = getWidgetPlacement(layoutSchema, "propertiesPanel");
1758
+ const applyPlacement = (cell, p) => {
1759
+ cell.style.gridColumn = `${p.col + 1} / span ${p.colSpan}`;
1760
+ cell.style.gridRow = `${p.row + 1} / span ${p.rowSpan}`;
1761
+ };
1762
+ if (showPanel) {
1763
+ propsCell.style.display = "";
1764
+ if (pw) applyPlacement(propsCell, pw);
1765
+ if (cw) applyPlacement(chartCell, cw);
1766
+ if (indCell && iw) {
1767
+ indCell.style.display = "";
1768
+ applyPlacement(indCell, iw);
1769
+ }
1770
+ if (statusCell && sw) applyPlacement(statusCell, sw);
1771
+ } else {
1772
+ propsCell.style.display = "none";
1773
+ const end = layoutSchema.columns + 1;
1774
+ const start = chartAreaColStart();
1775
+ chartCell.style.gridColumn = `${start} / ${end}`;
1776
+ if (cw) chartCell.style.gridRow = `${cw.row + 1} / span ${cw.rowSpan}`;
1777
+ if (indCell && iw) {
1778
+ indCell.style.display = "";
1779
+ indCell.style.gridColumn = `${start} / ${end}`;
1780
+ indCell.style.gridRow = `${iw.row + 1} / span ${iw.rowSpan}`;
1781
+ }
1782
+ if (statusCell && sw) {
1783
+ statusCell.style.gridColumn = `${start} / ${end}`;
1784
+ statusCell.style.gridRow = `${sw.row + 1} / span ${sw.rowSpan}`;
1785
+ }
1786
+ }
1177
1787
  };
1178
1788
  const applyLayoutFeatures = () => {
1179
1789
  const f = layoutFeatures;
1180
1790
  if (f.showTopBar) {
1181
- headerSlot.replaceChildren();
1182
- headerSlot.style.display = "";
1791
+ topBarHost.replaceChildren();
1183
1792
  const mounted = mountTopBar(
1184
- headerSlot,
1793
+ topBarHost,
1185
1794
  Object.assign(opts, {
1186
1795
  symbolInput: f.symbolInput,
1187
1796
  showSettings: f.showSettings,
@@ -1196,18 +1805,44 @@ function mountChartLayout(root, opts = {}) {
1196
1805
  topBar = mounted.el;
1197
1806
  setActiveInterval = mounted.setActiveInterval;
1198
1807
  } else {
1199
- headerSlot.replaceChildren();
1200
- headerSlot.style.display = "none";
1201
- topBar = headerSlot;
1202
- }
1203
- if (f.showLeftToolbar) mountLeftToolbar();
1204
- else unmountLeftToolbar();
1205
- crosshairLegend.el.style.display = f.showCrosshairLegend ? "" : "none";
1206
- statusBar.el.style.display = f.showStatusBar ? "" : "none";
1207
- propertiesPanel.el.style.display = f.showPropertiesPanel ? "" : "none";
1808
+ topBarHost.replaceChildren();
1809
+ topBar = topBarHost;
1810
+ }
1811
+ if (f.showLeftToolbar || f.showBottomToolbar) mountLeftToolbar();
1812
+ else {
1813
+ leftToolbar.replaceChildren();
1814
+ bottomToolbar.replaceChildren();
1815
+ setActiveDesktop = null;
1816
+ setActiveMobile = null;
1817
+ }
1818
+ if (!layerCompositorManaged) {
1819
+ layoutShell.applySchema(layoutSchema, f);
1820
+ }
1821
+ disposeMobileMq?.();
1822
+ const mq = window.matchMedia(MOBILE_MQ);
1823
+ const applyMobileTools = () => {
1824
+ const features = layoutFeatures;
1825
+ const mobile = mq.matches;
1826
+ if (features.showLeftToolbar) {
1827
+ leftToolbar.style.display = mobile ? "none" : "flex";
1828
+ }
1829
+ if (features.showBottomToolbar) {
1830
+ bottomToolbar.style.display = mobile ? "flex" : "none";
1831
+ const bottomCell = layoutShell.cells.get("bottomToolbar");
1832
+ if (bottomCell) {
1833
+ bottomCell.style.display = mobile && features.showBottomToolbar ? "" : "none";
1834
+ }
1835
+ }
1836
+ };
1837
+ mq.addEventListener("change", applyMobileTools);
1838
+ disposeMobileMq = () => mq.removeEventListener("change", applyMobileTools);
1839
+ applyMobileTools();
1840
+ if (!layerCompositorManaged) {
1841
+ crosshairLegend.el.style.display = f.showCrosshairLegend ? "" : "none";
1842
+ }
1208
1843
  detachContextMenu();
1209
1844
  if (f.showContextMenu) {
1210
- detachContextMenu = attachChartContextMenu(chartHost, {
1845
+ detachContextMenu = attachChartContextMenu(chartMain, {
1211
1846
  actions: opts.contextMenuActions
1212
1847
  });
1213
1848
  }
@@ -1215,10 +1850,36 @@ function mountChartLayout(root, opts = {}) {
1215
1850
  bindShortcutsModal();
1216
1851
  shortcutsBound = true;
1217
1852
  }
1853
+ applyPropertiesPanelLayout(lastSelectedDrawing);
1218
1854
  };
1855
+ const handleDrawingSelection = (drawing) => {
1856
+ lastSelectedDrawing = drawing;
1857
+ propertiesPanel.bind(drawing);
1858
+ applyPropertiesPanelLayout(drawing);
1859
+ };
1860
+ opts.onDrawingSelectionBind?.(propertiesPanel.bind);
1219
1861
  applyLayoutFeatures();
1862
+ applyPropertiesPanelLayout(null);
1863
+ let boundCompositorController = null;
1864
+ const syncCompositorShellVisibility = layerCompositorManaged ? (controller) => {
1865
+ syncCompositorShellVisibilityFromFeatures(controller, layoutFeatures);
1866
+ } : void 0;
1867
+ const syncCompositorShellVisibilityIfBound = () => {
1868
+ if (boundCompositorController) {
1869
+ syncCompositorShellVisibilityFromFeatures(boundCompositorController, layoutFeatures);
1870
+ }
1871
+ };
1872
+ const bindLayerCompositorController = layerCompositorManaged ? (controller) => {
1873
+ boundCompositorController = controller;
1874
+ syncCompositorShellVisibilityFromFeatures(controller, layoutFeatures);
1875
+ } : void 0;
1220
1876
  return {
1221
- chartHost,
1877
+ layoutRoot: layoutShell.root,
1878
+ layoutGrid: layoutShell.grid,
1879
+ chartHost: chartMain,
1880
+ chartMain,
1881
+ chartVolume,
1882
+ drawingOverlay,
1222
1883
  indicatorHost,
1223
1884
  topBar,
1224
1885
  setActiveInterval,
@@ -1227,86 +1888,1950 @@ function mountChartLayout(root, opts = {}) {
1227
1888
  detachContextMenu,
1228
1889
  setActiveDrawingTool,
1229
1890
  propertiesPanel,
1891
+ handleDrawingSelection,
1892
+ syncCompositorShellVisibility,
1893
+ bindLayerCompositorController,
1230
1894
  setLayoutFeatures: (patch) => {
1231
1895
  layoutFeatures = mergeLayoutFeatures(layoutFeatures, patch);
1232
1896
  applyLayoutFeatures();
1897
+ syncCompositorShellVisibilityIfBound();
1233
1898
  },
1234
- getLayoutFeatures: () => ({ ...layoutFeatures })
1899
+ getLayoutFeatures: () => ({ ...layoutFeatures }),
1900
+ getLayoutSchema: () => layerCompositorManaged ? layoutSchema : layoutShell.getSchema(),
1901
+ setLayoutSchema: (schema) => {
1902
+ if (layerCompositorManaged) return;
1903
+ layoutSchema = resolveLayoutSchema(schema);
1904
+ const grid = layoutShell;
1905
+ grid.setSchema(layoutSchema);
1906
+ grid.applySchema(layoutSchema, layoutFeatures);
1907
+ persistLayout(layoutSchema);
1908
+ },
1909
+ enableLayoutEditor: (enabled) => {
1910
+ if (layerCompositorManaged) return;
1911
+ layoutShell.enableEditor(enabled);
1912
+ },
1913
+ saveLayout: () => {
1914
+ if (layerCompositorManaged) return;
1915
+ persistLayout(layoutShell.getSchema());
1916
+ }
1235
1917
  };
1236
1918
  }
1237
1919
 
1238
- // src/drawing-context-menu.ts
1239
- import { t as t7 } from "@coderyo/i18n";
1240
- function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
1241
- const menu = document.createElement("div");
1242
- menu.style.cssText = "position:fixed;z-index:60;min-width:168px;padding:4px 0;background:#161b22;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #01040988;";
1243
- menu.style.left = `${clientX}px`;
1244
- menu.style.top = `${clientY}px`;
1245
- const add = (label, fn, disabled = false) => {
1246
- if (!fn) return;
1247
- const btn = document.createElement("button");
1248
- btn.type = "button";
1249
- btn.textContent = label;
1250
- btn.disabled = disabled;
1251
- btn.style.cssText = "display:block;width:100%;text-align:left;padding:8px 12px;border:none;background:transparent;color:#e6edf3;font-size:12px;cursor:pointer;";
1252
- if (disabled) btn.style.opacity = "0.45";
1253
- btn.onclick = () => {
1254
- fn();
1255
- close();
1920
+ // src/layer/types.ts
1921
+ var LAYER_PRESET_VERSION = 2;
1922
+
1923
+ // src/layer/overlay-mount.ts
1924
+ var OVERLAY_LAYER_TYPES = [
1925
+ "overlay.crosshairLegend",
1926
+ "overlay.drawing"
1927
+ ];
1928
+ function isOverlayLayerType(type) {
1929
+ return OVERLAY_LAYER_TYPES.includes(type);
1930
+ }
1931
+ function syncOverlayLayersToMain(layers, pageId) {
1932
+ const main = layers.find((l) => l.type === "chart.main" && l.pageId === pageId);
1933
+ if (!main) return;
1934
+ const legend = layers.find(
1935
+ (l) => l.type === "overlay.crosshairLegend" && l.pageId === pageId
1936
+ );
1937
+ if (legend) {
1938
+ legend.frame = {
1939
+ x: main.frame.x,
1940
+ y: main.frame.y,
1941
+ w: Math.min(0.35, main.frame.w),
1942
+ h: Math.min(0.1, main.frame.h)
1256
1943
  };
1257
- menu.appendChild(btn);
1258
- };
1944
+ }
1945
+ const drawing = layers.find((l) => l.type === "overlay.drawing" && l.pageId === pageId);
1259
1946
  if (drawing) {
1260
- const locked = Boolean(drawing.meta?.locked);
1261
- add(t7("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
1262
- add(t7("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
1263
- add(
1264
- locked ? t7("drawing.ctx.unlock", "\u89E3\u9396") : t7("drawing.ctx.lock", "\u9396\u5B9A"),
1265
- handlers.onToggleLock
1266
- );
1267
- if (drawing.type === "text") {
1268
- add(t7("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
1947
+ drawing.frame = {
1948
+ x: main.frame.x,
1949
+ y: main.frame.y,
1950
+ w: main.frame.w,
1951
+ h: main.frame.h
1952
+ };
1953
+ if (drawing.zIndex <= main.zIndex) {
1954
+ drawing.zIndex = main.zIndex + 2;
1269
1955
  }
1270
- add(t7("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
1271
1956
  }
1272
- document.body.appendChild(menu);
1273
- const close = () => {
1274
- menu.remove();
1275
- document.removeEventListener("click", close);
1276
- };
1277
- setTimeout(() => document.addEventListener("click", close), 0);
1278
- return close;
1957
+ }
1958
+ function syncAllOverlayLayersToMain(layers) {
1959
+ const pageIds = [...new Set(layers.map((l) => l.pageId))];
1960
+ for (const pageId of pageIds) {
1961
+ syncOverlayLayersToMain(layers, pageId);
1962
+ }
1963
+ }
1964
+ function getDrawingOverlayVisible(controller) {
1965
+ const drawing = controller.getLayersForActivePage().find((l) => l.type === "overlay.drawing");
1966
+ return drawing?.visible ?? true;
1967
+ }
1968
+ function ensureOverlayLayers(layers, pageId) {
1969
+ const main = layers.find((l) => l.type === "chart.main" && l.pageId === pageId);
1970
+ if (!main) return layers;
1971
+ const out = [...layers];
1972
+ if (!out.some((l) => l.type === "overlay.crosshairLegend" && l.pageId === pageId)) {
1973
+ out.push({
1974
+ id: `layer-crosshairLegend-${pageId}`,
1975
+ pageId,
1976
+ type: "overlay.crosshairLegend",
1977
+ widgetKey: "crosshairLegend",
1978
+ frame: { x: main.frame.x, y: main.frame.y, w: 0.2, h: 0.08 },
1979
+ zIndex: main.zIndex + 5,
1980
+ visible: true,
1981
+ locked: false
1982
+ });
1983
+ }
1984
+ if (!out.some((l) => l.type === "overlay.drawing" && l.pageId === pageId)) {
1985
+ out.push({
1986
+ id: `layer-drawingOverlay-${pageId}`,
1987
+ pageId,
1988
+ type: "overlay.drawing",
1989
+ widgetKey: "drawingOverlay",
1990
+ frame: { ...main.frame },
1991
+ zIndex: main.zIndex + 3,
1992
+ visible: true,
1993
+ locked: false
1994
+ });
1995
+ }
1996
+ syncOverlayLayersToMain(out, pageId);
1997
+ return out;
1279
1998
  }
1280
1999
 
1281
- // src/code-snippet-panel.ts
1282
- function mountCodeSnippetPanel(parent, getCode) {
1283
- const wrap = document.createElement("details");
1284
- wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;padding:6px 12px;font-size:11px;color:#8b949e;";
1285
- const summary = document.createElement("summary");
1286
- summary.textContent = "\u5D4C\u5165\u7A0B\u5F0F\u78BC\uFF08\u6574\u5408\u65B9\uFF09";
1287
- summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;";
1288
- const pre = document.createElement("pre");
1289
- pre.style.cssText = "margin:8px 0 0;padding:10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:11px;overflow:auto;max-height:160px;white-space:pre-wrap;";
1290
- const copyBtn = document.createElement("button");
1291
- copyBtn.type = "button";
1292
- copyBtn.textContent = "\u8907\u88FD";
1293
- copyBtn.style.cssText = "margin-top:6px;padding:4px 10px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;";
1294
- copyBtn.onclick = () => {
1295
- const code = getCode();
1296
- pre.textContent = code;
1297
- void navigator.clipboard.writeText(code);
2000
+ // src/layer/chart-pane-mount.ts
2001
+ var CHART_PANE_LAYER_TYPES = [
2002
+ "chart.main",
2003
+ "chart.volume",
2004
+ "chart.indicator",
2005
+ "chart.host",
2006
+ "chart.indicatorHost"
2007
+ ];
2008
+ var DEFAULT_SYNC_TIME_SCALE_GROUP = "chart-timescale";
2009
+ function isChartPaneLayerType(type) {
2010
+ return CHART_PANE_LAYER_TYPES.includes(type);
2011
+ }
2012
+ function widgetKeyForChartPaneType(type) {
2013
+ switch (type) {
2014
+ case "chart.main":
2015
+ return "chartMain";
2016
+ case "chart.volume":
2017
+ return "chartVolume";
2018
+ case "chart.indicator":
2019
+ case "chart.indicatorHost":
2020
+ return "chartIndicator";
2021
+ case "chart.host":
2022
+ return "chartMain";
2023
+ default:
2024
+ return null;
2025
+ }
2026
+ }
2027
+ function paneIdFromWidgetKey(widgetKey) {
2028
+ switch (widgetKey) {
2029
+ case "chartMain":
2030
+ case "chartHost":
2031
+ return "main";
2032
+ case "chartVolume":
2033
+ return "volume";
2034
+ case "chartIndicator":
2035
+ case "indicatorHost":
2036
+ return "indicator";
2037
+ default:
2038
+ return null;
2039
+ }
2040
+ }
2041
+ function expandLegacyChartHostLayers(layers) {
2042
+ return splitLegacyChartHost(layers).layers;
2043
+ }
2044
+ function splitLegacyChartHost(layers) {
2045
+ const host = layers.find((l) => l.type === "chart.host");
2046
+ if (!host || layers.some((l) => l.type === "chart.main")) {
2047
+ return { layers, idRemap: /* @__PURE__ */ new Map() };
2048
+ }
2049
+ const mainRatio = 0.65;
2050
+ const main = {
2051
+ ...host,
2052
+ id: `${host.id}-main`,
2053
+ type: "chart.main",
2054
+ widgetKey: "chartMain",
2055
+ frame: {
2056
+ x: host.frame.x,
2057
+ y: host.frame.y,
2058
+ w: host.frame.w,
2059
+ h: host.frame.h * mainRatio
2060
+ },
2061
+ syncTimeScaleGroupId: host.syncTimeScaleGroupId
1298
2062
  };
1299
- wrap.ontoggle = () => {
1300
- if (wrap.open) pre.textContent = getCode();
2063
+ const volume = {
2064
+ ...host,
2065
+ id: `${host.id}-volume`,
2066
+ type: "chart.volume",
2067
+ widgetKey: "chartVolume",
2068
+ frame: {
2069
+ x: host.frame.x,
2070
+ y: host.frame.y + host.frame.h * mainRatio,
2071
+ w: host.frame.w,
2072
+ h: host.frame.h * (1 - mainRatio)
2073
+ },
2074
+ zIndex: host.zIndex + 1,
2075
+ syncTimeScaleGroupId: host.syncTimeScaleGroupId
1301
2076
  };
1302
- wrap.append(summary, pre, copyBtn);
1303
- parent.appendChild(wrap);
1304
- return wrap;
2077
+ const rest = layers.filter((l) => l.id !== host.id);
2078
+ const idRemap = /* @__PURE__ */ new Map([[host.id, [main.id, volume.id]]]);
2079
+ return { layers: [...rest, main, volume], idRemap };
2080
+ }
2081
+ function syncCrosshairLegendToMain(layers) {
2082
+ syncAllOverlayLayersToMain(layers);
2083
+ }
2084
+ function upgradeIndicatorHostType(layers) {
2085
+ return layers.map((l) => {
2086
+ if (l.type !== "chart.indicatorHost") return l;
2087
+ return {
2088
+ ...l,
2089
+ type: "chart.indicator",
2090
+ widgetKey: l.widgetKey === "indicatorHost" ? "chartIndicator" : l.widgetKey,
2091
+ syncTimeScaleGroupId: l.syncTimeScaleGroupId
2092
+ };
2093
+ });
1305
2094
  }
1306
2095
 
1307
- // src/pine-editor-panel.ts
1308
- import { compilePineLite } from "@coderyo/pine-lite";
1309
- import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
2096
+ // src/layer/normalize.ts
2097
+ var SHELL_TYPES = [
2098
+ "shell.topBar",
2099
+ "shell.leftToolbar",
2100
+ "shell.bottomToolbar",
2101
+ "shell.statusBar",
2102
+ "shell.propertiesPanel"
2103
+ ];
2104
+ var CHART_TYPES = [
2105
+ "chart.host",
2106
+ "chart.main",
2107
+ "chart.volume",
2108
+ "chart.indicator",
2109
+ "chart.indicatorHost"
2110
+ ];
2111
+ var OVERLAY_TYPES = ["overlay.crosshairLegend", "overlay.drawing"];
2112
+ function isLayerType(t12) {
2113
+ return SHELL_TYPES.includes(t12) || CHART_TYPES.includes(t12) || OVERLAY_TYPES.includes(t12) || t12 === "group";
2114
+ }
2115
+ function clamp01(n) {
2116
+ if (!Number.isFinite(n)) return 0;
2117
+ return Math.min(1, Math.max(0, n));
2118
+ }
2119
+ function clampFrame(frame) {
2120
+ const w = Math.min(1, Math.max(0.02, clamp01(frame.w)));
2121
+ const h = Math.min(1, Math.max(0.02, clamp01(frame.h)));
2122
+ const x = clamp01(frame.x);
2123
+ const y = clamp01(frame.y);
2124
+ return {
2125
+ x: Math.min(x, 1 - w),
2126
+ y: Math.min(y, 1 - h),
2127
+ w,
2128
+ h
2129
+ };
2130
+ }
2131
+ function normalizeLayoutPreset(input) {
2132
+ const pages = input.pages?.length > 0 ? input.pages.map((p) => ({
2133
+ id: String(p.id || "page-1"),
2134
+ title: String(p.title || "Page 1")
2135
+ })) : [{ id: "page-1", title: "\u5716\u8868" }];
2136
+ const pageIds = new Set(pages.map((p) => p.id));
2137
+ const defaultPageId = pages[0].id;
2138
+ const layers = [];
2139
+ const seenLayer = /* @__PURE__ */ new Set();
2140
+ for (const raw of input.layers ?? []) {
2141
+ if (!raw?.id || seenLayer.has(raw.id)) continue;
2142
+ if (!isLayerType(raw.type)) continue;
2143
+ seenLayer.add(raw.id);
2144
+ const pageId = pageIds.has(raw.pageId) ? raw.pageId : defaultPageId;
2145
+ layers.push({
2146
+ id: raw.id,
2147
+ pageId,
2148
+ type: raw.type,
2149
+ widgetKey: String(raw.widgetKey || raw.id),
2150
+ frame: clampFrame(raw.frame ?? { x: 0, y: 0, w: 1, h: 1 }),
2151
+ zIndex: Math.round(Number.isFinite(raw.zIndex) ? raw.zIndex : 0),
2152
+ visible: raw.visible !== false,
2153
+ locked: raw.locked === true,
2154
+ groupId: raw.groupId,
2155
+ syncTimeScaleGroupId: raw.syncTimeScaleGroupId
2156
+ });
2157
+ }
2158
+ const { layers: splitLayers, idRemap } = splitLegacyChartHost(layers);
2159
+ let migrated = upgradeIndicatorHostType(splitLayers);
2160
+ for (const pageId of pageIds) {
2161
+ migrated = ensureOverlayLayers(migrated, pageId);
2162
+ }
2163
+ syncAllOverlayLayersToMain(migrated);
2164
+ migrated.sort((a, b) => a.zIndex - b.zIndex);
2165
+ migrated.forEach((l, i) => {
2166
+ l.zIndex = i;
2167
+ });
2168
+ const migratedIds = new Set(migrated.map((l) => l.id));
2169
+ const groups = [];
2170
+ const seenGroup = /* @__PURE__ */ new Set();
2171
+ for (const g of input.groups ?? []) {
2172
+ if (!g?.id || seenGroup.has(g.id)) continue;
2173
+ seenGroup.add(g.id);
2174
+ const layerIds = (g.layerIds ?? []).flatMap((id) => idRemap.get(id) ?? [id]).filter((id) => migratedIds.has(id));
2175
+ if (layerIds.length === 0) continue;
2176
+ groups.push({
2177
+ id: g.id,
2178
+ name: g.name,
2179
+ layerIds
2180
+ });
2181
+ }
2182
+ const revisionRaw = Number(input.revision);
2183
+ const revision = Number.isFinite(revisionRaw) && revisionRaw >= 1 ? Math.floor(revisionRaw) : 1;
2184
+ return {
2185
+ version: LAYER_PRESET_VERSION,
2186
+ revision,
2187
+ id: String(input.id || "unnamed"),
2188
+ name: String(input.name || input.id || "Untitled"),
2189
+ author: input.author === "user" ? "user" : "integrator",
2190
+ readonly: input.readonly === true,
2191
+ forkedFrom: input.forkedFrom,
2192
+ pages,
2193
+ layers: migrated,
2194
+ groups
2195
+ };
2196
+ }
2197
+ function cloneLayoutPreset(preset) {
2198
+ return normalizeLayoutPreset({
2199
+ ...preset,
2200
+ pages: preset.pages.map((p) => ({ ...p })),
2201
+ layers: preset.layers.map((l) => ({ ...l, frame: { ...l.frame } })),
2202
+ groups: preset.groups.map((g) => ({ ...g, layerIds: [...g.layerIds] }))
2203
+ });
2204
+ }
2205
+
2206
+ // src/layer/grid-to-preset.ts
2207
+ var WIDGET_META = {
2208
+ topBar: { layerId: "layer-topBar", type: "shell.topBar", widgetKey: "topBar", z: 10 },
2209
+ leftToolbar: { layerId: "layer-leftToolbar", type: "shell.leftToolbar", widgetKey: "leftToolbar", z: 20 },
2210
+ indicatorHost: {
2211
+ layerId: "layer-chartIndicator",
2212
+ type: "chart.indicator",
2213
+ widgetKey: "chartIndicator",
2214
+ z: 45
2215
+ },
2216
+ statusBar: { layerId: "layer-statusBar", type: "shell.statusBar", widgetKey: "statusBar", z: 50 },
2217
+ propertiesPanel: {
2218
+ layerId: "layer-propertiesPanel",
2219
+ type: "shell.propertiesPanel",
2220
+ widgetKey: "propertiesPanel",
2221
+ z: 60
2222
+ },
2223
+ bottomToolbar: { layerId: "layer-bottomToolbar", type: "shell.bottomToolbar", widgetKey: "bottomToolbar", z: 70 }
2224
+ };
2225
+ var MAIN_VOLUME_RATIO = 0.65;
2226
+ function chartHostLayers(w, columns, rows, pageId) {
2227
+ const frame = {
2228
+ x: w.col / columns,
2229
+ y: w.row / rows,
2230
+ w: w.colSpan / columns,
2231
+ h: w.rowSpan / rows
2232
+ };
2233
+ const mainH = frame.h * MAIN_VOLUME_RATIO;
2234
+ return [
2235
+ {
2236
+ id: "layer-chartMain",
2237
+ pageId,
2238
+ type: "chart.main",
2239
+ widgetKey: "chartMain",
2240
+ frame: { x: frame.x, y: frame.y, w: frame.w, h: mainH },
2241
+ zIndex: 30,
2242
+ visible: true,
2243
+ locked: false
2244
+ },
2245
+ {
2246
+ id: "layer-chartVolume",
2247
+ pageId,
2248
+ type: "chart.volume",
2249
+ widgetKey: "chartVolume",
2250
+ frame: { x: frame.x, y: frame.y + mainH, w: frame.w, h: frame.h - mainH },
2251
+ zIndex: 35,
2252
+ visible: true,
2253
+ locked: false
2254
+ }
2255
+ ];
2256
+ }
2257
+ function layoutSchemaToPreset(schema = DEFAULT_LAYOUT_SCHEMA, opts = {}) {
2258
+ const columns = schema.columns || 12;
2259
+ const rows = schema.rows || 12;
2260
+ const pageId = "page-1";
2261
+ const layers = [];
2262
+ for (const w of schema.widgets) {
2263
+ if (w.id === "chartHost") {
2264
+ layers.push(...chartHostLayers(w, columns, rows, pageId));
2265
+ continue;
2266
+ }
2267
+ const meta = WIDGET_META[w.id];
2268
+ if (!meta) continue;
2269
+ layers.push({
2270
+ id: meta.layerId,
2271
+ pageId,
2272
+ type: meta.type,
2273
+ widgetKey: meta.widgetKey,
2274
+ frame: {
2275
+ x: w.col / columns,
2276
+ y: w.row / rows,
2277
+ w: w.colSpan / columns,
2278
+ h: w.rowSpan / rows
2279
+ },
2280
+ zIndex: meta.z,
2281
+ visible: true,
2282
+ locked: false
2283
+ });
2284
+ }
2285
+ const chartMain = layers.find((l) => l.widgetKey === "chartMain");
2286
+ if (chartMain) {
2287
+ layers.push({
2288
+ id: "layer-crosshairLegend",
2289
+ pageId,
2290
+ type: "overlay.crosshairLegend",
2291
+ widgetKey: "crosshairLegend",
2292
+ frame: {
2293
+ x: chartMain.frame.x,
2294
+ y: chartMain.frame.y,
2295
+ w: Math.min(0.35, chartMain.frame.w),
2296
+ h: Math.min(0.1, chartMain.frame.h)
2297
+ },
2298
+ zIndex: chartMain.zIndex + 5,
2299
+ visible: true,
2300
+ locked: false
2301
+ });
2302
+ layers.push({
2303
+ id: "layer-drawingOverlay",
2304
+ pageId,
2305
+ type: "overlay.drawing",
2306
+ widgetKey: "drawingOverlay",
2307
+ frame: { ...chartMain.frame },
2308
+ zIndex: chartMain.zIndex + 3,
2309
+ visible: true,
2310
+ locked: false
2311
+ });
2312
+ }
2313
+ return normalizeLayoutPreset({
2314
+ version: LAYER_PRESET_VERSION,
2315
+ id: opts.id ?? "vendor-default",
2316
+ name: opts.name ?? "TradView \u9810\u8A2D",
2317
+ author: "integrator",
2318
+ readonly: false,
2319
+ pages: [{ id: pageId, title: "\u5716\u8868" }],
2320
+ layers,
2321
+ groups: []
2322
+ });
2323
+ }
2324
+
2325
+ // src/layer/default-presets.ts
2326
+ var VENDOR_DEFAULT_PRESET = layoutSchemaToPreset(void 0, {
2327
+ id: "vendor-default",
2328
+ name: "TradView \u9810\u8A2D"
2329
+ });
2330
+ var VENDOR_COMPACT_PRESET = normalizeLayoutPreset({
2331
+ ...layoutSchemaToPreset(void 0, { id: "vendor-compact", name: "\u7CBE\u7C21\u5716\u8868" }),
2332
+ layers: layoutSchemaToPreset(void 0).layers.map((l) => {
2333
+ if (l.widgetKey === "propertiesPanel") {
2334
+ return { ...l, visible: false, frame: { ...l.frame, w: 1e-3, h: 1e-3 } };
2335
+ }
2336
+ if (l.widgetKey === "leftToolbar") {
2337
+ return { ...l, frame: { x: 0, y: 0.06, w: 0.04, h: 0.88 } };
2338
+ }
2339
+ if (l.widgetKey === "chartMain") {
2340
+ return { ...l, frame: { x: 0.04, y: 0.06, w: 0.96, h: 0.4 } };
2341
+ }
2342
+ if (l.widgetKey === "chartVolume") {
2343
+ return { ...l, frame: { x: 0.04, y: 0.46, w: 0.96, h: 0.16 } };
2344
+ }
2345
+ if (l.widgetKey === "chartIndicator") {
2346
+ return { ...l, frame: { x: 0.04, y: 0.68, w: 0.96, h: 0.22 } };
2347
+ }
2348
+ return l;
2349
+ })
2350
+ });
2351
+ var VENDOR_DUAL_SYNC_PRESET = normalizeLayoutPreset({
2352
+ ...layoutSchemaToPreset(void 0, { id: "vendor-dual-sync", name: "\u96D9\u6642\u9593\u8EF8\u5206\u7D44" }),
2353
+ layers: layoutSchemaToPreset(void 0).layers.map((l) => {
2354
+ if (l.type === "chart.main" || l.type === "chart.volume") {
2355
+ return { ...l, syncTimeScaleGroupId: "prices" };
2356
+ }
2357
+ if (l.type === "chart.indicator") {
2358
+ return { ...l, syncTimeScaleGroupId: "osc" };
2359
+ }
2360
+ return l;
2361
+ })
2362
+ });
2363
+ var BUILTIN_PRESETS = [
2364
+ VENDOR_DEFAULT_PRESET,
2365
+ VENDOR_COMPACT_PRESET,
2366
+ VENDOR_DUAL_SYNC_PRESET
2367
+ ];
2368
+ function getBuiltinPreset(id) {
2369
+ return BUILTIN_PRESETS.find((p) => p.id === id);
2370
+ }
2371
+
2372
+ // src/layer/preset-store.ts
2373
+ var INDEX_KEY = "tradview:preset:v2:index";
2374
+ function presetStorageKey(id) {
2375
+ return `tradview:preset:v2:${id}`;
2376
+ }
2377
+ function readIndex() {
2378
+ try {
2379
+ const raw = localStorage.getItem(INDEX_KEY);
2380
+ if (!raw) return [];
2381
+ const arr = JSON.parse(raw);
2382
+ return Array.isArray(arr) ? arr.filter((id) => typeof id === "string") : [];
2383
+ } catch {
2384
+ return [];
2385
+ }
2386
+ }
2387
+ function writeIndex(ids) {
2388
+ try {
2389
+ localStorage.setItem(INDEX_KEY, JSON.stringify([...new Set(ids)]));
2390
+ } catch {
2391
+ }
2392
+ }
2393
+ function listPresets() {
2394
+ const out = BUILTIN_PRESETS.map((p) => ({
2395
+ id: p.id,
2396
+ name: p.name,
2397
+ author: p.author,
2398
+ readonly: p.readonly,
2399
+ builtin: true
2400
+ }));
2401
+ for (const id of readIndex()) {
2402
+ if (BUILTIN_PRESETS.some((p) => p.id === id)) continue;
2403
+ const loaded = loadPreset(id);
2404
+ if (loaded) {
2405
+ out.push({
2406
+ id: loaded.id,
2407
+ name: loaded.name,
2408
+ author: loaded.author,
2409
+ readonly: loaded.readonly,
2410
+ builtin: false
2411
+ });
2412
+ }
2413
+ }
2414
+ return out;
2415
+ }
2416
+ function loadPreset(id) {
2417
+ const builtin = getBuiltinPreset(id);
2418
+ if (builtin) return cloneLayoutPreset(builtin);
2419
+ try {
2420
+ const raw = localStorage.getItem(presetStorageKey(id));
2421
+ if (!raw) return null;
2422
+ return normalizeLayoutPreset(JSON.parse(raw));
2423
+ } catch {
2424
+ return null;
2425
+ }
2426
+ }
2427
+ function savePreset(preset) {
2428
+ const normalized = normalizeLayoutPreset(preset);
2429
+ if (getBuiltinPreset(normalized.id)) {
2430
+ throw new Error(`Cannot overwrite builtin preset: ${normalized.id}`);
2431
+ }
2432
+ try {
2433
+ localStorage.setItem(presetStorageKey(normalized.id), JSON.stringify(normalized));
2434
+ const ids = readIndex();
2435
+ if (!ids.includes(normalized.id)) {
2436
+ writeIndex([...ids, normalized.id]);
2437
+ }
2438
+ } catch {
2439
+ }
2440
+ }
2441
+ function deleteUserPreset(id) {
2442
+ if (getBuiltinPreset(id)) return false;
2443
+ try {
2444
+ localStorage.removeItem(presetStorageKey(id));
2445
+ writeIndex(readIndex().filter((x) => x !== id));
2446
+ return true;
2447
+ } catch {
2448
+ return false;
2449
+ }
2450
+ }
2451
+ function forkPreset(sourceId, newId, newName) {
2452
+ const src = loadPreset(sourceId);
2453
+ if (!src) return null;
2454
+ const forked = normalizeLayoutPreset({
2455
+ ...cloneLayoutPreset(src),
2456
+ id: newId,
2457
+ name: newName,
2458
+ author: "user",
2459
+ readonly: false,
2460
+ forkedFrom: sourceId
2461
+ });
2462
+ savePreset(forked);
2463
+ return forked;
2464
+ }
2465
+ function resolvePreset(id) {
2466
+ if (id && typeof id === "object") return normalizeLayoutPreset(id);
2467
+ if (typeof id === "string") {
2468
+ const loaded = loadPreset(id);
2469
+ if (loaded) return loaded;
2470
+ }
2471
+ return cloneLayoutPreset(getBuiltinPreset("vendor-default"));
2472
+ }
2473
+
2474
+ // src/layer/group-utils.ts
2475
+ function getLayersBoundingBox(layers) {
2476
+ if (layers.length === 0) return null;
2477
+ let minX = 1;
2478
+ let minY = 1;
2479
+ let maxX = 0;
2480
+ let maxY = 0;
2481
+ for (const l of layers) {
2482
+ const f = l.frame;
2483
+ minX = Math.min(minX, f.x);
2484
+ minY = Math.min(minY, f.y);
2485
+ maxX = Math.max(maxX, f.x + f.w);
2486
+ maxY = Math.max(maxY, f.y + f.h);
2487
+ }
2488
+ const w = maxX - minX;
2489
+ const h = maxY - minY;
2490
+ if (w <= 0 || h <= 0) return null;
2491
+ return { x: minX, y: minY, w, h };
2492
+ }
2493
+ function moveLayerFrames(frames, dx, dy) {
2494
+ return frames.map(
2495
+ (f) => clampFrame({
2496
+ ...f,
2497
+ x: f.x + dx,
2498
+ y: f.y + dy
2499
+ })
2500
+ );
2501
+ }
2502
+ function resizeGroupFrames(members, oldBbox, newBbox) {
2503
+ const ow = Math.max(oldBbox.w, 0.02);
2504
+ const oh = Math.max(oldBbox.h, 0.02);
2505
+ const nw = Math.max(newBbox.w, 0.02);
2506
+ const nh = Math.max(newBbox.h, 0.02);
2507
+ return members.map((m) => {
2508
+ const f = m.frame;
2509
+ const relX = (f.x - oldBbox.x) / ow;
2510
+ const relY = (f.y - oldBbox.y) / oh;
2511
+ const relW = f.w / ow;
2512
+ const relH = f.h / oh;
2513
+ return clampFrame({
2514
+ x: newBbox.x + relX * nw,
2515
+ y: newBbox.y + relY * nh,
2516
+ w: relW * nw,
2517
+ h: relH * nh
2518
+ });
2519
+ });
2520
+ }
2521
+ function clampBBox(frame) {
2522
+ return clampFrame(frame);
2523
+ }
2524
+
2525
+ // src/layer/layer-controller.ts
2526
+ var LayerController = class {
2527
+ preset;
2528
+ listeners = /* @__PURE__ */ new Set();
2529
+ focusListeners = /* @__PURE__ */ new Set();
2530
+ interactionDepth = 0;
2531
+ pageIdSeq = 0;
2532
+ activePageId;
2533
+ /** P2: active chart pane layer (click-to-focus). */
2534
+ focusedPaneLayerId = null;
2535
+ /** Monotonic preset revision (Bridge `host.layer.setPreset`). */
2536
+ presetRevision;
2537
+ constructor(initial) {
2538
+ this.preset = cloneLayoutPreset(initial);
2539
+ this.presetRevision = this.preset.revision ?? 1;
2540
+ this.activePageId = this.preset.pages[0]?.id ?? "page-1";
2541
+ }
2542
+ getPreset() {
2543
+ return cloneLayoutPreset(this.preset);
2544
+ }
2545
+ /** True while pointer drag/resize/marquee is active — blocks setPreset. */
2546
+ isInteracting() {
2547
+ return this.interactionDepth > 0;
2548
+ }
2549
+ beginInteraction() {
2550
+ this.interactionDepth++;
2551
+ }
2552
+ endInteraction() {
2553
+ this.interactionDepth = Math.max(0, this.interactionDepth - 1);
2554
+ }
2555
+ setPreset(next) {
2556
+ if (this.isInteracting()) return false;
2557
+ this.preset = cloneLayoutPreset(next);
2558
+ this.presetRevision = this.preset.revision ?? 1;
2559
+ if (!this.preset.pages.some((p) => p.id === this.activePageId)) {
2560
+ this.activePageId = this.preset.pages[0]?.id ?? "page-1";
2561
+ }
2562
+ this.emit();
2563
+ return true;
2564
+ }
2565
+ getLayersForActivePage() {
2566
+ return this.preset.layers.filter((l) => l.pageId === this.activePageId).sort((a, b) => a.zIndex - b.zIndex);
2567
+ }
2568
+ getFocusedPaneLayerId() {
2569
+ return this.focusedPaneLayerId;
2570
+ }
2571
+ getFocusedPaneId() {
2572
+ const layer = this.focusedPaneLayerId ? this.getLayer(this.focusedPaneLayerId) : void 0;
2573
+ return layer ? paneIdFromWidgetKey(layer.widgetKey) : null;
2574
+ }
2575
+ /** Raise z-index among chart panes only; does not rebuild compositor DOM. */
2576
+ focusChartPane(layerId) {
2577
+ const layer = this.getLayer(layerId);
2578
+ if (!layer || layer.pageId !== this.activePageId || !isChartPaneLayerType(layer.type)) {
2579
+ return;
2580
+ }
2581
+ if (this.focusedPaneLayerId === layerId) return;
2582
+ this.focusedPaneLayerId = layerId;
2583
+ const chartLayers = this.getLayersForActivePage().filter(
2584
+ (l) => isChartPaneLayerType(l.type)
2585
+ );
2586
+ const maxZ = Math.max(...chartLayers.map((l) => l.zIndex), layer.zIndex);
2587
+ const FOCUS_Z_CAP = 500;
2588
+ if (layer.zIndex < maxZ) layer.zIndex = Math.min(maxZ + 1, FOCUS_Z_CAP);
2589
+ this.notifyFocus();
2590
+ }
2591
+ subscribeFocus(listener) {
2592
+ this.focusListeners.add(listener);
2593
+ return () => this.focusListeners.delete(listener);
2594
+ }
2595
+ notifyFocus() {
2596
+ for (const fn of this.focusListeners) fn();
2597
+ }
2598
+ afterChartMainFrameChange(pageIds) {
2599
+ const ids = pageIds ? [...new Set(pageIds)] : [...new Set(this.preset.layers.map((l) => l.pageId))];
2600
+ for (const pageId of ids) {
2601
+ syncOverlayLayersToMain(this.preset.layers, pageId);
2602
+ }
2603
+ }
2604
+ setActivePage(pageId) {
2605
+ if (!this.preset.pages.some((p) => p.id === pageId)) return false;
2606
+ if (this.activePageId === pageId) return false;
2607
+ this.activePageId = pageId;
2608
+ this.focusedPaneLayerId = null;
2609
+ this.emit();
2610
+ this.notifyFocus();
2611
+ return true;
2612
+ }
2613
+ addPage(title) {
2614
+ const suffix = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${++this.pageIdSeq}`;
2615
+ const id = `page-${suffix}`;
2616
+ const pageTitle = title?.trim() || `Page ${this.preset.pages.length + 1}`;
2617
+ this.preset.pages.push({ id, title: pageTitle });
2618
+ const templatePageId = this.activePageId;
2619
+ const templateLayers = this.preset.layers.filter((l) => l.pageId === templatePageId);
2620
+ const maxZ = Math.max(-1, ...this.preset.layers.map((l) => l.zIndex));
2621
+ let z = maxZ + 1;
2622
+ for (const tpl of templateLayers) {
2623
+ this.preset.layers.push({
2624
+ ...tpl,
2625
+ id: `${tpl.id}-${id}`,
2626
+ pageId: id,
2627
+ frame: { ...tpl.frame },
2628
+ zIndex: z++,
2629
+ groupId: void 0
2630
+ });
2631
+ }
2632
+ this.activePageId = id;
2633
+ this.focusedPaneLayerId = null;
2634
+ this.emit();
2635
+ return id;
2636
+ }
2637
+ renamePage(pageId, title) {
2638
+ const page = this.preset.pages.find((p) => p.id === pageId);
2639
+ if (!page) return false;
2640
+ const trimmed = title.trim();
2641
+ if (!trimmed) return false;
2642
+ page.title = trimmed;
2643
+ this.emit();
2644
+ return true;
2645
+ }
2646
+ removePage(pageId) {
2647
+ if (this.preset.pages.length <= 1) return false;
2648
+ const idx = this.preset.pages.findIndex((p) => p.id === pageId);
2649
+ if (idx < 0) return false;
2650
+ this.preset.pages.splice(idx, 1);
2651
+ this.preset.layers = this.preset.layers.filter((l) => l.pageId !== pageId);
2652
+ for (const g of [...this.preset.groups]) {
2653
+ g.layerIds = g.layerIds.filter((lid) => this.getLayer(lid)?.pageId !== pageId);
2654
+ if (g.layerIds.length === 0) {
2655
+ this.preset.groups = this.preset.groups.filter((x) => x.id !== g.id);
2656
+ }
2657
+ }
2658
+ if (this.activePageId === pageId) {
2659
+ const nextPage = this.preset.pages[idx] ?? this.preset.pages[idx - 1];
2660
+ this.activePageId = nextPage?.id ?? this.preset.pages[0].id;
2661
+ this.focusedPaneLayerId = null;
2662
+ }
2663
+ this.emit();
2664
+ return true;
2665
+ }
2666
+ getLayer(layerId) {
2667
+ return this.preset.layers.find((l) => l.id === layerId);
2668
+ }
2669
+ /**
2670
+ * @public Set time-scale sync group for a chart pane layer (`''` clears → independent).
2671
+ */
2672
+ setLayerSyncGroup(layerId, groupId) {
2673
+ const layer = this.getLayer(layerId);
2674
+ if (!layer || !isChartPaneLayerType(layer.type)) return false;
2675
+ const trimmed = groupId == null ? "" : String(groupId).trim();
2676
+ layer.syncTimeScaleGroupId = trimmed.length > 0 ? trimmed : void 0;
2677
+ this.emit();
2678
+ return true;
2679
+ }
2680
+ getGroup(groupId) {
2681
+ return this.preset.groups.find((g) => g.id === groupId);
2682
+ }
2683
+ /** Layer ids that move/resize together (group members or singleton). */
2684
+ getTransformLayerIds(layerId) {
2685
+ const layer = this.getLayer(layerId);
2686
+ if (!layer?.groupId) return [layerId];
2687
+ const group = this.getGroup(layer.groupId);
2688
+ if (!group || group.layerIds.length === 0) return [layerId];
2689
+ return group.layerIds.filter((id) => {
2690
+ const l = this.getLayer(id);
2691
+ return l && l.pageId === this.activePageId && !l.locked;
2692
+ });
2693
+ }
2694
+ getGroupMemberLayers(groupId) {
2695
+ const group = this.getGroup(groupId);
2696
+ if (!group) return [];
2697
+ return group.layerIds.map((id) => this.getLayer(id)).filter((l) => !!l && l.pageId === this.activePageId);
2698
+ }
2699
+ setLayerFrame(layerId, frame) {
2700
+ const ids = this.getTransformLayerIds(layerId);
2701
+ if (ids.length === 1) {
2702
+ const layer = this.getLayer(layerId);
2703
+ if (!layer) return;
2704
+ layer.frame = clampFrame(frame);
2705
+ if (layer.type === "overlay.drawing") {
2706
+ const main = this.preset.layers.find(
2707
+ (l) => l.type === "chart.main" && l.pageId === layer.pageId
2708
+ );
2709
+ if (main) main.frame = clampFrame(frame);
2710
+ }
2711
+ if (layer.type === "chart.main" || layer.type === "overlay.drawing") {
2712
+ this.afterChartMainFrameChange([layer.pageId]);
2713
+ }
2714
+ this.emit();
2715
+ return;
2716
+ }
2717
+ const members = ids.map((id) => this.getLayer(id)).filter(Boolean);
2718
+ const oldBbox = getLayersBoundingBox(members);
2719
+ if (!oldBbox) return;
2720
+ const target = frame;
2721
+ const newFrames = resizeGroupFrames(members, oldBbox, target);
2722
+ members.forEach((m, i) => {
2723
+ m.frame = newFrames[i];
2724
+ });
2725
+ if (members.some((m) => m.type === "chart.main" || m.type === "overlay.drawing")) {
2726
+ this.afterChartMainFrameChange(members.map((m) => m.pageId));
2727
+ }
2728
+ this.emit();
2729
+ }
2730
+ expandTransformIds(layerIds) {
2731
+ const expanded = /* @__PURE__ */ new Set();
2732
+ for (const id of layerIds) {
2733
+ for (const tid of this.getTransformLayerIds(id)) expanded.add(tid);
2734
+ }
2735
+ return [...expanded];
2736
+ }
2737
+ moveLayers(layerIds, dx, dy) {
2738
+ const unique = this.expandTransformIds(layerIds);
2739
+ const layers = unique.map((id) => this.getLayer(id)).filter(Boolean);
2740
+ if (layers.length === 0) return;
2741
+ const frames = moveLayerFrames(
2742
+ layers.map((l) => l.frame),
2743
+ dx,
2744
+ dy
2745
+ );
2746
+ layers.forEach((l, i) => {
2747
+ l.frame = frames[i];
2748
+ });
2749
+ if (layers.some((l) => l.type === "chart.main" || l.type === "overlay.drawing")) {
2750
+ this.afterChartMainFrameChange(layers.map((l) => l.pageId));
2751
+ }
2752
+ this.emit();
2753
+ }
2754
+ resizeLayersFromBbox(layerIds, newBbox) {
2755
+ const unique = this.expandTransformIds(layerIds);
2756
+ const members = unique.map((id) => this.getLayer(id)).filter((l) => !!l);
2757
+ if (members.length === 0) return;
2758
+ if (members.length === 1) {
2759
+ members[0].frame = clampFrame(newBbox);
2760
+ if (members[0].type === "chart.main" || members[0].type === "overlay.drawing") {
2761
+ this.afterChartMainFrameChange([members[0].pageId]);
2762
+ }
2763
+ this.emit();
2764
+ return;
2765
+ }
2766
+ const oldBbox = getLayersBoundingBox(members);
2767
+ if (!oldBbox) return;
2768
+ const newFrames = resizeGroupFrames(members, oldBbox, newBbox);
2769
+ members.forEach((m, i) => {
2770
+ m.frame = newFrames[i];
2771
+ });
2772
+ if (members.some((m) => m.type === "chart.main" || m.type === "overlay.drawing")) {
2773
+ this.afterChartMainFrameChange(members.map((m) => m.pageId));
2774
+ }
2775
+ this.emit();
2776
+ }
2777
+ setLayerVisible(layerId, visible) {
2778
+ const ids = this.syncGroupLayerIds(layerId);
2779
+ for (const id of ids) {
2780
+ const layer = this.getLayer(id);
2781
+ if (layer) layer.visible = visible;
2782
+ }
2783
+ this.emit();
2784
+ }
2785
+ setLayerLocked(layerId, locked) {
2786
+ const ids = this.syncGroupLayerIds(layerId);
2787
+ for (const id of ids) {
2788
+ const layer = this.getLayer(id);
2789
+ if (layer) layer.locked = locked;
2790
+ }
2791
+ this.emit();
2792
+ }
2793
+ syncGroupLayerIds(layerId) {
2794
+ const layer = this.getLayer(layerId);
2795
+ if (!layer?.groupId) return [layerId];
2796
+ const group = this.getGroup(layer.groupId);
2797
+ if (!group?.layerIds.length) return [layerId];
2798
+ const onPage = group.layerIds.filter((id) => {
2799
+ const l = this.getLayer(id);
2800
+ return l && l.pageId === this.activePageId;
2801
+ });
2802
+ return onPage.length > 0 ? onPage : [layerId];
2803
+ }
2804
+ bumpZIndex(layerId, delta) {
2805
+ const ordered = this.getLayersForActivePage();
2806
+ const idx = ordered.findIndex((l) => l.id === layerId);
2807
+ if (idx < 0) return;
2808
+ const swap = idx + delta;
2809
+ if (swap < 0 || swap >= ordered.length) return;
2810
+ const a = ordered[idx];
2811
+ const b = ordered[swap];
2812
+ const tz = a.zIndex;
2813
+ a.zIndex = b.zIndex;
2814
+ b.zIndex = tz;
2815
+ this.preset.layers.sort((x, y) => x.zIndex - y.zIndex);
2816
+ this.preset.layers.forEach((l, i) => {
2817
+ l.zIndex = i;
2818
+ });
2819
+ this.emit();
2820
+ }
2821
+ reorderLayers(orderedIds) {
2822
+ const pageLayers = this.preset.layers.filter((l) => l.pageId === this.activePageId);
2823
+ const byId = new Map(pageLayers.map((l) => [l.id, l]));
2824
+ let z = 0;
2825
+ for (const id of orderedIds) {
2826
+ const layer = byId.get(id);
2827
+ if (layer) layer.zIndex = z++;
2828
+ }
2829
+ for (const layer of pageLayers) {
2830
+ if (!orderedIds.includes(layer.id)) layer.zIndex = z++;
2831
+ }
2832
+ this.preset.layers.sort((a, b) => a.zIndex - b.zIndex);
2833
+ this.emit();
2834
+ }
2835
+ createBindGroup(layerIds, name) {
2836
+ const unique = [...new Set(layerIds)].filter((id) => {
2837
+ const l = this.getLayer(id);
2838
+ return l && l.pageId === this.activePageId;
2839
+ });
2840
+ if (unique.length < 2) return null;
2841
+ for (const g of this.preset.groups) {
2842
+ for (const id of unique) {
2843
+ const idx = g.layerIds.indexOf(id);
2844
+ if (idx >= 0) g.layerIds.splice(idx, 1);
2845
+ }
2846
+ if (g.layerIds.length === 0) {
2847
+ this.preset.groups = this.preset.groups.filter((x) => x.id !== g.id);
2848
+ }
2849
+ }
2850
+ for (const id of unique) {
2851
+ const layer = this.getLayer(id);
2852
+ if (layer) delete layer.groupId;
2853
+ }
2854
+ const groupId = `group-${Date.now()}`;
2855
+ const group = { id: groupId, name, layerIds: unique };
2856
+ this.preset.groups.push(group);
2857
+ for (const id of unique) {
2858
+ const layer = this.getLayer(id);
2859
+ if (layer) layer.groupId = groupId;
2860
+ }
2861
+ this.emit();
2862
+ return groupId;
2863
+ }
2864
+ dissolveGroup(groupId) {
2865
+ const group = this.getGroup(groupId);
2866
+ if (!group) return;
2867
+ for (const id of group.layerIds) {
2868
+ const layer = this.getLayer(id);
2869
+ if (layer?.groupId === groupId) delete layer.groupId;
2870
+ }
2871
+ this.preset.groups = this.preset.groups.filter((g) => g.id !== groupId);
2872
+ this.emit();
2873
+ }
2874
+ removeLayerFromGroup(layerId) {
2875
+ const layer = this.getLayer(layerId);
2876
+ if (!layer?.groupId) return;
2877
+ const groupId = layer.groupId;
2878
+ delete layer.groupId;
2879
+ const group = this.getGroup(groupId);
2880
+ if (!group) return;
2881
+ group.layerIds = group.layerIds.filter((id) => id !== layerId);
2882
+ if (group.layerIds.length === 0) {
2883
+ this.preset.groups = this.preset.groups.filter((g) => g.id !== groupId);
2884
+ }
2885
+ this.emit();
2886
+ }
2887
+ subscribe(listener) {
2888
+ this.listeners.add(listener);
2889
+ return () => this.listeners.delete(listener);
2890
+ }
2891
+ emit() {
2892
+ for (const fn of this.listeners) fn();
2893
+ }
2894
+ };
2895
+
2896
+ // src/layer/layer-editor.ts
2897
+ var HANDLE_CURSORS = {
2898
+ nw: "nwse-resize",
2899
+ ne: "nesw-resize",
2900
+ sw: "nesw-resize",
2901
+ se: "nwse-resize"
2902
+ };
2903
+ var EPS = 1e-6;
2904
+ function framesEqual(a, b, eps = EPS) {
2905
+ return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps && Math.abs(a.w - b.w) < eps && Math.abs(a.h - b.h) < eps;
2906
+ }
2907
+ function frameIntersects(a, b) {
2908
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
2909
+ }
2910
+ function setWrapContentPassthrough(wrap, passthrough) {
2911
+ for (const child of wrap.children) {
2912
+ if (child.classList.contains("tv-layer-handle")) continue;
2913
+ child.style.pointerEvents = passthrough ? "none" : "";
2914
+ }
2915
+ }
2916
+ function clearWrapContentPassthrough(wrap) {
2917
+ for (const child of wrap.children) {
2918
+ if (child.classList.contains("tv-layer-handle")) continue;
2919
+ child.style.pointerEvents = "";
2920
+ }
2921
+ }
2922
+ function attachLayerEditor(compositorRoot, opts) {
2923
+ const { controller, onPresetChange, onMarqueeSelect } = opts;
2924
+ let enabled = false;
2925
+ let abort = null;
2926
+ let captureEl = null;
2927
+ let capturePointerId = null;
2928
+ const releaseCapture = () => {
2929
+ if (captureEl && capturePointerId !== null) {
2930
+ try {
2931
+ captureEl.releasePointerCapture(capturePointerId);
2932
+ } catch {
2933
+ }
2934
+ }
2935
+ if (capturePointerId !== null && controller.isInteracting()) {
2936
+ controller.endInteraction();
2937
+ }
2938
+ captureEl = null;
2939
+ capturePointerId = null;
2940
+ };
2941
+ const setCapture = (el, pointerId) => {
2942
+ releaseCapture();
2943
+ captureEl = el;
2944
+ capturePointerId = pointerId;
2945
+ controller.beginInteraction();
2946
+ try {
2947
+ el.setPointerCapture(pointerId);
2948
+ } catch {
2949
+ controller.endInteraction();
2950
+ captureEl = null;
2951
+ capturePointerId = null;
2952
+ }
2953
+ };
2954
+ const normDelta = (dxPx, dyPx) => {
2955
+ const w = compositorRoot.clientWidth || 1;
2956
+ const h = compositorRoot.clientHeight || 1;
2957
+ return { dx: dxPx / w, dy: dyPx / h };
2958
+ };
2959
+ const applyWrapStyle = (wrap, frame) => {
2960
+ wrap.style.left = `${frame.x * 100}%`;
2961
+ wrap.style.top = `${frame.y * 100}%`;
2962
+ wrap.style.width = `${frame.w * 100}%`;
2963
+ wrap.style.height = `${frame.h * 100}%`;
2964
+ };
2965
+ const decorateWrap = (wrap, layerId) => {
2966
+ wrap.querySelectorAll(".tv-layer-handle").forEach((el) => el.remove());
2967
+ const layer = controller.getLayer(layerId);
2968
+ if (!layer || layer.locked) {
2969
+ wrap.classList.remove("tv-layer-editable");
2970
+ wrap.style.cursor = "";
2971
+ wrap.style.outline = "";
2972
+ clearWrapContentPassthrough(wrap);
2973
+ return;
2974
+ }
2975
+ wrap.classList.add("tv-layer-editable");
2976
+ wrap.style.cursor = "move";
2977
+ setWrapContentPassthrough(wrap, true);
2978
+ if (wrap.dataset.focused === "1") {
2979
+ wrap.style.outline = "2px solid #58a6ff";
2980
+ wrap.style.outlineOffset = "-2px";
2981
+ } else {
2982
+ wrap.style.outline = "1px dashed #58a6ff88";
2983
+ wrap.style.outlineOffset = "";
2984
+ }
2985
+ const handles = ["nw", "ne", "sw", "se"];
2986
+ for (const h of handles) {
2987
+ const handle = document.createElement("div");
2988
+ handle.className = `tv-layer-handle tv-layer-handle-${h}`;
2989
+ handle.dataset.handle = h;
2990
+ handle.style.cssText = [
2991
+ "position:absolute",
2992
+ "width:8px",
2993
+ "height:8px",
2994
+ "background:#58a6ff",
2995
+ "border:1px solid #0d1117",
2996
+ "border-radius:2px",
2997
+ "z-index:2",
2998
+ `cursor:${HANDLE_CURSORS[h]}`,
2999
+ h.includes("n") ? "top:-4px" : "bottom:-4px",
3000
+ h.includes("w") ? "left:-4px" : "right:-4px"
3001
+ ].join(";");
3002
+ wrap.appendChild(handle);
3003
+ }
3004
+ };
3005
+ const commitChange = () => {
3006
+ onPresetChange?.();
3007
+ };
3008
+ const computeResizeBbox = (startBbox, corner, dx, dy) => {
3009
+ const newBbox = { ...startBbox };
3010
+ if (corner.includes("e")) newBbox.w = startBbox.w + dx;
3011
+ if (corner.includes("w")) {
3012
+ newBbox.x = startBbox.x + dx;
3013
+ newBbox.w = startBbox.w - dx;
3014
+ }
3015
+ if (corner.includes("s")) newBbox.h = startBbox.h + dy;
3016
+ if (corner.includes("n")) {
3017
+ newBbox.y = startBbox.y + dy;
3018
+ newBbox.h = startBbox.h - dy;
3019
+ }
3020
+ return clampFrame(newBbox);
3021
+ };
3022
+ const bindMarquee = (signal) => {
3023
+ compositorRoot.addEventListener(
3024
+ "pointerdown",
3025
+ (e) => {
3026
+ if (!e.shiftKey) return;
3027
+ if (e.target.closest(".tv-layer-wrap")) return;
3028
+ e.preventDefault();
3029
+ const rootRect = compositorRoot.getBoundingClientRect();
3030
+ const startX = e.clientX;
3031
+ const startY = e.clientY;
3032
+ const marquee = document.createElement("div");
3033
+ marquee.className = "tv-layer-marquee";
3034
+ marquee.style.cssText = "position:absolute;border:1px dashed #58a6ff;background:#58a6ff22;pointer-events:none;z-index:9999;";
3035
+ compositorRoot.appendChild(marquee);
3036
+ setCapture(compositorRoot, e.pointerId);
3037
+ const updateMarquee = (cx, cy) => {
3038
+ const x1 = Math.min(startX, cx) - rootRect.left;
3039
+ const y1 = Math.min(startY, cy) - rootRect.top;
3040
+ const x2 = Math.max(startX, cx) - rootRect.left;
3041
+ const y2 = Math.max(startY, cy) - rootRect.top;
3042
+ marquee.style.left = `${x1}px`;
3043
+ marquee.style.top = `${y1}px`;
3044
+ marquee.style.width = `${x2 - x1}px`;
3045
+ marquee.style.height = `${y2 - y1}px`;
3046
+ };
3047
+ const onMove = (ev) => updateMarquee(ev.clientX, ev.clientY);
3048
+ const onUp = (ev) => {
3049
+ releaseCapture();
3050
+ compositorRoot.removeEventListener("pointermove", onMove);
3051
+ compositorRoot.removeEventListener("pointerup", onUp);
3052
+ compositorRoot.removeEventListener("pointercancel", onUp);
3053
+ const w = compositorRoot.clientWidth || 1;
3054
+ const h = compositorRoot.clientHeight || 1;
3055
+ const sel = clampFrame({
3056
+ x: (Math.min(startX, ev.clientX) - rootRect.left) / w,
3057
+ y: (Math.min(startY, ev.clientY) - rootRect.top) / h,
3058
+ w: Math.abs(ev.clientX - startX) / w,
3059
+ h: Math.abs(ev.clientY - startY) / h
3060
+ });
3061
+ marquee.remove();
3062
+ if (sel.w < EPS && sel.h < EPS) return;
3063
+ const hits = controller.getLayersForActivePage().filter((l) => frameIntersects(l.frame, sel)).map((l) => l.id);
3064
+ if (hits.length > 0) onMarqueeSelect?.(hits);
3065
+ };
3066
+ updateMarquee(startX, startY);
3067
+ compositorRoot.addEventListener("pointermove", onMove, { signal });
3068
+ compositorRoot.addEventListener("pointerup", onUp, { signal });
3069
+ compositorRoot.addEventListener("pointercancel", onUp, { signal });
3070
+ },
3071
+ { signal }
3072
+ );
3073
+ };
3074
+ const bindWrap = (wrap, signal) => {
3075
+ const layerId = wrap.dataset.layerId;
3076
+ if (!layerId) return;
3077
+ const layer = controller.getLayer(layerId);
3078
+ if (!layer || layer.locked) return;
3079
+ const transformIds = () => controller.getTransformLayerIds(layerId);
3080
+ const onPointerDownMove = (e) => {
3081
+ if (e.target.classList.contains("tv-layer-handle")) return;
3082
+ e.preventDefault();
3083
+ e.stopPropagation();
3084
+ const ids = transformIds();
3085
+ const members = ids.map((id) => controller.getLayer(id)).filter(Boolean);
3086
+ const startFrames = members.map((m) => ({ ...m.frame }));
3087
+ const startX = e.clientX;
3088
+ const startY = e.clientY;
3089
+ setCapture(wrap, e.pointerId);
3090
+ wrap.classList.add("tv-layer-dragging");
3091
+ const onMove = (ev) => {
3092
+ const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
3093
+ const moved = startFrames.map(
3094
+ (f) => clampFrame({ ...f, x: f.x + dx, y: f.y + dy })
3095
+ );
3096
+ compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((el) => {
3097
+ const w = el;
3098
+ const lid = w.dataset.layerId;
3099
+ if (!lid || !ids.includes(lid)) return;
3100
+ const idx = ids.indexOf(lid);
3101
+ if (idx >= 0) applyWrapStyle(w, moved[idx]);
3102
+ });
3103
+ };
3104
+ const onUp = (ev) => {
3105
+ releaseCapture();
3106
+ wrap.classList.remove("tv-layer-dragging");
3107
+ wrap.removeEventListener("pointermove", onMove);
3108
+ wrap.removeEventListener("pointerup", onUp);
3109
+ wrap.removeEventListener("pointercancel", onUp);
3110
+ const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
3111
+ if (Math.abs(dx) > EPS || Math.abs(dy) > EPS) {
3112
+ controller.moveLayers(ids, dx, dy);
3113
+ commitChange();
3114
+ }
3115
+ };
3116
+ wrap.addEventListener("pointermove", onMove, { signal });
3117
+ wrap.addEventListener("pointerup", onUp, { signal });
3118
+ wrap.addEventListener("pointercancel", onUp, { signal });
3119
+ };
3120
+ wrap.addEventListener("pointerdown", onPointerDownMove, { signal });
3121
+ wrap.querySelectorAll(".tv-layer-handle").forEach((handleEl) => {
3122
+ const handle = handleEl;
3123
+ const corner = handle.dataset.handle;
3124
+ handle.addEventListener(
3125
+ "pointerdown",
3126
+ (e) => {
3127
+ e.preventDefault();
3128
+ e.stopPropagation();
3129
+ const ids = transformIds();
3130
+ const members = ids.map((id) => controller.getLayer(id)).filter(Boolean);
3131
+ let startBbox;
3132
+ if (members.length === 1) {
3133
+ startBbox = { ...members[0].frame };
3134
+ } else {
3135
+ startBbox = getLayersBoundingBox(members);
3136
+ }
3137
+ if (!startBbox) return;
3138
+ const startX = e.clientX;
3139
+ const startY = e.clientY;
3140
+ setCapture(handle, e.pointerId);
3141
+ const onMove = (ev) => {
3142
+ const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
3143
+ const newBbox = computeResizeBbox(startBbox, corner, dx, dy);
3144
+ if (ids.length === 1) {
3145
+ applyWrapStyle(wrap, newBbox);
3146
+ } else {
3147
+ const resized = resizeGroupFrames(members, startBbox, newBbox);
3148
+ compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((el) => {
3149
+ const w = el;
3150
+ const lid = w.dataset.layerId;
3151
+ if (!lid || !ids.includes(lid)) return;
3152
+ const idx = ids.indexOf(lid);
3153
+ if (idx >= 0) applyWrapStyle(w, resized[idx]);
3154
+ });
3155
+ }
3156
+ };
3157
+ const onUp = (ev) => {
3158
+ releaseCapture();
3159
+ handle.removeEventListener("pointermove", onMove);
3160
+ handle.removeEventListener("pointerup", onUp);
3161
+ handle.removeEventListener("pointercancel", onUp);
3162
+ const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
3163
+ const newBbox = computeResizeBbox(startBbox, corner, dx, dy);
3164
+ if (!framesEqual(newBbox, startBbox) && (Math.abs(dx) > EPS || Math.abs(dy) > EPS)) {
3165
+ controller.resizeLayersFromBbox(ids, newBbox);
3166
+ commitChange();
3167
+ }
3168
+ };
3169
+ handle.addEventListener("pointermove", onMove, { signal });
3170
+ handle.addEventListener("pointerup", onUp, { signal });
3171
+ handle.addEventListener("pointercancel", onUp, { signal });
3172
+ },
3173
+ { signal }
3174
+ );
3175
+ });
3176
+ };
3177
+ const cleanupEditChrome = () => {
3178
+ compositorRoot.classList.remove("tv-layer-edit-mode");
3179
+ compositorRoot.querySelectorAll(".tv-layer-marquee").forEach((el) => el.remove());
3180
+ compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((w) => {
3181
+ const el = w;
3182
+ el.classList.remove("tv-layer-editable", "tv-layer-dragging");
3183
+ el.style.outline = "";
3184
+ el.style.cursor = "";
3185
+ el.querySelectorAll(".tv-layer-handle").forEach((h) => h.remove());
3186
+ clearWrapContentPassthrough(el);
3187
+ });
3188
+ compositorRoot.style.zIndex = "";
3189
+ };
3190
+ const rebind = () => {
3191
+ releaseCapture();
3192
+ abort?.abort();
3193
+ if (!enabled) {
3194
+ cleanupEditChrome();
3195
+ return;
3196
+ }
3197
+ compositorRoot.classList.add("tv-layer-edit-mode");
3198
+ compositorRoot.style.zIndex = "80";
3199
+ abort = new AbortController();
3200
+ const { signal } = abort;
3201
+ bindMarquee(signal);
3202
+ compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((w) => {
3203
+ const wrap = w;
3204
+ const layerId = wrap.dataset.layerId;
3205
+ if (!layerId) return;
3206
+ decorateWrap(wrap, layerId);
3207
+ bindWrap(wrap, signal);
3208
+ });
3209
+ };
3210
+ return {
3211
+ setEnabled(next) {
3212
+ enabled = next;
3213
+ rebind();
3214
+ },
3215
+ isEnabled: () => enabled,
3216
+ rebind,
3217
+ destroy: () => {
3218
+ enabled = false;
3219
+ releaseCapture();
3220
+ abort?.abort();
3221
+ cleanupEditChrome();
3222
+ }
3223
+ };
3224
+ }
3225
+
3226
+ // src/layer/compositor.ts
3227
+ function resolveWidgetEl(widgets, layer) {
3228
+ const key = layer.widgetKey;
3229
+ if (layer.type === "overlay.drawing") {
3230
+ return widgets.drawingOverlay ?? widgets.chartMain ?? widgets.chartHost;
3231
+ }
3232
+ return widgets[key] ?? (layer.widgetKey === "chartMain" ? widgets.chartHost : void 0) ?? (layer.widgetKey === "chartIndicator" ? widgets.indicatorHost : void 0);
3233
+ }
3234
+ function applyWrapFrame(wrap, layer) {
3235
+ wrap.style.left = `${layer.frame.x * 100}%`;
3236
+ wrap.style.top = `${layer.frame.y * 100}%`;
3237
+ wrap.style.width = `${layer.frame.w * 100}%`;
3238
+ wrap.style.height = `${layer.frame.h * 100}%`;
3239
+ wrap.style.zIndex = String(layer.zIndex + 1);
3240
+ }
3241
+ function layerStructureSignature(layers, activePageId) {
3242
+ return `${activePageId}|${layers.map((l) => `${l.id}:${l.type}:${l.widgetKey}:${l.visible}:${l.locked}`).join("|")}`;
3243
+ }
3244
+ function wrapPointerEventsForLayer(layer, editorEnabled) {
3245
+ if (layer.locked) return "none";
3246
+ if (isOverlayLayerType(layer.type)) return "none";
3247
+ if (isChartPaneLayerType(layer.type) && editorEnabled) return "none";
3248
+ return "auto";
3249
+ }
3250
+ function mountLayerCompositor(parent, opts) {
3251
+ const controller = new LayerController(opts.preset);
3252
+ const wrappers = /* @__PURE__ */ new Map();
3253
+ let lastStructureSig = "";
3254
+ const root = document.createElement("div");
3255
+ root.className = "tv-layer-compositor";
3256
+ root.style.cssText = "position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:5;";
3257
+ parent.style.position = parent.style.position || "relative";
3258
+ parent.appendChild(root);
3259
+ const editor = attachLayerEditor(root, {
3260
+ controller,
3261
+ onPresetChange: () => opts.onPresetChange?.(controller.getPreset()),
3262
+ onMarqueeSelect: opts.onMarqueeSelect
3263
+ });
3264
+ const applyFocusStyles = () => {
3265
+ const focused = controller.focusedPaneLayerId;
3266
+ for (const [id, wrap] of wrappers) {
3267
+ const layer = controller.getLayer(id);
3268
+ if (!layer || !isChartPaneLayerType(layer.type)) continue;
3269
+ const active = id === focused;
3270
+ wrap.dataset.focused = active ? "1" : "0";
3271
+ wrap.style.outline = active ? "2px solid #58a6ff" : "none";
3272
+ wrap.style.outlineOffset = active ? "-2px" : "";
3273
+ }
3274
+ };
3275
+ const patchFocusVisuals = (layers) => {
3276
+ for (const layer of layers) {
3277
+ const wrap = wrappers.get(layer.id);
3278
+ if (wrap) applyWrapFrame(wrap, layer);
3279
+ }
3280
+ applyFocusStyles();
3281
+ };
3282
+ const patchLayoutFrames = (layers, patchOpts = {}) => {
3283
+ patchFocusVisuals(layers);
3284
+ if (patchOpts.rebind !== false) editor.rebind();
3285
+ };
3286
+ const ensureDefaultChartFocus = (chartPaneIds) => {
3287
+ if (controller.focusedPaneLayerId && !chartPaneIds.includes(controller.focusedPaneLayerId)) {
3288
+ controller.focusedPaneLayerId = chartPaneIds[chartPaneIds.length - 1] ?? null;
3289
+ }
3290
+ if (!controller.focusedPaneLayerId && chartPaneIds.length > 0) {
3291
+ controller.focusedPaneLayerId = chartPaneIds[chartPaneIds.length - 1];
3292
+ }
3293
+ };
3294
+ const bindChartPanePointer = (wrap, layer) => {
3295
+ wrap.addEventListener("pointerdown", (e) => {
3296
+ if (e.button !== 0 || controller.isInteracting()) return;
3297
+ const prev = controller.focusedPaneLayerId;
3298
+ if (prev === layer.id) return;
3299
+ queueMicrotask(() => {
3300
+ if (controller.isInteracting()) return;
3301
+ controller.focusChartPane(layer.id);
3302
+ opts.onChartPaneFocus?.(controller.getFocusedPaneId());
3303
+ });
3304
+ });
3305
+ };
3306
+ const syncRootPointerEvents = () => {
3307
+ root.style.pointerEvents = editor.isEnabled() ? "auto" : "none";
3308
+ };
3309
+ const apply = () => {
3310
+ syncRootPointerEvents();
3311
+ const preset = controller.getPreset();
3312
+ const layers = preset.layers.filter((l) => l.pageId === controller.activePageId && l.visible).sort((a, b) => a.zIndex - b.zIndex);
3313
+ if (opts.hideLegacyGrid) opts.hideLegacyGrid.style.visibility = "hidden";
3314
+ const chartPaneIds = layers.filter((l) => isChartPaneLayerType(l.type)).map((l) => l.id);
3315
+ ensureDefaultChartFocus(chartPaneIds);
3316
+ const structureSig = layerStructureSignature(layers, controller.activePageId);
3317
+ if (structureSig === lastStructureSig && wrappers.size > 0) {
3318
+ patchLayoutFrames(layers);
3319
+ return;
3320
+ }
3321
+ lastStructureSig = structureSig;
3322
+ root.replaceChildren();
3323
+ wrappers.clear();
3324
+ const editorOn = editor.isEnabled();
3325
+ for (const layer of layers) {
3326
+ if (layer.type === "overlay.drawing") continue;
3327
+ const el = resolveWidgetEl(opts.widgets, layer);
3328
+ if (!el) continue;
3329
+ const wrap = document.createElement("div");
3330
+ wrap.className = "tv-layer-wrap";
3331
+ if (isChartPaneLayerType(layer.type)) wrap.classList.add("tv-chart-pane-wrap");
3332
+ if (isOverlayLayerType(layer.type)) wrap.classList.add("tv-overlay-wrap");
3333
+ wrap.dataset.layerId = layer.id;
3334
+ wrap.dataset.widgetKey = layer.widgetKey;
3335
+ wrap.dataset.layerType = layer.type;
3336
+ if (layer.groupId) wrap.dataset.groupId = layer.groupId;
3337
+ const pe = wrapPointerEventsForLayer(layer, editorOn);
3338
+ wrap.style.cssText = [
3339
+ "position:absolute",
3340
+ "overflow:visible",
3341
+ "box-sizing:border-box",
3342
+ `pointer-events:${pe}`
3343
+ ].join(";");
3344
+ applyWrapFrame(wrap, layer);
3345
+ if (isChartPaneLayerType(layer.type) && !layer.locked && !editorOn) {
3346
+ bindChartPanePointer(wrap, layer);
3347
+ }
3348
+ if (layer.type === "overlay.crosshairLegend") {
3349
+ el.style.pointerEvents = "none";
3350
+ }
3351
+ el.style.width = "100%";
3352
+ el.style.height = "100%";
3353
+ el.style.minWidth = "0";
3354
+ el.style.minHeight = "0";
3355
+ el.style.overflow = layer.type === "overlay.crosshairLegend" ? "visible" : "hidden";
3356
+ wrap.appendChild(el);
3357
+ root.appendChild(wrap);
3358
+ wrappers.set(layer.id, wrap);
3359
+ }
3360
+ applyFocusStyles();
3361
+ editor.rebind();
3362
+ };
3363
+ const unsubLayout = controller.subscribe(apply);
3364
+ const unsubFocus = controller.subscribeFocus(() => {
3365
+ const visible = controller.getLayersForActivePage().filter((l) => l.visible);
3366
+ patchFocusVisuals(visible);
3367
+ });
3368
+ apply();
3369
+ return {
3370
+ root,
3371
+ controller,
3372
+ apply,
3373
+ enableLayerEditor: (enabled) => {
3374
+ editor.setEnabled(enabled);
3375
+ syncRootPointerEvents();
3376
+ apply();
3377
+ },
3378
+ isLayerEditorEnabled: () => editor.isEnabled(),
3379
+ destroy: () => {
3380
+ unsubLayout();
3381
+ unsubFocus();
3382
+ editor.destroy();
3383
+ root.remove();
3384
+ if (opts.hideLegacyGrid) opts.hideLegacyGrid.style.visibility = "";
3385
+ }
3386
+ };
3387
+ }
3388
+
3389
+ // src/layer/layer-panel.ts
3390
+ import { t as t8 } from "@coderyo/i18n";
3391
+ function mountLayerPanel(parent, controller, opts = {}) {
3392
+ const panel = document.createElement("aside");
3393
+ panel.className = "tv-layer-panel";
3394
+ panel.style.cssText = "display:none;position:fixed;right:12px;top:72px;width:240px;max-height:min(70vh,480px);z-index:2100;background:#161b22;border:1px solid #30363d;border-radius:8px;box-shadow:0 8px 24px #01040999;flex-direction:column;overflow:hidden;font-size:11px;color:#e6edf3;";
3395
+ const header = document.createElement("div");
3396
+ header.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #30363d;flex-shrink:0;";
3397
+ const title = document.createElement("span");
3398
+ title.textContent = t8("layer.panel.title", "\u5716\u5C64");
3399
+ title.style.fontWeight = "600";
3400
+ const closeBtn = document.createElement("button");
3401
+ closeBtn.type = "button";
3402
+ closeBtn.textContent = "\xD7";
3403
+ closeBtn.style.cssText = "background:transparent;border:none;color:#8b949e;font-size:18px;cursor:pointer;line-height:1;";
3404
+ header.append(title, closeBtn);
3405
+ const groupBar = document.createElement("div");
3406
+ groupBar.style.cssText = "display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #30363d;flex-shrink:0;flex-wrap:wrap;";
3407
+ const createGroupBtn = document.createElement("button");
3408
+ createGroupBtn.type = "button";
3409
+ createGroupBtn.textContent = t8("layer.panel.createGroup", "\u5EFA\u7ACB\u7FA4\u7D44");
3410
+ createGroupBtn.style.cssText = "flex:1;min-width:0;padding:4px 6px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:10px;";
3411
+ const removeFromGroupBtn = document.createElement("button");
3412
+ removeFromGroupBtn.type = "button";
3413
+ removeFromGroupBtn.textContent = t8("layer.panel.removeFromGroup", "\u79FB\u51FA\u7FA4\u7D44");
3414
+ removeFromGroupBtn.style.cssText = createGroupBtn.style.cssText;
3415
+ const ungroupBtn = document.createElement("button");
3416
+ ungroupBtn.type = "button";
3417
+ ungroupBtn.textContent = t8("layer.panel.ungroup", "\u89E3\u6563\u7FA4\u7D44");
3418
+ ungroupBtn.style.cssText = createGroupBtn.style.cssText;
3419
+ groupBar.append(createGroupBtn, removeFromGroupBtn, ungroupBtn);
3420
+ const list = document.createElement("div");
3421
+ list.style.cssText = "overflow:auto;flex:1;min-height:0;padding:6px 4px;";
3422
+ const footer = document.createElement("div");
3423
+ footer.style.cssText = "padding:8px;border-top:1px solid #30363d;flex-shrink:0;";
3424
+ const saveBtn = document.createElement("button");
3425
+ saveBtn.type = "button";
3426
+ saveBtn.textContent = t8("layer.panel.saveAs", "\u53E6\u5B58\u70BA\u6211\u7684\u7BC4\u672C");
3427
+ saveBtn.style.cssText = "width:100%;padding:6px 8px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;";
3428
+ footer.appendChild(saveBtn);
3429
+ panel.append(header, groupBar, list, footer);
3430
+ parent.appendChild(panel);
3431
+ let open = false;
3432
+ const selected = /* @__PURE__ */ new Set();
3433
+ let dragLayerId = null;
3434
+ const btnStyle = "width:24px;height:22px;padding:0;border:1px solid #30363d;border-radius:3px;background:#21262d;cursor:pointer;font-size:10px;";
3435
+ const render = () => {
3436
+ list.replaceChildren();
3437
+ const layers = [...controller.getLayersForActivePage()].sort((a, b) => b.zIndex - a.zIndex);
3438
+ for (const id of [...selected]) {
3439
+ if (!layers.some((l) => l.id === id)) selected.delete(id);
3440
+ }
3441
+ createGroupBtn.disabled = selected.size < 2;
3442
+ const selectedWithGroup = layers.filter((l) => selected.has(l.id) && l.groupId);
3443
+ ungroupBtn.disabled = selectedWithGroup.length === 0;
3444
+ const singleInGroup = selected.size === 1 && [...selected].some((id) => controller.getLayer(id)?.groupId);
3445
+ removeFromGroupBtn.disabled = !singleInGroup;
3446
+ for (const layer of layers) {
3447
+ const row = document.createElement("div");
3448
+ row.draggable = !layer.locked;
3449
+ row.dataset.layerId = layer.id;
3450
+ row.style.cssText = "display:flex;align-items:center;gap:4px;padding:4px 6px;margin-bottom:2px;border-radius:4px;background:#0d1117;border:1px solid #21262d;cursor:grab;";
3451
+ row.ondragstart = (e) => {
3452
+ if (layer.locked) return;
3453
+ dragLayerId = layer.id;
3454
+ e.dataTransfer?.setData("text/plain", layer.id);
3455
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
3456
+ row.style.opacity = "0.55";
3457
+ };
3458
+ row.ondragend = () => {
3459
+ row.style.opacity = "";
3460
+ dragLayerId = null;
3461
+ };
3462
+ row.ondragover = (e) => {
3463
+ e.preventDefault();
3464
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
3465
+ };
3466
+ row.ondrop = (e) => {
3467
+ e.preventDefault();
3468
+ if (!dragLayerId || dragLayerId === layer.id) return;
3469
+ const visual = [...controller.getLayersForActivePage()].sort(
3470
+ (a, b) => b.zIndex - a.zIndex
3471
+ );
3472
+ const ids = visual.map((l) => l.id);
3473
+ const from = ids.indexOf(dragLayerId);
3474
+ const to = ids.indexOf(layer.id);
3475
+ if (from < 0 || to < 0) return;
3476
+ ids.splice(from, 1);
3477
+ ids.splice(to, 0, dragLayerId);
3478
+ controller.reorderLayers([...ids].reverse());
3479
+ dragLayerId = null;
3480
+ };
3481
+ const pick = document.createElement("input");
3482
+ pick.type = "checkbox";
3483
+ pick.checked = selected.has(layer.id);
3484
+ pick.title = t8("layer.panel.select", "\u9078\u53D6");
3485
+ pick.style.cssText = "width:14px;height:14px;cursor:pointer;flex-shrink:0;";
3486
+ pick.onchange = () => {
3487
+ if (pick.checked) selected.add(layer.id);
3488
+ else selected.delete(layer.id);
3489
+ render();
3490
+ };
3491
+ const eye = document.createElement("button");
3492
+ eye.type = "button";
3493
+ eye.textContent = layer.visible ? "\u{1F441}" : "\u2014";
3494
+ eye.title = t8("layer.panel.visibility", "\u986F\u793A");
3495
+ eye.style.cssText = btnStyle;
3496
+ const lock = document.createElement("button");
3497
+ lock.type = "button";
3498
+ lock.textContent = layer.locked ? "\u{1F512}" : "\u{1F513}";
3499
+ lock.title = t8("layer.panel.lock", "\u9396\u5B9A");
3500
+ lock.style.cssText = btnStyle;
3501
+ const label = document.createElement("span");
3502
+ const groupTag = layer.groupId ? ` [${layer.groupId.slice(-4)}]` : "";
3503
+ label.textContent = `${layer.widgetKey}${groupTag}`;
3504
+ label.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
3505
+ let syncIn = null;
3506
+ if (isChartPaneLayerType(layer.type)) {
3507
+ syncIn = document.createElement("input");
3508
+ syncIn.type = "text";
3509
+ syncIn.placeholder = t8("layer.panel.syncGroup", "\u6642\u9593\u8EF8\u7D44");
3510
+ syncIn.value = layer.syncTimeScaleGroupId ?? "";
3511
+ syncIn.title = t8(
3512
+ "layer.panel.syncGroupHint",
3513
+ "\u76F8\u540C\u7D44\u540D\u540C\u6B65\u7E2E\u653E\uFF1B\u7559\u7A7A\u5247\u7368\u7ACB"
3514
+ );
3515
+ syncIn.style.cssText = "width:52px;flex-shrink:0;padding:2px 4px;font-size:10px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:3px;";
3516
+ syncIn.onchange = () => {
3517
+ controller.setLayerSyncGroup(layer.id, syncIn.value);
3518
+ };
3519
+ }
3520
+ const zUp = document.createElement("button");
3521
+ zUp.type = "button";
3522
+ zUp.textContent = "\u25B2";
3523
+ zUp.title = "z+";
3524
+ zUp.style.cssText = btnStyle;
3525
+ const zDown = document.createElement("button");
3526
+ zDown.type = "button";
3527
+ zDown.textContent = "\u25BC";
3528
+ zDown.title = "z-";
3529
+ zDown.style.cssText = btnStyle;
3530
+ eye.onclick = () => controller.setLayerVisible(layer.id, !layer.visible);
3531
+ lock.onclick = () => controller.setLayerLocked(layer.id, !layer.locked);
3532
+ zUp.onclick = () => controller.bumpZIndex(layer.id, 1);
3533
+ zDown.onclick = () => controller.bumpZIndex(layer.id, -1);
3534
+ if (syncIn) row.append(pick, eye, lock, label, syncIn, zUp, zDown);
3535
+ else row.append(pick, eye, lock, label, zUp, zDown);
3536
+ list.appendChild(row);
3537
+ }
3538
+ };
3539
+ createGroupBtn.onclick = () => {
3540
+ const ids = [...selected];
3541
+ if (ids.length < 2) return;
3542
+ controller.createBindGroup(ids);
3543
+ selected.clear();
3544
+ render();
3545
+ };
3546
+ removeFromGroupBtn.onclick = () => {
3547
+ const id = [...selected][0];
3548
+ if (!id) return;
3549
+ controller.removeLayerFromGroup(id);
3550
+ render();
3551
+ };
3552
+ ungroupBtn.onclick = () => {
3553
+ const groupIds = /* @__PURE__ */ new Set();
3554
+ for (const id of selected) {
3555
+ const layer = controller.getLayer(id);
3556
+ if (layer?.groupId) groupIds.add(layer.groupId);
3557
+ }
3558
+ for (const gid of groupIds) controller.dissolveGroup(gid);
3559
+ render();
3560
+ };
3561
+ const unsub = controller.subscribe(render);
3562
+ render();
3563
+ closeBtn.onclick = () => {
3564
+ open = false;
3565
+ panel.style.display = "none";
3566
+ };
3567
+ saveBtn.onclick = () => opts.onSaveAsPreset?.(controller.getPreset());
3568
+ return {
3569
+ el: panel,
3570
+ destroy: () => {
3571
+ unsub();
3572
+ panel.remove();
3573
+ },
3574
+ toggle: () => {
3575
+ open = !open;
3576
+ panel.style.display = open ? "flex" : "none";
3577
+ return open;
3578
+ },
3579
+ selectLayers(layerIds) {
3580
+ selected.clear();
3581
+ for (const id of layerIds) selected.add(id);
3582
+ render();
3583
+ }
3584
+ };
3585
+ }
3586
+
3587
+ // src/layer/page-navigator.ts
3588
+ import { t as t9 } from "@coderyo/i18n";
3589
+ function mountPageNavigator(parent, controller, opts = {}) {
3590
+ const mq = opts.narrowMq ?? "(max-width: 768px)";
3591
+ const nav = document.createElement("nav");
3592
+ nav.className = "tv-page-navigator";
3593
+ nav.style.cssText = "display:none;align-items:center;gap:4px;padding:4px 8px;border-top:1px solid #30363d;background:#161b22;flex-shrink:0;flex-wrap:wrap;font-size:11px;color:#e6edf3;";
3594
+ const tabs = document.createElement("div");
3595
+ tabs.style.cssText = "display:flex;gap:4px;flex-wrap:wrap;flex:1;min-width:0;";
3596
+ const actions = document.createElement("div");
3597
+ actions.style.cssText = "display:flex;gap:4px;flex-shrink:0;";
3598
+ const addBtn = document.createElement("button");
3599
+ addBtn.type = "button";
3600
+ addBtn.title = t9("page.nav.add", "\u65B0\u589E\u9801\u9762");
3601
+ addBtn.textContent = "+";
3602
+ addBtn.style.cssText = "width:28px;height:24px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;";
3603
+ const renameBtn = document.createElement("button");
3604
+ renameBtn.type = "button";
3605
+ renameBtn.title = t9("page.nav.rename", "\u91CD\u65B0\u547D\u540D");
3606
+ renameBtn.textContent = "\u270E";
3607
+ renameBtn.style.cssText = "width:28px;height:24px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:12px;";
3608
+ const delBtn = document.createElement("button");
3609
+ delBtn.type = "button";
3610
+ delBtn.title = t9("page.nav.delete", "\u522A\u9664\u9801\u9762");
3611
+ delBtn.textContent = "\xD7";
3612
+ delBtn.style.cssText = renameBtn.style.cssText;
3613
+ actions.append(addBtn, renameBtn, delBtn);
3614
+ nav.append(tabs, actions);
3615
+ parent.appendChild(nav);
3616
+ const tabBtnStyle = "padding:4px 10px;background:#21262d;color:#8b949e;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
3617
+ const tabBtnActiveStyle = tabBtnStyle.replace("#21262d", "#388bfd").replace("#8b949e", "#fff");
3618
+ const refresh = () => {
3619
+ tabs.replaceChildren();
3620
+ const preset = controller.getPreset();
3621
+ for (const page of preset.pages) {
3622
+ const btn = document.createElement("button");
3623
+ btn.type = "button";
3624
+ btn.textContent = page.title;
3625
+ btn.title = page.title;
3626
+ btn.style.cssText = page.id === controller.activePageId ? tabBtnActiveStyle : tabBtnStyle;
3627
+ btn.onclick = () => {
3628
+ if (controller.setActivePage(page.id)) {
3629
+ opts.onPageChange?.(page.id);
3630
+ }
3631
+ };
3632
+ tabs.appendChild(btn);
3633
+ }
3634
+ delBtn.disabled = preset.pages.length <= 1;
3635
+ };
3636
+ addBtn.onclick = () => {
3637
+ const title = window.prompt(t9("page.nav.addPrompt", "\u65B0\u9801\u9762\u540D\u7A31"), "");
3638
+ const id = controller.addPage(title ?? void 0);
3639
+ opts.onPageChange?.(id);
3640
+ };
3641
+ renameBtn.onclick = () => {
3642
+ const page = controller.getPreset().pages.find((p) => p.id === controller.activePageId);
3643
+ if (!page) return;
3644
+ let title = window.prompt(t9("page.nav.renamePrompt", "\u9801\u9762\u540D\u7A31"), page.title);
3645
+ if (title == null) return;
3646
+ while (title !== null && !controller.renamePage(page.id, title)) {
3647
+ title = window.prompt(
3648
+ t9("page.nav.renameEmpty", "\u540D\u7A31\u4E0D\u53EF\u70BA\u7A7A\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165"),
3649
+ title
3650
+ );
3651
+ if (title == null) return;
3652
+ }
3653
+ };
3654
+ delBtn.onclick = () => {
3655
+ const page = controller.getPreset().pages.find((p) => p.id === controller.activePageId);
3656
+ if (!page || controller.getPreset().pages.length <= 1) return;
3657
+ if (!window.confirm(t9("page.nav.deleteConfirm", `\u522A\u9664\u300C${page.title}\u300D\uFF1F`))) return;
3658
+ controller.removePage(page.id);
3659
+ opts.onPageChange?.(controller.activePageId);
3660
+ };
3661
+ const media = window.matchMedia(mq);
3662
+ const syncVisible = () => {
3663
+ nav.style.display = opts.alwaysVisible || media.matches ? "flex" : "none";
3664
+ };
3665
+ media.addEventListener("change", syncVisible);
3666
+ syncVisible();
3667
+ const unsub = controller.subscribe(refresh);
3668
+ refresh();
3669
+ return {
3670
+ el: nav,
3671
+ destroy: () => {
3672
+ unsub();
3673
+ media.removeEventListener("change", syncVisible);
3674
+ nav.remove();
3675
+ },
3676
+ refresh
3677
+ };
3678
+ }
3679
+
3680
+ // src/layer/bind-layer-time-scale-sync.ts
3681
+ function bindLayerTimeScaleSync(chart, controller, options) {
3682
+ const apply = () => {
3683
+ chart.applyTimeScaleSyncFromLayers(
3684
+ controller.getPreset().layers,
3685
+ controller.activePageId
3686
+ );
3687
+ options?.onSync?.();
3688
+ };
3689
+ apply();
3690
+ return controller.subscribe(apply);
3691
+ }
3692
+
3693
+ // src/layer/merge-preset.ts
3694
+ function upsertById(current, incoming) {
3695
+ if (!incoming?.length) return current.map((x) => ({ ...x }));
3696
+ const map = new Map(current.map((x) => [x.id, { ...x }]));
3697
+ for (const item of incoming) {
3698
+ if (!item?.id) continue;
3699
+ map.set(item.id, { ...item });
3700
+ }
3701
+ return [...map.values()];
3702
+ }
3703
+ function mergePages(current, incoming) {
3704
+ return upsertById(current, incoming);
3705
+ }
3706
+ function mergeLayers(current, incoming) {
3707
+ if (!incoming?.length) return current.map((l) => ({ ...l, frame: { ...l.frame } }));
3708
+ const map = new Map(current.map((l) => [l.id, { ...l, frame: { ...l.frame } }]));
3709
+ for (const raw of incoming) {
3710
+ if (!raw?.id) continue;
3711
+ const prev = map.get(raw.id);
3712
+ map.set(raw.id, {
3713
+ ...prev ?? raw,
3714
+ ...raw,
3715
+ frame: { ...prev?.frame ?? raw.frame ?? { x: 0, y: 0, w: 1, h: 1 } }
3716
+ });
3717
+ }
3718
+ return [...map.values()];
3719
+ }
3720
+ function mergeGroups(current, incoming) {
3721
+ if (!incoming?.length) {
3722
+ return current.map((g) => ({ ...g, layerIds: [...g.layerIds] }));
3723
+ }
3724
+ const map = new Map(current.map((g) => [g.id, { ...g, layerIds: [...g.layerIds] }]));
3725
+ for (const raw of incoming) {
3726
+ if (!raw?.id) continue;
3727
+ map.set(raw.id, {
3728
+ id: raw.id,
3729
+ name: raw.name ?? map.get(raw.id)?.name,
3730
+ layerIds: [...raw.layerIds ?? map.get(raw.id)?.layerIds ?? []]
3731
+ });
3732
+ }
3733
+ return [...map.values()];
3734
+ }
3735
+ function mergeLayoutPreset(current, partial) {
3736
+ return normalizeLayoutPreset({
3737
+ ...current,
3738
+ ...partial,
3739
+ version: LAYER_PRESET_VERSION,
3740
+ pages: mergePages(current.pages, partial.pages),
3741
+ layers: mergeLayers(current.layers, partial.layers),
3742
+ groups: mergeGroups(current.groups, partial.groups)
3743
+ });
3744
+ }
3745
+
3746
+ // src/layer/resolve-pane-layers.ts
3747
+ import {
3748
+ isValidLayerBridgePane,
3749
+ resolvePaneLayerIds as resolvePaneLayerIdsCore
3750
+ } from "@coderyo/core";
3751
+ var PANE_TO_LAYER_TYPE = {
3752
+ main: "chart.main",
3753
+ volume: "chart.volume",
3754
+ indicator: "chart.indicator"
3755
+ };
3756
+ function layerTypeForBridgePane(pane) {
3757
+ return PANE_TO_LAYER_TYPE[pane];
3758
+ }
3759
+ function resolvePaneLayerIds(preset, pane, opts) {
3760
+ return resolvePaneLayerIdsCore(preset, pane, opts);
3761
+ }
3762
+
3763
+ // src/drawing-context-menu.ts
3764
+ import { t as t10 } from "@coderyo/i18n";
3765
+ function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
3766
+ const menu = document.createElement("div");
3767
+ menu.style.cssText = "position:fixed;z-index:60;min-width:168px;padding:4px 0;background:#161b22;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #01040988;";
3768
+ menu.style.left = `${clientX}px`;
3769
+ menu.style.top = `${clientY}px`;
3770
+ const add = (label, fn, disabled = false) => {
3771
+ if (!fn) return;
3772
+ const btn = document.createElement("button");
3773
+ btn.type = "button";
3774
+ btn.textContent = label;
3775
+ btn.disabled = disabled;
3776
+ btn.style.cssText = "display:block;width:100%;text-align:left;padding:8px 12px;border:none;background:transparent;color:#e6edf3;font-size:12px;cursor:pointer;";
3777
+ if (disabled) btn.style.opacity = "0.45";
3778
+ btn.onclick = () => {
3779
+ fn();
3780
+ close();
3781
+ };
3782
+ menu.appendChild(btn);
3783
+ };
3784
+ if (drawing) {
3785
+ const locked = Boolean(drawing.meta?.locked);
3786
+ add(t10("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
3787
+ add(t10("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
3788
+ add(
3789
+ locked ? t10("drawing.ctx.unlock", "\u89E3\u9396") : t10("drawing.ctx.lock", "\u9396\u5B9A"),
3790
+ handlers.onToggleLock
3791
+ );
3792
+ if (drawing.type === "text") {
3793
+ add(t10("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
3794
+ }
3795
+ add(t10("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
3796
+ }
3797
+ document.body.appendChild(menu);
3798
+ const close = () => {
3799
+ menu.remove();
3800
+ document.removeEventListener("click", close);
3801
+ };
3802
+ setTimeout(() => document.addEventListener("click", close), 0);
3803
+ return close;
3804
+ }
3805
+
3806
+ // src/code-snippet-panel.ts
3807
+ function mountCodeSnippetPanel(parent, getCode) {
3808
+ const wrap = document.createElement("details");
3809
+ wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;padding:6px 12px;font-size:11px;color:#8b949e;";
3810
+ const summary = document.createElement("summary");
3811
+ summary.textContent = "\u5D4C\u5165\u7A0B\u5F0F\u78BC\uFF08\u6574\u5408\u65B9\uFF09";
3812
+ summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;";
3813
+ const pre = document.createElement("pre");
3814
+ pre.style.cssText = "margin:8px 0 0;padding:10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:11px;overflow:auto;max-height:160px;white-space:pre-wrap;";
3815
+ const copyBtn = document.createElement("button");
3816
+ copyBtn.type = "button";
3817
+ copyBtn.textContent = "\u8907\u88FD";
3818
+ copyBtn.style.cssText = "margin-top:6px;padding:4px 10px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;";
3819
+ copyBtn.onclick = () => {
3820
+ const code = getCode();
3821
+ pre.textContent = code;
3822
+ void navigator.clipboard.writeText(code);
3823
+ };
3824
+ wrap.ontoggle = () => {
3825
+ if (wrap.open) pre.textContent = getCode();
3826
+ };
3827
+ wrap.append(summary, pre, copyBtn);
3828
+ parent.appendChild(wrap);
3829
+ return wrap;
3830
+ }
3831
+
3832
+ // src/pine-editor-panel.ts
3833
+ import { compilePineLite } from "@coderyo/pine-lite";
3834
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
1310
3835
  import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language";
1311
3836
  import { linter } from "@codemirror/lint";
1312
3837
  import { EditorState } from "@codemirror/state";
@@ -1319,7 +3844,7 @@ import {
1319
3844
  lineNumbers,
1320
3845
  placeholder
1321
3846
  } from "@codemirror/view";
1322
- import { t as t8 } from "@coderyo/i18n";
3847
+ import { t as t11 } from "@coderyo/i18n";
1323
3848
 
1324
3849
  // src/pine-language.ts
1325
3850
  import { StreamLanguage } from "@codemirror/language";
@@ -1379,10 +3904,10 @@ function createPineLinter(onStatus) {
1379
3904
  const src = view.state.doc.toString();
1380
3905
  const result = compilePineLite(src);
1381
3906
  if (result.ok) {
1382
- onStatus?.(t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
3907
+ onStatus?.(t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1383
3908
  return [];
1384
3909
  }
1385
- onStatus?.(result.errors[0] ?? t8("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
3910
+ onStatus?.(result.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
1386
3911
  return pineDiagnosticsToCm(src, result.diagnostics ?? []);
1387
3912
  });
1388
3913
  }
@@ -1411,16 +3936,16 @@ function mountPineEditorPanel(parent, opts = {}) {
1411
3936
  wrap.open = true;
1412
3937
  wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
1413
3938
  const summary = document.createElement("summary");
1414
- summary.textContent = t8("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
3939
+ summary.textContent = t11("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
1415
3940
  summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
1416
3941
  const toolbar = document.createElement("div");
1417
3942
  toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
1418
3943
  const status = document.createElement("span");
1419
3944
  status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
1420
- status.textContent = t8("pine.status.idle", "\u5C31\u7DD2");
3945
+ status.textContent = t11("pine.status.idle", "\u5C31\u7DD2");
1421
3946
  const applyBtn = document.createElement("button");
1422
3947
  applyBtn.type = "button";
1423
- applyBtn.textContent = t8("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
3948
+ applyBtn.textContent = t11("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1424
3949
  applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
1425
3950
  const host = document.createElement("div");
1426
3951
  host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
@@ -1435,10 +3960,10 @@ function mountPineEditorPanel(parent, opts = {}) {
1435
3960
  savePineScriptPreference(src);
1436
3961
  if (r.ok) {
1437
3962
  status.style.color = "#3fb950";
1438
- status.textContent = t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
3963
+ status.textContent = t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1439
3964
  } else {
1440
3965
  status.style.color = "#f85149";
1441
- status.textContent = r.errors[0] ?? t8("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
3966
+ status.textContent = r.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
1442
3967
  }
1443
3968
  };
1444
3969
  const extensions = [
@@ -1456,7 +3981,7 @@ function mountPineEditorPanel(parent, opts = {}) {
1456
3981
  status.textContent = msg;
1457
3982
  }),
1458
3983
  keymap.of([...defaultKeymap, ...historyKeymap]),
1459
- placeholder(t8("pine.placeholder", "// plot(sma(close, 20))")),
3984
+ placeholder(t11("pine.placeholder", "// plot(sma(close, 20))")),
1460
3985
  EditorView.updateListener.of((u) => {
1461
3986
  if (!u.docChanged) return;
1462
3987
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -1485,29 +4010,70 @@ function mountPineEditorPanel(parent, opts = {}) {
1485
4010
  };
1486
4011
  }
1487
4012
  export {
4013
+ BUILTIN_PRESETS,
4014
+ CHART_PANE_LAYER_TYPES,
1488
4015
  DEFAULT_LAYOUT_FEATURES,
4016
+ DEFAULT_LAYOUT_SCHEMA,
4017
+ DEFAULT_SYNC_TIME_SCALE_GROUP,
1489
4018
  GRID_SETTING_KEY,
4019
+ LAYER_PRESET_VERSION,
4020
+ LAYOUT_SCHEMA_VERSION,
4021
+ LayerController,
4022
+ OVERLAY_LAYER_TYPES,
1490
4023
  PINE_SCRIPT_STORAGE_KEY,
1491
4024
  RETURN_CURSOR_KEY,
1492
4025
  THEME_STORAGE_KEY,
4026
+ VENDOR_COMPACT_PRESET,
4027
+ VENDOR_DEFAULT_PRESET,
4028
+ VENDOR_DUAL_SYNC_PRESET,
4029
+ applyCompositorShellFeatures,
1493
4030
  applyThemeToDocument,
1494
4031
  attachChartContextMenu,
4032
+ attachLayerEditor,
4033
+ bindLayerTimeScaleSync,
1495
4034
  bindShortcutsModal,
4035
+ clampBBox,
4036
+ clampFrame,
4037
+ cloneLayoutPreset,
4038
+ cloneLayoutSchema,
4039
+ createCompositorShell,
1496
4040
  createDemoLayoutOptions,
1497
4041
  createI18nProvider,
4042
+ createLayoutGrid,
1498
4043
  createSymbolSearchDialog,
1499
4044
  createThemeProvider,
4045
+ deleteUserPreset,
4046
+ ensureOverlayLayers,
4047
+ expandLegacyChartHostLayers,
4048
+ forkPreset,
4049
+ getBuiltinPreset,
4050
+ getDrawingOverlayVisible,
4051
+ getLayersBoundingBox,
4052
+ getWidgetPlacement,
4053
+ isChartPaneLayerType,
4054
+ isOverlayLayerType,
4055
+ isValidLayerBridgePane as isValidBridgeLayerPane,
4056
+ layerTypeForBridgePane,
4057
+ layoutSchemaToPreset,
4058
+ layoutStorageKey,
4059
+ listPresets,
1500
4060
  loadIndicatorConfig,
4061
+ loadLayoutSchema,
1501
4062
  loadPineScriptPreference,
4063
+ loadPreset,
1502
4064
  loadReturnToCursorPreference,
1503
4065
  loadShowGridPreference,
1504
4066
  loadTheme,
1505
4067
  mergeLayoutFeatures,
4068
+ mergeLayoutPreset,
1506
4069
  mountChartLayout,
1507
4070
  mountCodeSnippetPanel,
1508
4071
  mountCrosshairLegend,
1509
4072
  mountDrawingPropertiesPanel,
4073
+ mountLayerCompositor,
4074
+ mountLayerPanel,
1510
4075
  mountLogoSlot,
4076
+ mountPageNavigator,
1511
4077
  mountPineEditorPanel,
1512
4078
  mountSettingsPanel as mountSettingsMenu,
1513
4079
  mountSettingsPanel,
@@ -1516,14 +4082,32 @@ export {
1516
4082
  mountSymbolSearchDialogTrigger,
1517
4083
  mountThemeToggle,
1518
4084
  mountTopBar,
4085
+ moveLayerFrames,
4086
+ normalizeLayoutPreset,
4087
+ normalizeLayoutSchema,
1519
4088
  openDrawingContextMenu,
1520
4089
  openShortcutsModal,
4090
+ paneIdFromWidgetKey,
1521
4091
  pineLanguage,
4092
+ presetStorageKey,
4093
+ resizeGroupFrames,
1522
4094
  resolveLayoutFeatures,
4095
+ resolveLayoutSchema,
4096
+ resolvePaneLayerIds,
4097
+ resolvePreset,
1523
4098
  saveIndicatorConfig,
4099
+ saveLayoutSchema,
1524
4100
  savePineScriptPreference,
4101
+ savePreset,
1525
4102
  saveReturnToCursorPreference,
1526
4103
  saveShowGridPreference,
1527
- saveTheme
4104
+ saveTheme,
4105
+ splitLegacyChartHost,
4106
+ syncAllOverlayLayersToMain,
4107
+ syncCompositorShellVisibilityFromFeatures,
4108
+ syncCrosshairLegendToMain,
4109
+ syncOverlayLayersToMain,
4110
+ upgradeIndicatorHostType,
4111
+ widgetKeyForChartPaneType
1528
4112
  };
1529
4113
  //# sourceMappingURL=index.js.map