@coderyo/ui-shell 1.0.2 → 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
@@ -1,9 +1,42 @@
1
1
  // src/top-bar.ts
2
2
  import { DEFAULT_INTERVALS } from "@coderyo/data";
3
- import { t as t3 } from "@coderyo/i18n";
3
+
4
+ // src/logo-slot.ts
5
+ function mountLogoSlot(parent, opts = {}) {
6
+ const slot = document.createElement("div");
7
+ slot.className = "tv-logo-slot";
8
+ slot.style.cssText = "display:flex;align-items:center;gap:8px;margin-right:12px;flex-shrink:0;font-weight:600;font-size:13px;color:var(--tv-fg,#e6edf3);";
9
+ const label = opts.label ?? "TradView";
10
+ const text = document.createElement("span");
11
+ text.textContent = label;
12
+ const children = [];
13
+ if (opts.imageSrc) {
14
+ const img = document.createElement("img");
15
+ img.src = opts.imageSrc;
16
+ img.alt = label;
17
+ img.style.cssText = "height:22px;width:auto;display:block;";
18
+ children.push(img);
19
+ }
20
+ children.push(text);
21
+ if (opts.href) {
22
+ const link = document.createElement("a");
23
+ link.href = opts.href;
24
+ link.style.cssText = "color:inherit;text-decoration:none;display:flex;align-items:center;gap:8px;";
25
+ link.append(...children);
26
+ slot.appendChild(link);
27
+ } else {
28
+ slot.append(...children);
29
+ }
30
+ parent.appendChild(slot);
31
+ return slot;
32
+ }
4
33
 
5
34
  // src/user-preferences.ts
6
- import { DEFAULT_INDICATOR_CONFIG, indicatorConfigStorageKey } from "@coderyo/indicators";
35
+ import {
36
+ defaultChartStorage,
37
+ loadIndicatorConfig as loadIndicatorConfigFromCore,
38
+ saveIndicatorConfig as saveIndicatorConfigToCore
39
+ } from "@coderyo/core";
7
40
  var GRID_SETTING_KEY = "tradview:settings:showGrid";
8
41
  var RETURN_CURSOR_KEY = "tradview:settings:returnToCursorAfterDraw";
9
42
  function loadShowGridPreference() {
@@ -33,30 +66,25 @@ function saveReturnToCursorPreference(v) {
33
66
  }
34
67
  }
35
68
  function loadIndicatorConfig(symbol, interval) {
36
- try {
37
- const raw = localStorage.getItem(indicatorConfigStorageKey(symbol, interval));
38
- if (!raw) return { ...DEFAULT_INDICATOR_CONFIG };
39
- return { ...DEFAULT_INDICATOR_CONFIG, ...JSON.parse(raw) };
40
- } catch {
41
- return { ...DEFAULT_INDICATOR_CONFIG };
42
- }
69
+ return loadIndicatorConfigFromCore(defaultChartStorage, symbol, interval);
43
70
  }
44
71
  function saveIndicatorConfig(symbol, interval, config) {
45
- try {
46
- localStorage.setItem(indicatorConfigStorageKey(symbol, interval), JSON.stringify(config));
47
- } catch {
48
- }
72
+ saveIndicatorConfigToCore(defaultChartStorage, symbol, interval, config);
49
73
  }
50
74
 
51
75
  // src/settings-panel.ts
52
- import { DEFAULT_INDICATOR_CONFIG as DEFAULT_INDICATOR_CONFIG2 } from "@coderyo/indicators";
76
+ import {
77
+ DEFAULT_INDICATOR_CONFIG,
78
+ disableIndicatorLayer,
79
+ listActiveIndicatorLayers
80
+ } from "@coderyo/indicators";
53
81
  import { t } from "@coderyo/i18n";
54
82
  function mountSettingsPanel(parent, opts = {}) {
55
83
  let open = false;
56
84
  let tab = "chart";
57
85
  let showGrid = opts.showGrid ?? loadShowGridPreference();
58
86
  let returnToCursor = opts.returnToCursorAfterDraw ?? loadReturnToCursorPreference();
59
- let indicatorConfig = { ...opts.indicatorConfig ?? DEFAULT_INDICATOR_CONFIG2 };
87
+ let indicatorConfig = { ...opts.indicatorConfig ?? DEFAULT_INDICATOR_CONFIG };
60
88
  const wrap = document.createElement("div");
61
89
  wrap.style.cssText = "position:relative;";
62
90
  const btn = document.createElement("button");
@@ -64,12 +92,27 @@ function mountSettingsPanel(parent, opts = {}) {
64
92
  btn.title = t("settings.title", "\u8A2D\u5B9A");
65
93
  btn.textContent = "\u2699";
66
94
  btn.style.cssText = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:14px;";
67
- const panel = document.createElement("div");
68
- 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);
69
112
  const tabs = document.createElement("div");
70
- tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;";
113
+ tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;flex-shrink:0;";
71
114
  const content = document.createElement("div");
72
- 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;";
73
116
  const tabIds = [
74
117
  ["chart", t("settings.tab.chart", "\u5716\u8868")],
75
118
  ["drawing", t("settings.tab.drawing", "\u7E6A\u5716")],
@@ -101,6 +144,17 @@ function mountSettingsPanel(parent, opts = {}) {
101
144
  row.append(input, document.createTextNode(label));
102
145
  return row;
103
146
  };
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) => {
153
+ e.stopPropagation();
154
+ onClick();
155
+ };
156
+ return b;
157
+ };
104
158
  const numberField = (label, value, onChange) => {
105
159
  const row = document.createElement("label");
106
160
  row.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;";
@@ -113,6 +167,49 @@ function mountSettingsPanel(parent, opts = {}) {
113
167
  row.appendChild(input);
114
168
  return row;
115
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
+ };
116
213
  const renderContent = () => {
117
214
  content.replaceChildren();
118
215
  if (tab === "chart") {
@@ -131,23 +228,43 @@ function mountSettingsPanel(parent, opts = {}) {
131
228
  opts.onReturnToCursorChange?.(v);
132
229
  })
133
230
  );
231
+ content.appendChild(
232
+ actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
233
+ opts.onClearAllDrawings?.();
234
+ renderContent();
235
+ })
236
+ );
134
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);
135
249
  content.appendChild(
136
250
  checkbox(t("settings.ind.macd", "MACD \u7A97\u683C"), indicatorConfig.showMacd, (v) => {
137
251
  indicatorConfig = { ...indicatorConfig, showMacd: v };
138
252
  opts.onIndicatorConfigChange?.(indicatorConfig);
253
+ renderContent();
139
254
  })
140
255
  );
