@coderyo/ui-shell 1.0.1 → 1.0.3

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,21 @@ 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 { DEFAULT_INDICATOR_CONFIG } from "@coderyo/indicators";
53
77
  import { t } from "@coderyo/i18n";
54
78
  function mountSettingsPanel(parent, opts = {}) {
55
79
  let open = false;
56
80
  let tab = "chart";
57
81
  let showGrid = opts.showGrid ?? loadShowGridPreference();
58
82
  let returnToCursor = opts.returnToCursorAfterDraw ?? loadReturnToCursorPreference();
59
- let indicatorConfig = { ...opts.indicatorConfig ?? DEFAULT_INDICATOR_CONFIG2 };
83
+ let indicatorConfig = { ...opts.indicatorConfig ?? DEFAULT_INDICATOR_CONFIG };
60
84
  const wrap = document.createElement("div");
61
85
  wrap.style.cssText = "position:relative;";
62
86
  const btn = document.createElement("button");
@@ -101,6 +125,17 @@ function mountSettingsPanel(parent, opts = {}) {
101
125
  row.append(input, document.createTextNode(label));
102
126
  return row;
103
127
  };
128
+ const actionButton = (label, onClick) => {
129
+ const btn2 = document.createElement("button");
130
+ btn2.type = "button";
131
+ btn2.textContent = label;
132
+ btn2.style.cssText = "display:block;width:100%;margin-top:10px;padding:6px 10px;border-radius:4px;border:1px solid #f85149;background:#21262d;color:#f85149;cursor:pointer;font-size:12px;";
133
+ btn2.onclick = (e) => {
134
+ e.stopPropagation();
135
+ onClick();
136
+ };
137
+ return btn2;
138
+ };
104
139
  const numberField = (label, value, onChange) => {
105
140
  const row = document.createElement("label");
106
141
  row.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;";
@@ -131,6 +166,14 @@ function mountSettingsPanel(parent, opts = {}) {
131
166
  opts.onReturnToCursorChange?.(v);
132
167
  })
133
168
  );
169
+ if (opts.onClearAllDrawings) {
170
+ content.appendChild(
171
+ actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
172
+ opts.onClearAllDrawings?.();
173
+ renderContent();
174
+ })
175
+ );
176
+ }
134
177
  } else {
135
178
  content.appendChild(
136
179
  checkbox(t("settings.ind.macd", "MACD \u7A97\u683C"), indicatorConfig.showMacd, (v) => {
@@ -222,6 +265,15 @@ function mountSettingsPanel(parent, opts = {}) {
222
265
  opts.onIndicatorConfigChange?.(indicatorConfig);
223
266
  })
224
267
  );
268
+ if (opts.onClearAllIndicators) {
269
+ content.appendChild(
270
+ actionButton(t("settings.ind.clearAll", "\u6E05\u7A7A\u6240\u6709\u6307\u6A19"), () => {
271
+ opts.onClearAllIndicators?.();
272
+ if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
273
+ renderContent();
274
+ })
275
+ );
276
+ }
225
277
  }
226
278
  };
227
279
  renderTabs();