141
256
  content.appendChild(
142
257
  checkbox(t("settings.ind.rsi", "RSI \u7A97\u683C"), indicatorConfig.showRsi, (v) => {
143
258
  indicatorConfig = { ...indicatorConfig, showRsi: v };
144
259
  opts.onIndicatorConfigChange?.(indicatorConfig);
260
+ renderContent();
145
261
  })
146
262
  );
147
263
  content.appendChild(
148
264
  checkbox(t("settings.ind.kdj", "KDJ \u7A97\u683C"), indicatorConfig.showKdj, (v) => {
149
265
  indicatorConfig = { ...indicatorConfig, showKdj: v };
150
266
  opts.onIndicatorConfigChange?.(indicatorConfig);
267
+ renderContent();
151
268
  })
152
269
  );
153
270
  const src = document.createElement("label");
@@ -172,6 +289,7 @@ function mountSettingsPanel(parent, opts = {}) {
172
289
  checkbox(t("settings.ind.ema", "EMA \u758A\u52A0"), indicatorConfig.showEma, (v) => {
173
290
  indicatorConfig = { ...indicatorConfig, showEma: v };
174
291
  opts.onIndicatorConfigChange?.(indicatorConfig);
292
+ renderContent();
175
293
  })
176
294
  );
177
295
  content.appendChild(
@@ -184,6 +302,7 @@ function mountSettingsPanel(parent, opts = {}) {
184
302
  checkbox(t("settings.ind.boll", "BOLL \u901A\u9053"), indicatorConfig.showBoll, (v) => {
185
303
  indicatorConfig = { ...indicatorConfig, showBoll: v };
186
304
  opts.onIndicatorConfigChange?.(indicatorConfig);
305
+ renderContent();
187
306
  })
188
307
  );
189
308
  content.appendChild(
@@ -222,23 +341,151 @@ function mountSettingsPanel(parent, opts = {}) {
222
341
  opts.onIndicatorConfigChange?.(indicatorConfig);
223
342
  })
224
343
  );
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
+ );
351
+ }
352
+ };
353
+ const setOpen = (next) => {
354
+ open = next;
355
+ overlay.style.display = open ? "flex" : "none";
356
+ if (open) {
357
+ if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
358
+ renderContent();
225
359
  }
226
360
  };
227
361
  renderTabs();
228
362
  renderContent();
229
- panel.append(tabs, content);
363
+ dialog.append(header, tabs, content);
364
+ overlay.appendChild(dialog);
365
+ document.body.appendChild(overlay);
230
366
  btn.onclick = (e) => {
231
367
  e.stopPropagation();
232
- open = !open;
233
- panel.style.display = open ? "block" : "none";
368
+ setOpen(!open);
369
+ };
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);
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
+ };
388
+ return wrap;
389
+ }
390
+
391
+ // src/symbol-search-dialog.ts
392
+ function createSymbolSearchDialog(opts) {
393
+ const i18n = opts.i18n;
394
+ const backdrop = document.createElement("div");
395
+ backdrop.className = "tv-symbol-dialog-backdrop";
396
+ backdrop.style.cssText = "display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.55);align-items:flex-start;justify-content:center;padding:12vh 16px 16px;";
397
+ const panel = document.createElement("div");
398
+ panel.className = "tv-symbol-dialog";
399
+ panel.style.cssText = "width:min(420px,100%);background:var(--tv-surface,#161b22);border:1px solid var(--tv-border,#30363d);border-radius:8px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,0.4);";
400
+ const title = document.createElement("div");
401
+ title.textContent = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
402
+ title.style.cssText = "font-size:13px;font-weight:600;color:var(--tv-fg,#e6edf3);margin-bottom:8px;";
403
+ const input = document.createElement("input");
404
+ input.type = "search";
405
+ input.placeholder = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
406
+ input.value = opts.initialSymbol ?? "";
407
+ input.style.cssText = "width:100%;box-sizing:border-box;padding:8px 10px;border-radius:4px;border:1px solid var(--tv-border,#30363d);background:var(--tv-bg,#0d1117);color:var(--tv-fg,#e6edf3);font-size:13px;";
408
+ const list = document.createElement("div");
409
+ list.style.cssText = "margin-top:8px;max-height:240px;overflow:auto;";
410
+ panel.append(title, input, list);
411
+ backdrop.appendChild(panel);
412
+ let timer;
413
+ let mounted = false;
414
+ const renderHits = (hits) => {
415
+ list.replaceChildren();
416
+ if (hits.length === 0) {
417
+ const empty = document.createElement("div");
418
+ empty.textContent = "\u2014";
419
+ empty.style.cssText = "padding:8px;color:var(--tv-muted,#8b949e);font-size:12px;";
420
+ list.appendChild(empty);
421
+ return;
422
+ }
423
+ for (const hit of hits) {
424
+ const row = document.createElement("button");
425
+ row.type = "button";
426
+ row.textContent = hit.exchange ? `${hit.symbol} \xB7 ${hit.exchange}` : hit.symbol;
427
+ row.style.cssText = "display:block;width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;color:var(--tv-fg,#e6edf3);cursor:pointer;font-size:12px;border-radius:4px;";
428
+ row.onmouseenter = () => {
429
+ row.style.background = "var(--tv-btn-bg,#21262d)";
430
+ };
431
+ row.onmouseleave = () => {
432
+ row.style.background = "transparent";
433
+ };
434
+ row.onclick = () => {
435
+ opts.onSelect(hit.symbol);
436
+ close();
437
+ };
438
+ list.appendChild(row);
439
+ }
440
+ };
441
+ const runSearch = () => {
442
+ const q = input.value.trim();
443
+ if (q.length < 1) {
444
+ list.replaceChildren();
445
+ return;
446
+ }
447
+ void opts.onSearch(q).then(renderHits).catch(() => list.replaceChildren());
448
+ };
449
+ input.addEventListener("input", () => {
450
+ clearTimeout(timer);
451
+ timer = setTimeout(runSearch, 200);
452
+ });
453
+ input.addEventListener("keydown", (e) => {
454
+ if (e.key === "Escape") close();
455
+ if (e.key === "Enter") runSearch();
456
+ });
457
+ backdrop.addEventListener("click", (e) => {
458
+ if (e.target === backdrop) close();
459
+ });
460
+ const open = () => {
461
+ if (!mounted) {
462
+ document.body.appendChild(backdrop);
463
+ mounted = true;
464
+ }
465
+ backdrop.style.display = "flex";
466
+ input.focus();
467
+ input.select();
468
+ runSearch();
234
469
  };
235
470
  const close = () => {
236
- open = false;
237
- panel.style.display = "none";
471
+ backdrop.style.display = "none";
238
472
  };
239
- document.addEventListener("click", close);
240
- panel.onclick = (e) => e.stopPropagation();
241
- wrap.append(btn, panel);
473
+ const destroy = () => {
474
+ clearTimeout(timer);
475
+ backdrop.remove();
476
+ mounted = false;
477
+ };
478
+ return { open, close, destroy };
479
+ }
480
+ function mountSymbolSearchDialogTrigger(parent, dialog, opts) {
481
+ const wrap = document.createElement("div");
482
+ wrap.style.cssText = "display:flex;align-items:center;margin-right:8px;flex-shrink:0;";
483
+ const btn = document.createElement("button");
484
+ btn.type = "button";
485
+ btn.textContent = opts.initialSymbol ?? (opts.i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1");
486
+ btn.style.cssText = "min-width:140px;text-align:left;background:var(--tv-bg,#0d1117);color:var(--tv-fg,#e6edf3);border:1px solid var(--tv-border,#30363d);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;";
487
+ btn.onclick = () => dialog.open();
488
+ wrap.appendChild(btn);
242
489
  parent.appendChild(wrap);
243
490
  return wrap;
244
491
  }
@@ -306,13 +553,32 @@ function mountSymbolSearch(parent, opts) {
306
553
  return wrap;
307
554
  }
308
555
 
556
+ // src/theme-toggle.ts
557
+ function mountThemeToggle(parent, opts) {
558
+ const i18n = opts.i18n;
559
+ const btn = document.createElement("button");
560
+ btn.type = "button";
561
+ btn.className = "tv-theme-toggle";
562
+ btn.style.cssText = "background:var(--tv-btn-bg,#21262d);color:var(--tv-fg,#e6edf3);border:1px solid var(--tv-border,#30363d);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;flex-shrink:0;";
563
+ const paint = (theme) => {
564
+ btn.textContent = theme === "dark" ? i18n?.t("theme.dark", "\u6DF1\u8272") ?? "\u6DF1\u8272" : i18n?.t("theme.light", "\u6DFA\u8272") ?? "\u6DFA\u8272";
565
+ };
566
+ btn.onclick = () => {
567
+ const next = opts.themeProvider.toggle();
568
+ opts.onThemeChange?.(next);
569
+ };
570
+ opts.themeProvider.subscribe((theme) => paint(theme));
571
+ parent.appendChild(btn);
572
+ return btn;
573
+ }
574
+
309
575
  // src/top-bar.ts
310
576
  function mountManualSymbolInput(parent, opts) {
311
577
  const wrap = document.createElement("div");
312
578
  wrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-right:8px;";
313
579
  const input = document.createElement("input");
314
580
  input.type = "text";
315
- input.placeholder = t3("symbol.search", "Symbol");
581
+ input.placeholder = opts.i18n?.t("symbol.search", "Symbol") ?? "Symbol";
316
582
  input.value = opts.initialSymbol ?? "";
317
583
  input.style.cssText = "width:140px;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 8px;font-size:12px;";
318
584
  const apply = () => {
@@ -332,9 +598,14 @@ function mountManualSymbolInput(parent, opts) {
332
598
  parent.appendChild(wrap);
333
599
  }
334
600
  function mountTopBar(parent, opts = {}) {
601
+ const i18n = opts.i18n;
602
+ const tKey = (key, fallback) => i18n?.t(key, fallback) ?? fallback;
335
603
  const bar = document.createElement("div");
336
604
  bar.className = "tv-topbar";
337
- bar.style.cssText = "display:flex;gap:8px;padding:8px 12px;align-items:center;border-bottom:1px solid #30363d;background:#161b22;flex-shrink:0;box-sizing:border-box;width:100%;min-width:0;overflow-x:auto;overflow-y:visible;position:relative;";
605
+ bar.style.cssText = "display:flex;gap:8px;padding:8px 12px;align-items:center;border-bottom:1px solid var(--tv-border,#30363d);background:var(--tv-surface,#161b22);flex-shrink:0;box-sizing:border-box;width:100%;min-width:0;overflow-x:auto;overflow-y:visible;position:relative;";
606
+ if (opts.logo !== false) {
607
+ mountLogoSlot(bar, opts.logo ?? { label: "TradView" });
608
+ }
338
609
  const symbolMode = opts.symbolInput ?? "manual";
339
610
  if (symbolMode === "search" && opts.onSymbolSearch && opts.onSymbolSelect) {
340
611
  mountSymbolSearch(bar, {
@@ -342,10 +613,22 @@ function mountTopBar(parent, opts = {}) {
342
613
  onSearch: opts.onSymbolSearch,
343
614
  onSelect: opts.onSymbolSelect
344
615
  });
616
+ } else if (symbolMode === "dialog" && opts.onSymbolSearch && opts.onSymbolSelect) {
617
+ const dialog = createSymbolSearchDialog({
618
+ initialSymbol: opts.initialSymbol,
619
+ onSearch: opts.onSymbolSearch,
620
+ onSelect: opts.onSymbolSelect,
621
+ i18n
622
+ });
623
+ mountSymbolSearchDialogTrigger(bar, dialog, {
624
+ initialSymbol: opts.initialSymbol,
625
+ i18n
626
+ });
345
627
  } else if (symbolMode === "manual" && opts.onSymbolSelect) {
346
628
  mountManualSymbolInput(bar, {
347
629
  initialSymbol: opts.initialSymbol,
348
- onSymbolSelect: opts.onSymbolSelect
630
+ onSymbolSelect: opts.onSymbolSelect,
631
+ i18n
349
632
  });
350
633
  }
351
634
  const intervals = opts.intervals ?? DEFAULT_INTERVALS;
@@ -364,7 +647,7 @@ function mountTopBar(parent, opts = {}) {
364
647
  for (const iv of intervals) {
365
648
  const btn = document.createElement("button");
366
649
  btn.type = "button";
367
- btn.textContent = t3(`interval.${iv}`, iv);
650
+ btn.textContent = tKey(`interval.${iv}`, iv);
368
651
  btn.style.cssText = btnStyle;
369
652
  btn.onclick = () => {
370
653
  if (activeInterval === iv) return;
@@ -388,7 +671,15 @@ function mountTopBar(parent, opts = {}) {
388
671
  b.onclick = () => fn?.();
389
672
  return b;
390
673
  };
391
- bar.appendChild(mkBtn(t3("theme.dark", "\u4E3B\u984C"), opts.onThemeToggle));
674
+ if (opts.themeProvider) {
675
+ mountThemeToggle(bar, {
676
+ themeProvider: opts.themeProvider,
677
+ i18n,
678
+ onThemeChange: opts.onThemeChange
679
+ });
680
+ } else {
681
+ bar.appendChild(mkBtn(tKey("theme.dark", "\u4E3B\u984C"), opts.onThemeToggle));
682
+ }
392
683
  if (opts.showSettings && opts.settings) mountSettingsPanel(bar, opts.settings);
393
684
  bar.appendChild(mkBtn("\u26F6", opts.onFullscreen));
394
685
  bar.appendChild(mkBtn("\u{1F4F7}", opts.onScreenshot));
@@ -403,8 +694,119 @@ function mountTopBar(parent, opts = {}) {
403
694
  };
404
695
  }
405
696
 
697
+ // src/theme-provider.ts
698
+ var THEME_STORAGE_KEY = "tradview:theme";
699
+ var THEME_VARS = {
700
+ dark: {
701
+ "--tv-bg": "#0d1117",
702
+ "--tv-surface": "#161b22",
703
+ "--tv-border": "#30363d",
704
+ "--tv-fg": "#e6edf3",
705
+ "--tv-muted": "#8b949e",
706
+ "--tv-accent": "#388bfd",
707
+ "--tv-btn-bg": "#21262d"
708
+ },
709
+ light: {
710
+ "--tv-bg": "#ffffff",
711
+ "--tv-surface": "#f6f8fa",
712
+ "--tv-border": "#d0d7de",
713
+ "--tv-fg": "#24292f",
714
+ "--tv-muted": "#656d76",
715
+ "--tv-accent": "#0969da",
716
+ "--tv-btn-bg": "#f6f8fa"
717
+ }
718
+ };
719
+ function loadTheme() {
720
+ try {
721
+ const v = localStorage.getItem(THEME_STORAGE_KEY);
722
+ return v === "light" ? "light" : "dark";
723
+ } catch {
724
+ return "dark";
725
+ }
726
+ }
727
+ function saveTheme(theme) {
728
+ try {
729
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
730
+ } catch {
731
+ }
732
+ }
733
+ function applyThemeToDocument(theme, root = document.documentElement) {
734
+ const vars = THEME_VARS[theme];
735
+ for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
736
+ root.dataset.tradviewTheme = theme;
737
+ }
738
+ function createThemeProvider(initial) {
739
+ let theme = initial ?? loadTheme();
740
+ const listeners = /* @__PURE__ */ new Set();
741
+ const notify = () => {
742
+ applyThemeToDocument(theme);
743
+ for (const l of listeners) l(theme);
744
+ };
745
+ applyThemeToDocument(theme);
746
+ return {
747
+ getTheme: () => theme,
748
+ setTheme: (next) => {
749
+ if (next === theme) return;
750
+ theme = next;
751
+ saveTheme(theme);
752
+ notify();
753
+ },
754
+ toggle: () => {
755
+ theme = theme === "dark" ? "light" : "dark";
756
+ saveTheme(theme);
757
+ notify();
758
+ return theme;
759
+ },
760
+ subscribe: (listener) => {
761
+ listeners.add(listener);
762
+ listener(theme);
763
+ return () => listeners.delete(listener);
764
+ }
765
+ };
766
+ }
767
+
768
+ // src/i18n-provider.ts
769
+ import {
770
+ getLocale,
771
+ registerLocale,
772
+ setLocale,
773
+ t as i18nT
774
+ } from "@coderyo/i18n";
775
+ function interpolate(text, params) {
776
+ if (!params) return text;
777
+ return text.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
778
+ }
779
+ function createI18nProvider(defaultLocale = "zh-TW") {
780
+ setLocale(defaultLocale);
781
+ const listeners = /* @__PURE__ */ new Set();
782
+ return {
783
+ t(key, fallback, params) {
784
+ return interpolate(i18nT(key, fallback), params);
785
+ },
786
+ getLocale,
787
+ setLocale(locale) {
788
+ setLocale(locale);
789
+ for (const l of listeners) l(locale);
790
+ },
791
+ registerLocale(loc, dictionary) {
792
+ registerLocale(loc, dictionary);
793
+ if (getLocale() === loc) {
794
+ for (const l of listeners) l(loc);
795
+ }
796
+ },
797
+ subscribe(listener) {
798
+ listeners.add(listener);
799
+ listener(getLocale());
800
+ return () => listeners.delete(listener);
801
+ }
802
+ };
803
+ }
804
+
805
+ // src/chart-layout.ts
806
+ import { t as t7 } from "@coderyo/i18n";
807
+
406
808
  // src/context-menu.ts
407
- import { t as t4 } from "@coderyo/i18n";
809
+ import { t as t3 } from "@coderyo/i18n";
408
810
  function attachChartContextMenu(chartHost, opts = {}) {
409
811
  let menu = null;
410
812
  const close = () => {
@@ -418,7 +820,7 @@ function attachChartContextMenu(chartHost, opts = {}) {
418
820
  const actions = opts.actions ?? [
419
821
  {
420
822
  id: "fit",
421
- label: t4("context.fitContent", "\u9069\u914D\u756B\u9762"),
823
+ label: t3("context.fitContent", "\u9069\u914D\u756B\u9762"),
422
824
  onClick: () => {
423
825
  }
424
826
  }
@@ -468,7 +870,7 @@ function mountCrosshairLegend(chartHost, opts = {}) {
468
870
  title.style.cssText = "color:#8b949e;margin-bottom:2px;";
469
871
  const body = document.createElement("div");
470
872
  box.append(title, body);
471
- chartHost.appendChild(box);
873
+ if (chartHost) chartHost.appendChild(box);
472
874
  const fmt2 = (n) => n == null ? "\u2014" : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
473
875
  const render = (payload) => {
474
876
  const parts = [opts.symbol, opts.interval].filter(Boolean);
@@ -497,13 +899,13 @@ O ${fmt2(o?.o)} H ${fmt2(o?.h)} L ${fmt2(o?.l)} C ${fmt2(o?.c)}`;
497
899
  }
498
900
 
499
901
  // src/drawing-properties-panel.ts
500
- import { t as t5 } from "@coderyo/i18n";
902
+ import { t as t4 } from "@coderyo/i18n";
501
903
  function mountDrawingPropertiesPanel(parent, opts = {}) {
502
904
  const panel = document.createElement("aside");
503
905
  panel.className = "tv-drawing-props";
504
- 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;";
505
907
  const title = document.createElement("div");
506
- title.textContent = t5("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
908
+ title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
507
909
  title.style.cssText = "font-weight:600;margin-bottom:10px;";
508
910
  const typeEl = document.createElement("div");
509
911
  typeEl.style.color = "#8b949e";
@@ -517,26 +919,26 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
517
919
  row.appendChild(span);
518
920
  return row;
519
921
  };
520
- const colorRow = mkRow(t5("drawing.props.color", "\u984F\u8272"));
922
+ const colorRow = mkRow(t4("drawing.props.color", "\u984F\u8272"));
521
923
  const colorInput = document.createElement("input");
522
924
  colorInput.type = "color";
523
925
  colorInput.style.cssText = "width:100%;height:28px;border:none;background:transparent;cursor:pointer;";
524
926
  colorRow.appendChild(colorInput);
525
- const widthRow = mkRow(t5("drawing.props.lineWidth", "\u7DDA\u5BEC"));
927
+ const widthRow = mkRow(t4("drawing.props.lineWidth", "\u7DDA\u5BEC"));
526
928
  const widthInput = document.createElement("input");
527
929
  widthInput.type = "range";
528
930
  widthInput.min = "1";
529
931
  widthInput.max = "6";
530
932
  widthInput.style.width = "100%";
531
933
  widthRow.appendChild(widthInput);
532
- const textRow = mkRow(t5("drawing.props.text", "\u6587\u5B57"));
934
+ const textRow = mkRow(t4("drawing.props.text", "\u6587\u5B57"));
533
935
  const textInput = document.createElement("input");
534
936
  textInput.type = "text";
535
937
  textInput.style.cssText = "padding:4px 8px;border-radius:4px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;";
536
938
  textRow.appendChild(textInput);
537
939
  const lockedHint = document.createElement("div");
538
940
  lockedHint.style.cssText = "color:#f78166;font-size:11px;margin-top:6px;display:none;";
539
- lockedHint.textContent = t5("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
941
+ lockedHint.textContent = t4("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
540
942
  panel.append(title, typeEl, colorRow, widthRow, textRow, lockedHint);
541
943
  parent.appendChild(panel);
542
944
  const emit = () => {
@@ -555,7 +957,7 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
555
957
  return;
556
958
  }
557
959
  panel.style.display = "block";
558
- typeEl.textContent = `${t5("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
960
+ typeEl.textContent = `${t4("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
559
961
  const meta = drawing.meta ?? {};
560
962
  colorInput.value = String(meta.color ?? "#58a6ff");
561
963
  widthInput.value = String(meta.lineWidth ?? 2);
@@ -579,6 +981,414 @@ function mountIndicatorPaneHost(parent) {
579
981
  return host;
580
982
  }
581
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
+
582
1392
  // src/layout-features.ts
583
1393
  var DEFAULT_LAYOUT_FEATURES = {
584
1394
  showTopBar: false,
@@ -639,7 +1449,7 @@ function createDemoLayoutOptions(partial = {}) {
639
1449
  }
640
1450
 
641
1451
  // src/status-bar.ts
642
- import { getLocale, setLocale, t as t6 } from "@coderyo/i18n";
1452
+ import { getLocale as getLocale2, setLocale as setLocale2, t as t5 } from "@coderyo/i18n";
643
1453
  function fmt(n, digits = 2) {
644
1454
  if (n == null || Number.isNaN(n)) return "\u2014";
645
1455
  return n.toLocaleString(void 0, { maximumFractionDigits: digits });
@@ -656,7 +1466,7 @@ function mountStatusBar(parent, opts = {}) {
656
1466
  const localeWrap = document.createElement("label");
657
1467
  localeWrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-left:auto;";
658
1468
  const localeLabel = document.createElement("span");
659
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
1469
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
660
1470
  const localeSelect = document.createElement("select");
661
1471
  localeSelect.style.cssText = "background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;font-size:11px;padding:2px 4px;";
662
1472
  for (const loc of ["zh-TW", "en"]) {
@@ -665,11 +1475,11 @@ function mountStatusBar(parent, opts = {}) {
665
1475
  opt.textContent = loc;
666
1476
  localeSelect.appendChild(opt);
667
1477
  }
668
- localeSelect.value = opts.locale ?? getLocale();
1478
+ localeSelect.value = opts.locale ?? getLocale2();
669
1479
  localeSelect.onchange = () => {
670
- setLocale(localeSelect.value);
1480
+ setLocale2(localeSelect.value);
671
1481
  opts.onLocaleChange?.(localeSelect.value);
672
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
1482
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
673
1483
  render(opts);
674
1484
  };
675
1485
  localeWrap.append(localeLabel, localeSelect);
@@ -677,14 +1487,14 @@ function mountStatusBar(parent, opts = {}) {
677
1487
  parent.appendChild(bar);
678
1488
  const render = (state) => {
679
1489
  const merged = { ...opts, ...state };
680
- conn.textContent = `${t6("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
1490
+ conn.textContent = `${t5("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
681
1491
  const parts = [merged.symbol, merged.interval].filter(Boolean);
682
1492
  sym.textContent = parts.length ? parts.join(" \xB7 ") : "";
683
1493
  const o = merged.ohlcv;
684
1494
  if (o && (o.o != null || o.c != null)) {
685
1495
  ohlcv.textContent = `O ${fmt(o.o)} H ${fmt(o.h)} L ${fmt(o.l)} C ${fmt(o.c)} V ${fmt(o.v, 0)}`;
686
1496
  } else {
687
- ohlcv.textContent = t6("status.ohlcvHint", "\u79FB\u52D5\u5341\u5B57\u7DDA\u67E5\u770B OHLCV");
1497
+ ohlcv.textContent = t5("status.ohlcvHint", "\u79FB\u52D5\u5341\u5B57\u7DDA\u67E5\u770B OHLCV");
688
1498
  }
689
1499
  };
690
1500
  render(opts);
@@ -698,7 +1508,7 @@ function mountStatusBar(parent, opts = {}) {
698
1508
  }
699
1509
 
700
1510
  // src/shortcuts-modal.ts
701
- import { t as t7 } from "@coderyo/i18n";
1511
+ import { t as t6 } from "@coderyo/i18n";
702
1512
  var SHORTCUTS = [
703
1513
  { key: "\u2196 / Esc", desc: "\u6E38\u6A19\uFF08\u9078\u53D6/\u7DE8\u8F2F\u7E6A\u5716\uFF09" },
704
1514
  { key: "Delete", desc: "\u522A\u9664\u9078\u4E2D\u7E6A\u5716" },
@@ -727,7 +1537,7 @@ function openShortcutsModal() {
727
1537
  const box = document.createElement("div");
728
1538
  box.style.cssText = "min-width:320px;max-width:90vw;padding:16px 20px;background:#161b22;border:1px solid #30363d;border-radius:8px;color:#e6edf3;";
729
1539
  const h = document.createElement("h2");
730
- h.textContent = t7("shortcuts.title", "\u5FEB\u6377\u9375");
1540
+ h.textContent = t6("shortcuts.title", "\u5FEB\u6377\u9375");
731
1541
  h.style.cssText = "font-size:16px;margin:0 0 12px;";
732
1542
  const list = document.createElement("div");
733
1543
  list.style.cssText = "font-size:13px;line-height:1.8;";
@@ -738,7 +1548,7 @@ function openShortcutsModal() {
738
1548
  }
739
1549
  const closeBtn = document.createElement("button");
740
1550
  closeBtn.type = "button";
741
- closeBtn.textContent = t7("shortcuts.close", "\u95DC\u9589");
1551
+ closeBtn.textContent = t6("shortcuts.close", "\u95DC\u9589");
742
1552
  closeBtn.style.cssText = "margin-top:14px;padding:6px 14px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;";
743
1553
  const close = () => backdrop.remove();
744
1554
  closeBtn.onclick = close;
@@ -762,7 +1572,7 @@ var DRAWING_TOOLS = [
762
1572
  ];
763
1573
  var MOBILE_MQ = "(max-width: 768px)";
764
1574
  function mountToolButtons(parent, activeTool, onSelect, layout) {
765
- 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;";
766
1576
  const activeStyle = btnStyle.replace("#21262d", "#388bfd").replace("#e6edf3", "#fff");
767
1577
  const buttons = /* @__PURE__ */ new Map();
768
1578
  for (const tool of DRAWING_TOOLS) {
@@ -785,17 +1595,26 @@ function mountToolButtons(parent, activeTool, onSelect, layout) {
785
1595
  }
786
1596
  function mountChartLayout(root, opts = {}) {
787
1597
  let layoutFeatures = resolveLayoutFeatures(opts);
1598
+ const layoutId = opts.layoutId ?? "default";
1599
+ root.replaceChildren();
788
1600
  root.style.display = "flex";
789
1601
  root.style.flexDirection = "column";
790
- root.style.height = "100%";
791
- root.style.width = "100%";
1602
+ root.style.height = root.style.height || "100%";
1603
+ root.style.width = root.style.width || "100%";
792
1604
  root.style.minWidth = "0";
793
1605
  root.style.boxSizing = "border-box";
794
- root.style.overflow = "visible";
795
- root.style.background = "#0d1117";
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) => {
1612
+ root.style.background = theme === "dark" ? "#0d1117" : "#f6f8fa";
1613
+ });
796
1614
  let activeTool = opts.activeDrawingTool ?? "cursor";
797
1615
  let setActiveDesktop = null;
798
1616
  let setActiveMobile = null;
1617
+ let lastSelectedDrawing = null;
799
1618
  const setActiveDrawingTool = (tool) => {
800
1619
  activeTool = tool;
801
1620
  setActiveDesktop?.(tool);
@@ -805,89 +1624,225 @@ function mountChartLayout(root, opts = {}) {
805
1624
  setActiveDrawingTool(tool);
806
1625
  opts.onDrawingToolSelect?.(tool);
807
1626
  };
808
- const headerSlot = document.createElement("div");
809
- headerSlot.className = "tv-layout-header";
810
- headerSlot.style.cssText = "display:none;flex-shrink:0;width:100%;min-width:0;overflow:visible;position:relative;z-index:30;";
811
- const body = document.createElement("div");
812
- body.className = "tv-layout-body";
813
- body.style.cssText = "display:flex;flex:1;min-height:0;min-width:0;overflow:hidden;position:relative;z-index:0;";
814
- const leftAside = document.createElement("aside");
815
- 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;";
816
- const bottomBar = document.createElement("div");
817
- bottomBar.style.cssText = "display:none;flex-shrink:0;gap:6px;padding:6px 8px;border-top:1px solid #30363d;background:#161b22;overflow-x:auto;";
818
- const chartColumn = document.createElement("div");
819
- 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;";
820
1640
  const chartHost = document.createElement("div");
821
- chartHost.style.cssText = "flex:1;min-height:0;width:100%;height:100%;position:relative;overflow:hidden;";
822
- chartColumn.appendChild(chartHost);
823
- const indicatorHost = mountIndicatorPaneHost(chartColumn);
824
- const statusBar = mountStatusBar(chartColumn, opts.statusBar ?? {});
825
- body.appendChild(chartColumn);
826
- 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, {
827
1653
  onStyleChange: opts.onDrawingStyleChange
828
1654
  });
829
- opts.onDrawingSelectionBind?.(propertiesPanel.bind);
830
- root.appendChild(headerSlot);
831
- root.appendChild(body);
832
- root.appendChild(bottomBar);
833
- 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;
834
1701
  let setActiveInterval = () => {
835
1702
  };
836
- const crosshairLegend = mountCrosshairLegend(chartHost, { symbol: opts.initialSymbol });
1703
+ const crosshairLegend = mountCrosshairLegend(
1704
+ layerCompositorManaged ? void 0 : chartMain,
1705
+ { symbol: opts.initialSymbol }
1706
+ );
837
1707
  let detachContextMenu = () => {
838
1708
  };
839
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
+ };
840
1734
  const mountLeftToolbar = () => {
841
- if (leftAside.parentElement) return;
842
- const desktopTools = mountToolButtons(leftAside, activeTool, onToolSelect, "column");
1735
+ leftToolbar.replaceChildren();
1736
+ const desktopTools = mountToolButtons(leftToolbar, activeTool, onToolSelect, "column");
843
1737
  setActiveDesktop = desktopTools.setActive;
844
- body.insertBefore(leftAside, chartColumn);
845
- bottomBar.style.flexDirection = "row";
846
- const mobileTools = mountToolButtons(bottomBar, activeTool, onToolSelect, "row");
1738
+ mountToolbarActions(leftToolbar, true);
1739
+ bottomToolbar.replaceChildren();
1740
+ const mobileTools = mountToolButtons(bottomToolbar, activeTool, onToolSelect, "row");
847
1741
  setActiveMobile = mobileTools.setActive;
848
- const mq = window.matchMedia(MOBILE_MQ);
849
- const applyLayout = () => {
850
- const mobile = mq.matches;
851
- leftAside.style.display = mobile ? "none" : "flex";
852
- const showBottom = layoutFeatures.showBottomToolbar !== false;
853
- bottomBar.style.display = mobile && showBottom ? "flex" : "none";
854
- };
855
- mq.addEventListener("change", applyLayout);
856
- applyLayout();
1742
+ mountToolbarActions(bottomToolbar, false);
857
1743
  };
858
- const unmountLeftToolbar = () => {
859
- leftAside.remove();
860
- bottomBar.innerHTML = "";
861
- setActiveDesktop = null;
862
- 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
+ }
863
1787
  };
864
1788
  const applyLayoutFeatures = () => {
865
1789
  const f = layoutFeatures;
866
1790
  if (f.showTopBar) {
867
- headerSlot.replaceChildren();
868
- headerSlot.style.display = "";
1791
+ topBarHost.replaceChildren();
869
1792
  const mounted = mountTopBar(
870
- headerSlot,
1793
+ topBarHost,
871
1794
  Object.assign(opts, {
872
1795
  symbolInput: f.symbolInput,
873
- showSettings: f.showSettings
1796
+ showSettings: f.showSettings,
1797
+ themeProvider: opts.themeProvider ?? themeProvider,
1798
+ i18n: opts.i18n ?? i18nProvider,
1799
+ onThemeChange: (theme) => {
1800
+ opts.onThemeChange?.(theme);
1801
+ if (!opts.onThemeChange) opts.onThemeToggle?.();
1802
+ }
874
1803
  })
875
1804
  );
876
1805
  topBar = mounted.el;
877
1806
  setActiveInterval = mounted.setActiveInterval;
878
1807
  } else {
879
- headerSlot.replaceChildren();
880
- headerSlot.style.display = "none";
881
- topBar = headerSlot;
882
- }
883
- if (f.showLeftToolbar) mountLeftToolbar();
884
- else unmountLeftToolbar();
885
- crosshairLegend.el.style.display = f.showCrosshairLegend ? "" : "none";
886
- statusBar.el.style.display = f.showStatusBar ? "" : "none";
887
- propertiesPanel.el.style.display = f.showPropertiesPanel ? "" : "none";
888
- detachContextMenu();
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
+ }
1843
+ detachContextMenu();
889
1844
  if (f.showContextMenu) {
890
- detachContextMenu = attachChartContextMenu(chartHost, {
1845
+ detachContextMenu = attachChartContextMenu(chartMain, {
891
1846
  actions: opts.contextMenuActions
892
1847
  });
893
1848
  }
@@ -895,10 +1850,36 @@ function mountChartLayout(root, opts = {}) {
895
1850
  bindShortcutsModal();
896
1851
  shortcutsBound = true;
897
1852
  }
1853
+ applyPropertiesPanelLayout(lastSelectedDrawing);
1854
+ };
1855
+ const handleDrawingSelection = (drawing) => {
1856
+ lastSelectedDrawing = drawing;
1857
+ propertiesPanel.bind(drawing);
1858
+ applyPropertiesPanelLayout(drawing);
898
1859
  };
1860
+ opts.onDrawingSelectionBind?.(propertiesPanel.bind);
899
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;
900
1876
  return {
901
- chartHost,
1877
+ layoutRoot: layoutShell.root,
1878
+ layoutGrid: layoutShell.grid,
1879
+ chartHost: chartMain,
1880
+ chartMain,
1881
+ chartVolume,
1882
+ drawingOverlay,
902
1883
  indicatorHost,
903
1884
  topBar,
904
1885
  setActiveInterval,
@@ -907,16 +1888,1880 @@ function mountChartLayout(root, opts = {}) {
907
1888
  detachContextMenu,
908
1889
  setActiveDrawingTool,
909
1890
  propertiesPanel,
1891
+ handleDrawingSelection,
1892
+ syncCompositorShellVisibility,
1893
+ bindLayerCompositorController,
910
1894
  setLayoutFeatures: (patch) => {
911
1895
  layoutFeatures = mergeLayoutFeatures(layoutFeatures, patch);
912
1896
  applyLayoutFeatures();
1897
+ syncCompositorShellVisibilityIfBound();
913
1898
  },
914
- 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
+ }
915
1917
  };
916
1918
  }
917
1919
 
918
- // src/drawing-context-menu.ts
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)
1943
+ };
1944
+ }
1945
+ const drawing = layers.find((l) => l.type === "overlay.drawing" && l.pageId === pageId);
1946
+ if (drawing) {
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;
1955
+ }
1956
+ }
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;
1998
+ }
1999
+
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
2062
+ };
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
2076
+ };
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
+ });
2094
+ }
2095
+
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
919
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";
920
3765
  function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
921
3766
  const menu = document.createElement("div");
922
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;";
@@ -938,16 +3783,16 @@ function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
938
3783
  };
939
3784
  if (drawing) {
940
3785
  const locked = Boolean(drawing.meta?.locked);
941
- add(t8("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
942
- add(t8("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
3786
+ add(t10("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
3787
+ add(t10("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
943
3788
  add(
944
- locked ? t8("drawing.ctx.unlock", "\u89E3\u9396") : t8("drawing.ctx.lock", "\u9396\u5B9A"),
3789
+ locked ? t10("drawing.ctx.unlock", "\u89E3\u9396") : t10("drawing.ctx.lock", "\u9396\u5B9A"),
945
3790
  handlers.onToggleLock
946
3791
  );
947
3792
  if (drawing.type === "text") {
948
- add(t8("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
3793
+ add(t10("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
949
3794
  }
950
- add(t8("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
3795
+ add(t10("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
951
3796
  }
952
3797
  document.body.appendChild(menu);
953
3798
  const close = () => {
@@ -999,7 +3844,7 @@ import {
999
3844
  lineNumbers,
1000
3845
  placeholder
1001
3846
  } from "@codemirror/view";
1002
- import { t as t9 } from "@coderyo/i18n";
3847
+ import { t as t11 } from "@coderyo/i18n";
1003
3848
 
1004
3849
  // src/pine-language.ts
1005
3850
  import { StreamLanguage } from "@codemirror/language";
@@ -1059,10 +3904,10 @@ function createPineLinter(onStatus) {
1059
3904
  const src = view.state.doc.toString();
1060
3905
  const result = compilePineLite(src);
1061
3906
  if (result.ok) {
1062
- onStatus?.(t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
3907
+ onStatus?.(t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1063
3908
  return [];
1064
3909
  }
1065
- onStatus?.(result.errors[0] ?? t9("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
3910
+ onStatus?.(result.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
1066
3911
  return pineDiagnosticsToCm(src, result.diagnostics ?? []);
1067
3912
  });
1068
3913
  }
@@ -1091,16 +3936,16 @@ function mountPineEditorPanel(parent, opts = {}) {
1091
3936
  wrap.open = true;
1092
3937
  wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
1093
3938
  const summary = document.createElement("summary");
1094
- summary.textContent = t9("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
3939
+ summary.textContent = t11("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
1095
3940
  summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
1096
3941
  const toolbar = document.createElement("div");
1097
3942
  toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
1098
3943
  const status = document.createElement("span");
1099
3944
  status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
1100
- status.textContent = t9("pine.status.idle", "\u5C31\u7DD2");
3945
+ status.textContent = t11("pine.status.idle", "\u5C31\u7DD2");
1101
3946
  const applyBtn = document.createElement("button");
1102
3947
  applyBtn.type = "button";
1103
- applyBtn.textContent = t9("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
3948
+ applyBtn.textContent = t11("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1104
3949
  applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
1105
3950
  const host = document.createElement("div");
1106
3951
  host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
@@ -1115,10 +3960,10 @@ function mountPineEditorPanel(parent, opts = {}) {
1115
3960
  savePineScriptPreference(src);
1116
3961
  if (r.ok) {
1117
3962
  status.style.color = "#3fb950";
1118
- status.textContent = t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
3963
+ status.textContent = t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1119
3964
  } else {
1120
3965
  status.style.color = "#f85149";
1121
- status.textContent = r.errors[0] ?? t9("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
3966
+ status.textContent = r.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
1122
3967
  }
1123
3968
  };
1124
3969
  const extensions = [
@@ -1136,7 +3981,7 @@ function mountPineEditorPanel(parent, opts = {}) {
1136
3981
  status.textContent = msg;
1137
3982
  }),
1138
3983
  keymap.of([...defaultKeymap, ...historyKeymap]),
1139
- placeholder(t9("pine.placeholder", "// plot(sma(close, 20))")),
3984
+ placeholder(t11("pine.placeholder", "// plot(sma(close, 20))")),
1140
3985
  EditorView.updateListener.of((u) => {
1141
3986
  if (!u.docChanged) return;
1142
3987
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -1165,35 +4010,104 @@ function mountPineEditorPanel(parent, opts = {}) {
1165
4010
  };
1166
4011
  }
1167
4012
  export {
4013
+ BUILTIN_PRESETS,
4014
+ CHART_PANE_LAYER_TYPES,
1168
4015
  DEFAULT_LAYOUT_FEATURES,
4016
+ DEFAULT_LAYOUT_SCHEMA,
4017
+ DEFAULT_SYNC_TIME_SCALE_GROUP,
1169
4018
  GRID_SETTING_KEY,
4019
+ LAYER_PRESET_VERSION,
4020
+ LAYOUT_SCHEMA_VERSION,
4021
+ LayerController,
4022
+ OVERLAY_LAYER_TYPES,
1170
4023
  PINE_SCRIPT_STORAGE_KEY,
1171
4024
  RETURN_CURSOR_KEY,
4025
+ THEME_STORAGE_KEY,
4026
+ VENDOR_COMPACT_PRESET,
4027
+ VENDOR_DEFAULT_PRESET,
4028
+ VENDOR_DUAL_SYNC_PRESET,
4029
+ applyCompositorShellFeatures,
4030
+ applyThemeToDocument,
1172
4031
  attachChartContextMenu,
4032
+ attachLayerEditor,
4033
+ bindLayerTimeScaleSync,
1173
4034
  bindShortcutsModal,
4035
+ clampBBox,
4036
+ clampFrame,
4037
+ cloneLayoutPreset,
4038
+ cloneLayoutSchema,
4039
+ createCompositorShell,
1174
4040
  createDemoLayoutOptions,
4041
+ createI18nProvider,
4042
+ createLayoutGrid,
4043
+ createSymbolSearchDialog,
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,
1175
4060
  loadIndicatorConfig,
4061
+ loadLayoutSchema,
1176
4062
  loadPineScriptPreference,
4063
+ loadPreset,
1177
4064
  loadReturnToCursorPreference,
1178
4065
  loadShowGridPreference,
4066
+ loadTheme,
1179
4067
  mergeLayoutFeatures,
4068
+ mergeLayoutPreset,
1180
4069
  mountChartLayout,
1181
4070
  mountCodeSnippetPanel,
1182
4071
  mountCrosshairLegend,
1183
4072
  mountDrawingPropertiesPanel,
4073
+ mountLayerCompositor,
4074
+ mountLayerPanel,
4075
+ mountLogoSlot,
4076
+ mountPageNavigator,
1184
4077
  mountPineEditorPanel,
1185
4078
  mountSettingsPanel as mountSettingsMenu,
1186
4079
  mountSettingsPanel,
1187
4080
  mountStatusBar,
1188
4081
  mountSymbolSearch,
4082
+ mountSymbolSearchDialogTrigger,
4083
+ mountThemeToggle,
1189
4084
  mountTopBar,
4085
+ moveLayerFrames,
4086
+ normalizeLayoutPreset,
4087
+ normalizeLayoutSchema,
1190
4088
  openDrawingContextMenu,
1191
4089
  openShortcutsModal,
4090
+ paneIdFromWidgetKey,
1192
4091
  pineLanguage,
4092
+ presetStorageKey,
4093
+ resizeGroupFrames,
1193
4094
  resolveLayoutFeatures,
4095
+ resolveLayoutSchema,
4096
+ resolvePaneLayerIds,
4097
+ resolvePreset,
1194
4098
  saveIndicatorConfig,
4099
+ saveLayoutSchema,
1195
4100
  savePineScriptPreference,
4101
+ savePreset,
1196
4102
  saveReturnToCursorPreference,
1197
- saveShowGridPreference
4103
+ saveShowGridPreference,
4104
+ saveTheme,
4105
+ splitLegacyChartHost,
4106
+ syncAllOverlayLayersToMain,
4107
+ syncCompositorShellVisibilityFromFeatures,
4108
+ syncCrosshairLegendToMain,
4109
+ syncOverlayLayersToMain,
4110
+ upgradeIndicatorHostType,
4111
+ widgetKeyForChartPaneType
1198
4112
  };
1199
4113
  //# sourceMappingURL=index.js.map