@@ -230,6 +282,10 @@ function mountSettingsPanel(parent, opts = {}) {
230
282
  btn.onclick = (e) => {
231
283
  e.stopPropagation();
232
284
  open = !open;
285
+ if (open) {
286
+ if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
287
+ renderContent();
288
+ }
233
289
  panel.style.display = open ? "block" : "none";
234
290
  };
235
291
  const close = () => {
@@ -243,6 +299,108 @@ function mountSettingsPanel(parent, opts = {}) {
243
299
  return wrap;
244
300
  }
245
301
 
302
+ // src/symbol-search-dialog.ts
303
+ function createSymbolSearchDialog(opts) {
304
+ const i18n = opts.i18n;
305
+ const backdrop = document.createElement("div");
306
+ backdrop.className = "tv-symbol-dialog-backdrop";
307
+ 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;";
308
+ const panel = document.createElement("div");
309
+ panel.className = "tv-symbol-dialog";
310
+ 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);";
311
+ const title = document.createElement("div");
312
+ title.textContent = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
313
+ title.style.cssText = "font-size:13px;font-weight:600;color:var(--tv-fg,#e6edf3);margin-bottom:8px;";
314
+ const input = document.createElement("input");
315
+ input.type = "search";
316
+ input.placeholder = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
317
+ input.value = opts.initialSymbol ?? "";
318
+ 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;";
319
+ const list = document.createElement("div");
320
+ list.style.cssText = "margin-top:8px;max-height:240px;overflow:auto;";
321
+ panel.append(title, input, list);
322
+ backdrop.appendChild(panel);
323
+ let timer;
324
+ let mounted = false;
325
+ const renderHits = (hits) => {
326
+ list.replaceChildren();
327
+ if (hits.length === 0) {
328
+ const empty = document.createElement("div");
329
+ empty.textContent = "\u2014";
330
+ empty.style.cssText = "padding:8px;color:var(--tv-muted,#8b949e);font-size:12px;";
331
+ list.appendChild(empty);
332
+ return;
333
+ }
334
+ for (const hit of hits) {
335
+ const row = document.createElement("button");
336
+ row.type = "button";
337
+ row.textContent = hit.exchange ? `${hit.symbol} \xB7 ${hit.exchange}` : hit.symbol;
338
+ 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;";
339
+ row.onmouseenter = () => {
340
+ row.style.background = "var(--tv-btn-bg,#21262d)";
341
+ };
342
+ row.onmouseleave = () => {
343
+ row.style.background = "transparent";
344
+ };
345
+ row.onclick = () => {
346
+ opts.onSelect(hit.symbol);
347
+ close();
348
+ };
349
+ list.appendChild(row);
350
+ }
351
+ };
352
+ const runSearch = () => {
353
+ const q = input.value.trim();
354
+ if (q.length < 1) {
355
+ list.replaceChildren();
356
+ return;
357
+ }
358
+ void opts.onSearch(q).then(renderHits).catch(() => list.replaceChildren());
359
+ };
360
+ input.addEventListener("input", () => {
361
+ clearTimeout(timer);
362
+ timer = setTimeout(runSearch, 200);
363
+ });
364
+ input.addEventListener("keydown", (e) => {
365
+ if (e.key === "Escape") close();
366
+ if (e.key === "Enter") runSearch();
367
+ });
368
+ backdrop.addEventListener("click", (e) => {
369
+ if (e.target === backdrop) close();
370
+ });
371
+ const open = () => {
372
+ if (!mounted) {
373
+ document.body.appendChild(backdrop);
374
+ mounted = true;
375
+ }
376
+ backdrop.style.display = "flex";
377
+ input.focus();
378
+ input.select();
379
+ runSearch();
380
+ };
381
+ const close = () => {
382
+ backdrop.style.display = "none";
383
+ };
384
+ const destroy = () => {
385
+ clearTimeout(timer);
386
+ backdrop.remove();
387
+ mounted = false;
388
+ };
389
+ return { open, close, destroy };
390
+ }
391
+ function mountSymbolSearchDialogTrigger(parent, dialog, opts) {
392
+ const wrap = document.createElement("div");
393
+ wrap.style.cssText = "display:flex;align-items:center;margin-right:8px;flex-shrink:0;";
394
+ const btn = document.createElement("button");
395
+ btn.type = "button";
396
+ btn.textContent = opts.initialSymbol ?? (opts.i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1");
397
+ 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;";
398
+ btn.onclick = () => dialog.open();
399
+ wrap.appendChild(btn);
400
+ parent.appendChild(wrap);
401
+ return wrap;
402
+ }
403
+
246
404
  // src/symbol-search.ts
247
405
  import { t as t2 } from "@coderyo/i18n";
248
406
  function mountSymbolSearch(parent, opts) {
@@ -306,13 +464,32 @@ function mountSymbolSearch(parent, opts) {
306
464
  return wrap;
307
465
  }
308
466
 
467
+ // src/theme-toggle.ts
468
+ function mountThemeToggle(parent, opts) {
469
+ const i18n = opts.i18n;
470
+ const btn = document.createElement("button");
471
+ btn.type = "button";
472
+ btn.className = "tv-theme-toggle";
473
+ 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;";
474
+ const paint = (theme) => {
475
+ btn.textContent = theme === "dark" ? i18n?.t("theme.dark", "\u6DF1\u8272") ?? "\u6DF1\u8272" : i18n?.t("theme.light", "\u6DFA\u8272") ?? "\u6DFA\u8272";
476
+ };
477
+ btn.onclick = () => {
478
+ const next = opts.themeProvider.toggle();
479
+ opts.onThemeChange?.(next);
480
+ };
481
+ opts.themeProvider.subscribe((theme) => paint(theme));
482
+ parent.appendChild(btn);
483
+ return btn;
484
+ }
485
+
309
486
  // src/top-bar.ts
310
487
  function mountManualSymbolInput(parent, opts) {
311
488
  const wrap = document.createElement("div");
312
489
  wrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-right:8px;";
313
490
  const input = document.createElement("input");
314
491
  input.type = "text";
315
- input.placeholder = t3("symbol.search", "Symbol");
492
+ input.placeholder = opts.i18n?.t("symbol.search", "Symbol") ?? "Symbol";
316
493
  input.value = opts.initialSymbol ?? "";
317
494
  input.style.cssText = "width:140px;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 8px;font-size:12px;";
318
495
  const apply = () => {
@@ -332,9 +509,14 @@ function mountManualSymbolInput(parent, opts) {
332
509
  parent.appendChild(wrap);
333
510
  }
334
511
  function mountTopBar(parent, opts = {}) {
512
+ const i18n = opts.i18n;
513
+ const tKey = (key, fallback) => i18n?.t(key, fallback) ?? fallback;
335
514
  const bar = document.createElement("div");
336
515
  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;";
516
+ 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;";
517
+ if (opts.logo !== false) {
518
+ mountLogoSlot(bar, opts.logo ?? { label: "TradView" });
519
+ }
338
520
  const symbolMode = opts.symbolInput ?? "manual";
339
521
  if (symbolMode === "search" && opts.onSymbolSearch && opts.onSymbolSelect) {
340
522
  mountSymbolSearch(bar, {
@@ -342,17 +524,32 @@ function mountTopBar(parent, opts = {}) {
342
524
  onSearch: opts.onSymbolSearch,
343
525
  onSelect: opts.onSymbolSelect
344
526
  });
527
+ } else if (symbolMode === "dialog" && opts.onSymbolSearch && opts.onSymbolSelect) {
528
+ const dialog = createSymbolSearchDialog({
529
+ initialSymbol: opts.initialSymbol,
530
+ onSearch: opts.onSymbolSearch,
531
+ onSelect: opts.onSymbolSelect,
532
+ i18n
533
+ });
534
+ mountSymbolSearchDialogTrigger(bar, dialog, {
535
+ initialSymbol: opts.initialSymbol,
536
+ i18n
537
+ });
345
538
  } else if (symbolMode === "manual" && opts.onSymbolSelect) {
346
539
  mountManualSymbolInput(bar, {
347
540
  initialSymbol: opts.initialSymbol,
348
- onSymbolSelect: opts.onSymbolSelect
541
+ onSymbolSelect: opts.onSymbolSelect,
542
+ i18n
349
543
  });
350
544
  }
351
545
  const intervals = opts.intervals ?? DEFAULT_INTERVALS;
352
- const btnStyle = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;";
546
+ const btnStyle = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;flex-shrink:0;";
353
547
  const btnActiveStyle = btnStyle.replace("#21262d", "#388bfd").replace("#e6edf3", "#fff");
354
548
  const intervalButtons = /* @__PURE__ */ new Map();
355
549
  let activeInterval = opts.activeInterval ?? intervals[0];
550
+ const intervalRow = document.createElement("div");
551
+ intervalRow.className = "tv-topbar-intervals";
552
+ intervalRow.style.cssText = "display:flex;gap:8px;flex-wrap:nowrap;align-items:center;flex-shrink:0;";
356
553
  const paintIntervalButtons = () => {
357
554
  for (const [iv, btn] of intervalButtons) {
358
555
  btn.style.cssText = iv === activeInterval ? btnActiveStyle : btnStyle;
@@ -361,7 +558,7 @@ function mountTopBar(parent, opts = {}) {
361
558
  for (const iv of intervals) {
362
559
  const btn = document.createElement("button");
363
560
  btn.type = "button";
364
- btn.textContent = t3(`interval.${iv}`, iv);
561
+ btn.textContent = tKey(`interval.${iv}`, iv);
365
562
  btn.style.cssText = btnStyle;
366
563
  btn.onclick = () => {
367
564
  if (activeInterval === iv) return;
@@ -370,9 +567,10 @@ function mountTopBar(parent, opts = {}) {
370
567
  opts.onIntervalChange?.(iv);
371
568
  };
372
569
  intervalButtons.set(iv, btn);
373
- bar.appendChild(btn);
570
+ intervalRow.appendChild(btn);
374
571
  }
375
572
  paintIntervalButtons();
573
+ bar.appendChild(intervalRow);
376
574
  const spacer = document.createElement("div");
377
575
  spacer.style.flex = "1";
378
576
  bar.appendChild(spacer);
@@ -384,11 +582,19 @@ function mountTopBar(parent, opts = {}) {
384
582
  b.onclick = () => fn?.();
385
583
  return b;
386
584
  };
387
- bar.appendChild(mkBtn(t3("theme.dark", "\u4E3B\u984C"), opts.onThemeToggle));
585
+ if (opts.themeProvider) {
586
+ mountThemeToggle(bar, {
587
+ themeProvider: opts.themeProvider,
588
+ i18n,
589
+ onThemeChange: opts.onThemeChange
590
+ });
591
+ } else {
592
+ bar.appendChild(mkBtn(tKey("theme.dark", "\u4E3B\u984C"), opts.onThemeToggle));
593
+ }
388
594
  if (opts.showSettings && opts.settings) mountSettingsPanel(bar, opts.settings);
389
595
  bar.appendChild(mkBtn("\u26F6", opts.onFullscreen));
390
596
  bar.appendChild(mkBtn("\u{1F4F7}", opts.onScreenshot));
391
- parent.prepend(bar);
597
+ parent.appendChild(bar);
392
598
  return {
393
599
  el: bar,
394
600
  setActiveInterval: (interval) => {
@@ -399,8 +605,116 @@ function mountTopBar(parent, opts = {}) {
399
605
  };
400
606
  }
401
607
 
608
+ // src/theme-provider.ts
609
+ var THEME_STORAGE_KEY = "tradview:theme";
610
+ var THEME_VARS = {
611
+ dark: {
612
+ "--tv-bg": "#0d1117",
613
+ "--tv-surface": "#161b22",
614
+ "--tv-border": "#30363d",
615
+ "--tv-fg": "#e6edf3",
616
+ "--tv-muted": "#8b949e",
617
+ "--tv-accent": "#388bfd",
618
+ "--tv-btn-bg": "#21262d"
619
+ },
620
+ light: {
621
+ "--tv-bg": "#ffffff",
622
+ "--tv-surface": "#f6f8fa",
623
+ "--tv-border": "#d0d7de",
624
+ "--tv-fg": "#24292f",
625
+ "--tv-muted": "#656d76",
626
+ "--tv-accent": "#0969da",
627
+ "--tv-btn-bg": "#f6f8fa"
628
+ }
629
+ };
630
+ function loadTheme() {
631
+ try {
632
+ const v = localStorage.getItem(THEME_STORAGE_KEY);
633
+ return v === "light" ? "light" : "dark";
634
+ } catch {
635
+ return "dark";
636
+ }
637
+ }
638
+ function saveTheme(theme) {
639
+ try {
640
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
641
+ } catch {
642
+ }
643
+ }
644
+ function applyThemeToDocument(theme, root = document.documentElement) {
645
+ const vars = THEME_VARS[theme];
646
+ for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
647
+ root.dataset.tradviewTheme = theme;
648
+ }
649
+ function createThemeProvider(initial) {
650
+ let theme = initial ?? loadTheme();
651
+ const listeners = /* @__PURE__ */ new Set();
652
+ const notify = () => {
653
+ applyThemeToDocument(theme);
654
+ for (const l of listeners) l(theme);
655
+ };
656
+ applyThemeToDocument(theme);
657
+ return {
658
+ getTheme: () => theme,
659
+ setTheme: (next) => {
660
+ if (next === theme) return;
661
+ theme = next;
662
+ saveTheme(theme);
663
+ notify();
664
+ },
665
+ toggle: () => {
666
+ theme = theme === "dark" ? "light" : "dark";
667
+ saveTheme(theme);
668
+ notify();
669
+ return theme;
670
+ },
671
+ subscribe: (listener) => {
672
+ listeners.add(listener);
673
+ listener(theme);
674
+ return () => listeners.delete(listener);
675
+ }
676
+ };
677
+ }
678
+
679
+ // src/i18n-provider.ts
680
+ import {
681
+ getLocale,
682
+ registerLocale,
683
+ setLocale,
684
+ t as i18nT
685
+ } from "@coderyo/i18n";
686
+ function interpolate(text, params) {
687
+ if (!params) return text;
688
+ return text.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
689
+ }
690
+ function createI18nProvider(defaultLocale = "zh-TW") {
691
+ setLocale(defaultLocale);
692
+ const listeners = /* @__PURE__ */ new Set();
693
+ return {
694
+ t(key, fallback, params) {
695
+ return interpolate(i18nT(key, fallback), params);
696
+ },
697
+ getLocale,
698
+ setLocale(locale) {
699
+ setLocale(locale);
700
+ for (const l of listeners) l(locale);
701
+ },
702
+ registerLocale(loc, dictionary) {
703
+ registerLocale(loc, dictionary);
704
+ if (getLocale() === loc) {
705
+ for (const l of listeners) l(loc);
706
+ }
707
+ },
708
+ subscribe(listener) {
709
+ listeners.add(listener);
710
+ listener(getLocale());
711
+ return () => listeners.delete(listener);
712
+ }
713
+ };
714
+ }
715
+
402
716
  // src/context-menu.ts
403
- import { t as t4 } from "@coderyo/i18n";
717
+ import { t as t3 } from "@coderyo/i18n";
404
718
  function attachChartContextMenu(chartHost, opts = {}) {
405
719
  let menu = null;
406
720
  const close = () => {
@@ -414,7 +728,7 @@ function attachChartContextMenu(chartHost, opts = {}) {
414
728
  const actions = opts.actions ?? [
415
729
  {
416
730
  id: "fit",
417
- label: t4("context.fitContent", "\u9069\u914D\u756B\u9762"),
731
+ label: t3("context.fitContent", "\u9069\u914D\u756B\u9762"),
418
732
  onClick: () => {
419
733
  }
420
734
  }
@@ -493,13 +807,13 @@ O ${fmt2(o?.o)} H ${fmt2(o?.h)} L ${fmt2(o?.l)} C ${fmt2(o?.c)}`;
493
807
  }
494
808
 
495
809
  // src/drawing-properties-panel.ts
496
- import { t as t5 } from "@coderyo/i18n";
810
+ import { t as t4 } from "@coderyo/i18n";
497
811
  function mountDrawingPropertiesPanel(parent, opts = {}) {
498
812
  const panel = document.createElement("aside");
499
813
  panel.className = "tv-drawing-props";
500
814
  panel.style.cssText = "display:none;width:220px;flex-shrink:0;border-left:1px solid #30363d;background:#161b22;padding:10px 12px;font-size:12px;color:#e6edf3;overflow:auto;";
501
815
  const title = document.createElement("div");
502
- title.textContent = t5("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
816
+ title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
503
817
  title.style.cssText = "font-weight:600;margin-bottom:10px;";
504
818
  const typeEl = document.createElement("div");
505
819
  typeEl.style.color = "#8b949e";
@@ -513,26 +827,26 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
513
827
  row.appendChild(span);
514
828
  return row;
515
829
  };
516
- const colorRow = mkRow(t5("drawing.props.color", "\u984F\u8272"));
830
+ const colorRow = mkRow(t4("drawing.props.color", "\u984F\u8272"));
517
831
  const colorInput = document.createElement("input");
518
832
  colorInput.type = "color";
519
833
  colorInput.style.cssText = "width:100%;height:28px;border:none;background:transparent;cursor:pointer;";
520
834
  colorRow.appendChild(colorInput);
521
- const widthRow = mkRow(t5("drawing.props.lineWidth", "\u7DDA\u5BEC"));
835
+ const widthRow = mkRow(t4("drawing.props.lineWidth", "\u7DDA\u5BEC"));
522
836
  const widthInput = document.createElement("input");
523
837
  widthInput.type = "range";
524
838
  widthInput.min = "1";
525
839
  widthInput.max = "6";
526
840
  widthInput.style.width = "100%";
527
841
  widthRow.appendChild(widthInput);
528
- const textRow = mkRow(t5("drawing.props.text", "\u6587\u5B57"));
842
+ const textRow = mkRow(t4("drawing.props.text", "\u6587\u5B57"));
529
843
  const textInput = document.createElement("input");
530
844
  textInput.type = "text";
531
845
  textInput.style.cssText = "padding:4px 8px;border-radius:4px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;";
532
846
  textRow.appendChild(textInput);
533
847
  const lockedHint = document.createElement("div");
534
848
  lockedHint.style.cssText = "color:#f78166;font-size:11px;margin-top:6px;display:none;";
535
- lockedHint.textContent = t5("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
849
+ lockedHint.textContent = t4("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
536
850
  panel.append(title, typeEl, colorRow, widthRow, textRow, lockedHint);
537
851
  parent.appendChild(panel);
538
852
  const emit = () => {
@@ -551,7 +865,7 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
551
865
  return;
552
866
  }
553
867
  panel.style.display = "block";
554
- typeEl.textContent = `${t5("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
868
+ typeEl.textContent = `${t4("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
555
869
  const meta = drawing.meta ?? {};
556
870
  colorInput.value = String(meta.color ?? "#58a6ff");
557
871
  widthInput.value = String(meta.lineWidth ?? 2);
@@ -635,7 +949,7 @@ function createDemoLayoutOptions(partial = {}) {
635
949
  }
636
950
 
637
951
  // src/status-bar.ts
638
- import { getLocale, setLocale, t as t6 } from "@coderyo/i18n";
952
+ import { getLocale as getLocale2, setLocale as setLocale2, t as t5 } from "@coderyo/i18n";
639
953
  function fmt(n, digits = 2) {
640
954
  if (n == null || Number.isNaN(n)) return "\u2014";
641
955
  return n.toLocaleString(void 0, { maximumFractionDigits: digits });
@@ -652,7 +966,7 @@ function mountStatusBar(parent, opts = {}) {
652
966
  const localeWrap = document.createElement("label");
653
967
  localeWrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-left:auto;";
654
968
  const localeLabel = document.createElement("span");
655
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
969
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
656
970
  const localeSelect = document.createElement("select");
657
971
  localeSelect.style.cssText = "background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;font-size:11px;padding:2px 4px;";
658
972
  for (const loc of ["zh-TW", "en"]) {
@@ -661,11 +975,11 @@ function mountStatusBar(parent, opts = {}) {
661
975
  opt.textContent = loc;
662
976
  localeSelect.appendChild(opt);
663
977
  }
664
- localeSelect.value = opts.locale ?? getLocale();
978
+ localeSelect.value = opts.locale ?? getLocale2();
665
979
  localeSelect.onchange = () => {
666
- setLocale(localeSelect.value);
980
+ setLocale2(localeSelect.value);
667
981
  opts.onLocaleChange?.(localeSelect.value);
668
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
982
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
669
983
  render(opts);
670
984
  };
671
985
  localeWrap.append(localeLabel, localeSelect);
@@ -673,14 +987,14 @@ function mountStatusBar(parent, opts = {}) {
673
987
  parent.appendChild(bar);
674
988
  const render = (state) => {
675
989
  const merged = { ...opts, ...state };
676
- conn.textContent = `${t6("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
990
+ conn.textContent = `${t5("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
677
991
  const parts = [merged.symbol, merged.interval].filter(Boolean);
678
992
  sym.textContent = parts.length ? parts.join(" \xB7 ") : "";
679
993
  const o = merged.ohlcv;
680
994
  if (o && (o.o != null || o.c != null)) {
681
995
  ohlcv.textContent = `O ${fmt(o.o)} H ${fmt(o.h)} L ${fmt(o.l)} C ${fmt(o.c)} V ${fmt(o.v, 0)}`;
682
996
  } else {
683
- ohlcv.textContent = t6("status.ohlcvHint", "\u79FB\u52D5\u5341\u5B57\u7DDA\u67E5\u770B OHLCV");
997
+ ohlcv.textContent = t5("status.ohlcvHint", "\u79FB\u52D5\u5341\u5B57\u7DDA\u67E5\u770B OHLCV");
684
998
  }
685
999
  };
686
1000
  render(opts);
@@ -694,7 +1008,7 @@ function mountStatusBar(parent, opts = {}) {
694
1008
  }
695
1009
 
696
1010
  // src/shortcuts-modal.ts
697
- import { t as t7 } from "@coderyo/i18n";
1011
+ import { t as t6 } from "@coderyo/i18n";
698
1012
  var SHORTCUTS = [
699
1013
  { key: "\u2196 / Esc", desc: "\u6E38\u6A19\uFF08\u9078\u53D6/\u7DE8\u8F2F\u7E6A\u5716\uFF09" },
700
1014
  { key: "Delete", desc: "\u522A\u9664\u9078\u4E2D\u7E6A\u5716" },
@@ -723,7 +1037,7 @@ function openShortcutsModal() {
723
1037
  const box = document.createElement("div");
724
1038
  box.style.cssText = "min-width:320px;max-width:90vw;padding:16px 20px;background:#161b22;border:1px solid #30363d;border-radius:8px;color:#e6edf3;";
725
1039
  const h = document.createElement("h2");
726
- h.textContent = t7("shortcuts.title", "\u5FEB\u6377\u9375");
1040
+ h.textContent = t6("shortcuts.title", "\u5FEB\u6377\u9375");
727
1041
  h.style.cssText = "font-size:16px;margin:0 0 12px;";
728
1042
  const list = document.createElement("div");
729
1043
  list.style.cssText = "font-size:13px;line-height:1.8;";
@@ -734,7 +1048,7 @@ function openShortcutsModal() {
734
1048
  }
735
1049
  const closeBtn = document.createElement("button");
736
1050
  closeBtn.type = "button";
737
- closeBtn.textContent = t7("shortcuts.close", "\u95DC\u9589");
1051
+ closeBtn.textContent = t6("shortcuts.close", "\u95DC\u9589");
738
1052
  closeBtn.style.cssText = "margin-top:14px;padding:6px 14px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;";
739
1053
  const close = () => backdrop.remove();
740
1054
  closeBtn.onclick = close;
@@ -784,7 +1098,15 @@ function mountChartLayout(root, opts = {}) {
784
1098
  root.style.display = "flex";
785
1099
  root.style.flexDirection = "column";
786
1100
  root.style.height = "100%";
787
- root.style.background = "#0d1117";
1101
+ root.style.width = "100%";
1102
+ root.style.minWidth = "0";
1103
+ root.style.boxSizing = "border-box";
1104
+ root.style.overflow = "visible";
1105
+ const themeProvider = opts.themeProvider ?? createThemeProvider();
1106
+ const i18nProvider = opts.i18n ?? createI18nProvider();
1107
+ themeProvider.subscribe((theme) => {
1108
+ root.style.background = theme === "dark" ? "#0d1117" : "#f6f8fa";
1109
+ });
788
1110
  let activeTool = opts.activeDrawingTool ?? "cursor";
789
1111
  let setActiveDesktop = null;
790
1112
  let setActiveMobile = null;
@@ -797,10 +1119,14 @@ function mountChartLayout(root, opts = {}) {
797
1119
  setActiveDrawingTool(tool);
798
1120
  opts.onDrawingToolSelect?.(tool);
799
1121
  };
1122
+ const headerSlot = document.createElement("div");
1123
+ headerSlot.className = "tv-layout-header";
1124
+ headerSlot.style.cssText = "display:none;flex-shrink:0;width:100%;min-width:0;overflow:visible;position:relative;z-index:30;";
800
1125
  const body = document.createElement("div");
801
- body.style.cssText = "display:flex;flex:1;min-height:0;";
1126
+ body.className = "tv-layout-body";
1127
+ body.style.cssText = "display:flex;flex:1;min-height:0;min-width:0;overflow:hidden;position:relative;z-index:0;";
802
1128
  const leftAside = document.createElement("aside");
803
- 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;z-index:20;";
1129
+ leftAside.style.cssText = "width:48px;border-right:1px solid #30363d;background:#161b22;display:flex;flex-direction:column;align-items:center;padding:8px 4px;gap:8px;flex-shrink:0;";
804
1130
  const bottomBar = document.createElement("div");
805
1131
  bottomBar.style.cssText = "display:none;flex-shrink:0;gap:6px;padding:6px 8px;border-top:1px solid #30363d;background:#161b22;overflow-x:auto;";
806
1132
  const chartColumn = document.createElement("div");
@@ -815,11 +1141,10 @@ function mountChartLayout(root, opts = {}) {
815
1141
  onStyleChange: opts.onDrawingStyleChange
816
1142
  });
817
1143
  opts.onDrawingSelectionBind?.(propertiesPanel.bind);
1144
+ root.appendChild(headerSlot);
818
1145
  root.appendChild(body);
819
1146
  root.appendChild(bottomBar);
820
- let topBar = document.createElement("div");
821
- topBar.style.display = "none";
822
- root.insertBefore(topBar, body);
1147
+ let topBar = headerSlot;
823
1148
  let setActiveInterval = () => {
824
1149
  };
825
1150
  const crosshairLegend = mountCrosshairLegend(chartHost, { symbol: opts.initialSymbol });
@@ -853,18 +1178,27 @@ function mountChartLayout(root, opts = {}) {
853
1178
  const applyLayoutFeatures = () => {
854
1179
  const f = layoutFeatures;
855
1180
  if (f.showTopBar) {
856
- topBar.remove();
1181
+ headerSlot.replaceChildren();
1182
+ headerSlot.style.display = "";
857
1183
  const mounted = mountTopBar(
858
- root,
1184
+ headerSlot,
859
1185
  Object.assign(opts, {
860
1186
  symbolInput: f.symbolInput,
861
- showSettings: f.showSettings
1187
+ showSettings: f.showSettings,
1188
+ themeProvider: opts.themeProvider ?? themeProvider,
1189
+ i18n: opts.i18n ?? i18nProvider,
1190
+ onThemeChange: (theme) => {
1191
+ opts.onThemeChange?.(theme);
1192
+ if (!opts.onThemeChange) opts.onThemeToggle?.();
1193
+ }
862
1194
  })
863
1195
  );
864
1196
  topBar = mounted.el;
865
1197
  setActiveInterval = mounted.setActiveInterval;
866
1198
  } else {
867
- topBar.style.display = "none";
1199
+ headerSlot.replaceChildren();
1200
+ headerSlot.style.display = "none";
1201
+ topBar = headerSlot;
868
1202
  }
869
1203
  if (f.showLeftToolbar) mountLeftToolbar();
870
1204
  else unmountLeftToolbar();
@@ -902,7 +1236,7 @@ function mountChartLayout(root, opts = {}) {
902
1236
  }
903
1237
 
904
1238
  // src/drawing-context-menu.ts
905
- import { t as t8 } from "@coderyo/i18n";
1239
+ import { t as t7 } from "@coderyo/i18n";
906
1240
  function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
907
1241
  const menu = document.createElement("div");
908
1242
  menu.style.cssText = "position:fixed;z-index:60;min-width:168px;padding:4px 0;background:#161b22;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #01040988;";
@@ -924,16 +1258,16 @@ function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
924
1258
  };
925
1259
  if (drawing) {
926
1260
  const locked = Boolean(drawing.meta?.locked);
927
- add(t8("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
928
- add(t8("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
1261
+ add(t7("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
1262
+ add(t7("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
929
1263
  add(
930
- locked ? t8("drawing.ctx.unlock", "\u89E3\u9396") : t8("drawing.ctx.lock", "\u9396\u5B9A"),
1264
+ locked ? t7("drawing.ctx.unlock", "\u89E3\u9396") : t7("drawing.ctx.lock", "\u9396\u5B9A"),
931
1265
  handlers.onToggleLock
932
1266
  );
933
1267
  if (drawing.type === "text") {
934
- add(t8("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
1268
+ add(t7("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
935
1269
  }
936
- add(t8("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
1270
+ add(t7("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
937
1271
  }
938
1272
  document.body.appendChild(menu);
939
1273
  const close = () => {
@@ -985,7 +1319,7 @@ import {
985
1319
  lineNumbers,
986
1320
  placeholder
987
1321
  } from "@codemirror/view";
988
- import { t as t9 } from "@coderyo/i18n";
1322
+ import { t as t8 } from "@coderyo/i18n";
989
1323
 
990
1324
  // src/pine-language.ts
991
1325
  import { StreamLanguage } from "@codemirror/language";
@@ -1045,10 +1379,10 @@ function createPineLinter(onStatus) {
1045
1379
  const src = view.state.doc.toString();
1046
1380
  const result = compilePineLite(src);
1047
1381
  if (result.ok) {
1048
- onStatus?.(t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1382
+ onStatus?.(t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1049
1383
  return [];
1050
1384
  }
1051
- onStatus?.(result.errors[0] ?? t9("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
1385
+ onStatus?.(result.errors[0] ?? t8("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
1052
1386
  return pineDiagnosticsToCm(src, result.diagnostics ?? []);
1053
1387
  });
1054
1388
  }
@@ -1077,16 +1411,16 @@ function mountPineEditorPanel(parent, opts = {}) {
1077
1411
  wrap.open = true;
1078
1412
  wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
1079
1413
  const summary = document.createElement("summary");
1080
- summary.textContent = t9("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
1414
+ summary.textContent = t8("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
1081
1415
  summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
1082
1416
  const toolbar = document.createElement("div");
1083
1417
  toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
1084
1418
  const status = document.createElement("span");
1085
1419
  status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
1086
- status.textContent = t9("pine.status.idle", "\u5C31\u7DD2");
1420
+ status.textContent = t8("pine.status.idle", "\u5C31\u7DD2");
1087
1421
  const applyBtn = document.createElement("button");
1088
1422
  applyBtn.type = "button";
1089
- applyBtn.textContent = t9("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1423
+ applyBtn.textContent = t8("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1090
1424
  applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
1091
1425
  const host = document.createElement("div");
1092
1426
  host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
@@ -1101,10 +1435,10 @@ function mountPineEditorPanel(parent, opts = {}) {
1101
1435
  savePineScriptPreference(src);
1102
1436
  if (r.ok) {
1103
1437
  status.style.color = "#3fb950";
1104
- status.textContent = t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1438
+ status.textContent = t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1105
1439
  } else {
1106
1440
  status.style.color = "#f85149";
1107
- status.textContent = r.errors[0] ?? t9("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
1441
+ status.textContent = r.errors[0] ?? t8("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
1108
1442
  }
1109
1443
  };
1110
1444
  const extensions = [
@@ -1122,7 +1456,7 @@ function mountPineEditorPanel(parent, opts = {}) {
1122
1456
  status.textContent = msg;
1123
1457
  }),
1124
1458
  keymap.of([...defaultKeymap, ...historyKeymap]),
1125
- placeholder(t9("pine.placeholder", "// plot(sma(close, 20))")),
1459
+ placeholder(t8("pine.placeholder", "// plot(sma(close, 20))")),
1126
1460
  EditorView.updateListener.of((u) => {
1127
1461
  if (!u.docChanged) return;
1128
1462
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -1155,23 +1489,32 @@ export {
1155
1489
  GRID_SETTING_KEY,
1156
1490
  PINE_SCRIPT_STORAGE_KEY,
1157
1491
  RETURN_CURSOR_KEY,
1492
+ THEME_STORAGE_KEY,
1493
+ applyThemeToDocument,
1158
1494
  attachChartContextMenu,
1159
1495
  bindShortcutsModal,
1160
1496
  createDemoLayoutOptions,
1497
+ createI18nProvider,
1498
+ createSymbolSearchDialog,
1499
+ createThemeProvider,
1161
1500
  loadIndicatorConfig,
1162
1501
  loadPineScriptPreference,
1163
1502
  loadReturnToCursorPreference,
1164
1503
  loadShowGridPreference,
1504
+ loadTheme,
1165
1505
  mergeLayoutFeatures,
1166
1506
  mountChartLayout,
1167
1507
  mountCodeSnippetPanel,
1168
1508
  mountCrosshairLegend,
1169
1509
  mountDrawingPropertiesPanel,
1510
+ mountLogoSlot,
1170
1511
  mountPineEditorPanel,
1171
1512
  mountSettingsPanel as mountSettingsMenu,
1172
1513
  mountSettingsPanel,
1173
1514
  mountStatusBar,
1174
1515
  mountSymbolSearch,
1516
+ mountSymbolSearchDialogTrigger,
1517
+ mountThemeToggle,
1175
1518
  mountTopBar,
1176
1519
  openDrawingContextMenu,
1177
1520
  openShortcutsModal,
@@ -1180,6 +1523,7 @@ export {
1180
1523
  saveIndicatorConfig,
1181
1524
  savePineScriptPreference,
1182
1525
  saveReturnToCursorPreference,
1183
- saveShowGridPreference
1526
+ saveShowGridPreference,
1527
+ saveTheme
1184
1528
  };
1185
1529
  //# sourceMappingURL=index.js.map