@coderyo/ui-shell 1.0.2 → 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;flex-shrink:0;box-sizing:border-box;width:100%;min-width:0;overflow-x:auto;overflow-y:visible;position:relative;";
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,10 +524,22 @@ 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;
@@ -364,7 +558,7 @@ function mountTopBar(parent, opts = {}) {
364
558
  for (const iv of intervals) {
365
559
  const btn = document.createElement("button");
366
560
  btn.type = "button";
367
- btn.textContent = t3(`interval.${iv}`, iv);
561
+ btn.textContent = tKey(`interval.${iv}`, iv);
368
562
  btn.style.cssText = btnStyle;
369
563
  btn.onclick = () => {
370
564
  if (activeInterval === iv) return;
@@ -388,7 +582,15 @@ function mountTopBar(parent, opts = {}) {
388
582
  b.onclick = () => fn?.();
389
583
  return b;
390
584
  };
391
- 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
+ }
392
594
  if (opts.showSettings && opts.settings) mountSettingsPanel(bar, opts.settings);
393
595
  bar.appendChild(mkBtn("\u26F6", opts.onFullscreen));
394
596
  bar.appendChild(mkBtn("\u{1F4F7}", opts.onScreenshot));
@@ -403,8 +605,116 @@ function mountTopBar(parent, opts = {}) {
403
605
  };
404
606
  }
405
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
+
406
716
  // src/context-menu.ts
407
- import { t as t4 } from "@coderyo/i18n";
717
+ import { t as t3 } from "@coderyo/i18n";
408
718
  function attachChartContextMenu(chartHost, opts = {}) {
409
719
  let menu = null;
410
720
  const close = () => {
@@ -418,7 +728,7 @@ function attachChartContextMenu(chartHost, opts = {}) {
418
728
  const actions = opts.actions ?? [
419
729
  {
420
730
  id: "fit",
421
- label: t4("context.fitContent", "\u9069\u914D\u756B\u9762"),
731
+ label: t3("context.fitContent", "\u9069\u914D\u756B\u9762"),
422
732
  onClick: () => {
423
733
  }
424
734
  }
@@ -497,13 +807,13 @@ O ${fmt2(o?.o)} H ${fmt2(o?.h)} L ${fmt2(o?.l)} C ${fmt2(o?.c)}`;
497
807
  }
498
808
 
499
809
  // src/drawing-properties-panel.ts
500
- import { t as t5 } from "@coderyo/i18n";
810
+ import { t as t4 } from "@coderyo/i18n";
501
811
  function mountDrawingPropertiesPanel(parent, opts = {}) {
502
812
  const panel = document.createElement("aside");
503
813
  panel.className = "tv-drawing-props";
504
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;";
505
815
  const title = document.createElement("div");
506
- title.textContent = t5("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
816
+ title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
507
817
  title.style.cssText = "font-weight:600;margin-bottom:10px;";
508
818
  const typeEl = document.createElement("div");
509
819
  typeEl.style.color = "#8b949e";
@@ -517,26 +827,26 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
517
827
  row.appendChild(span);
518
828
  return row;
519
829
  };
520
- const colorRow = mkRow(t5("drawing.props.color", "\u984F\u8272"));
830
+ const colorRow = mkRow(t4("drawing.props.color", "\u984F\u8272"));
521
831
  const colorInput = document.createElement("input");
522
832
  colorInput.type = "color";
523
833
  colorInput.style.cssText = "width:100%;height:28px;border:none;background:transparent;cursor:pointer;";
524
834
  colorRow.appendChild(colorInput);
525
- const widthRow = mkRow(t5("drawing.props.lineWidth", "\u7DDA\u5BEC"));
835
+ const widthRow = mkRow(t4("drawing.props.lineWidth", "\u7DDA\u5BEC"));
526
836
  const widthInput = document.createElement("input");
527
837
  widthInput.type = "range";
528
838
  widthInput.min = "1";
529
839
  widthInput.max = "6";
530
840
  widthInput.style.width = "100%";
531
841
  widthRow.appendChild(widthInput);
532
- const textRow = mkRow(t5("drawing.props.text", "\u6587\u5B57"));
842
+ const textRow = mkRow(t4("drawing.props.text", "\u6587\u5B57"));
533
843
  const textInput = document.createElement("input");
534
844
  textInput.type = "text";
535
845
  textInput.style.cssText = "padding:4px 8px;border-radius:4px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;";
536
846
  textRow.appendChild(textInput);
537
847
  const lockedHint = document.createElement("div");
538
848
  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");
849
+ lockedHint.textContent = t4("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
540
850
  panel.append(title, typeEl, colorRow, widthRow, textRow, lockedHint);
541
851
  parent.appendChild(panel);
542
852
  const emit = () => {
@@ -555,7 +865,7 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
555
865
  return;
556
866
  }
557
867
  panel.style.display = "block";
558
- typeEl.textContent = `${t5("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
868
+ typeEl.textContent = `${t4("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
559
869
  const meta = drawing.meta ?? {};
560
870
  colorInput.value = String(meta.color ?? "#58a6ff");
561
871
  widthInput.value = String(meta.lineWidth ?? 2);
@@ -639,7 +949,7 @@ function createDemoLayoutOptions(partial = {}) {
639
949
  }
640
950
 
641
951
  // src/status-bar.ts
642
- import { getLocale, setLocale, t as t6 } from "@coderyo/i18n";
952
+ import { getLocale as getLocale2, setLocale as setLocale2, t as t5 } from "@coderyo/i18n";
643
953
  function fmt(n, digits = 2) {
644
954
  if (n == null || Number.isNaN(n)) return "\u2014";
645
955
  return n.toLocaleString(void 0, { maximumFractionDigits: digits });
@@ -656,7 +966,7 @@ function mountStatusBar(parent, opts = {}) {
656
966
  const localeWrap = document.createElement("label");
657
967
  localeWrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-left:auto;";
658
968
  const localeLabel = document.createElement("span");
659
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
969
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
660
970
  const localeSelect = document.createElement("select");
661
971
  localeSelect.style.cssText = "background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;font-size:11px;padding:2px 4px;";
662
972
  for (const loc of ["zh-TW", "en"]) {
@@ -665,11 +975,11 @@ function mountStatusBar(parent, opts = {}) {
665
975
  opt.textContent = loc;
666
976
  localeSelect.appendChild(opt);
667
977
  }
668
- localeSelect.value = opts.locale ?? getLocale();
978
+ localeSelect.value = opts.locale ?? getLocale2();
669
979
  localeSelect.onchange = () => {
670
- setLocale(localeSelect.value);
980
+ setLocale2(localeSelect.value);
671
981
  opts.onLocaleChange?.(localeSelect.value);
672
- localeLabel.textContent = t6("status.locale", "\u8A9E\u7CFB");
982
+ localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
673
983
  render(opts);
674
984
  };
675
985
  localeWrap.append(localeLabel, localeSelect);
@@ -677,14 +987,14 @@ function mountStatusBar(parent, opts = {}) {
677
987
  parent.appendChild(bar);
678
988
  const render = (state) => {
679
989
  const merged = { ...opts, ...state };
680
- conn.textContent = `${t6("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
990
+ conn.textContent = `${t5("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
681
991
  const parts = [merged.symbol, merged.interval].filter(Boolean);
682
992
  sym.textContent = parts.length ? parts.join(" \xB7 ") : "";
683
993
  const o = merged.ohlcv;
684
994
  if (o && (o.o != null || o.c != null)) {
685
995
  ohlcv.textContent = `O ${fmt(o.o)} H ${fmt(o.h)} L ${fmt(o.l)} C ${fmt(o.c)} V ${fmt(o.v, 0)}`;
686
996
  } else {
687
- 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");
688
998
  }
689
999
  };
690
1000
  render(opts);
@@ -698,7 +1008,7 @@ function mountStatusBar(parent, opts = {}) {
698
1008
  }
699
1009
 
700
1010
  // src/shortcuts-modal.ts
701
- import { t as t7 } from "@coderyo/i18n";
1011
+ import { t as t6 } from "@coderyo/i18n";
702
1012
  var SHORTCUTS = [
703
1013
  { key: "\u2196 / Esc", desc: "\u6E38\u6A19\uFF08\u9078\u53D6/\u7DE8\u8F2F\u7E6A\u5716\uFF09" },
704
1014
  { key: "Delete", desc: "\u522A\u9664\u9078\u4E2D\u7E6A\u5716" },
@@ -727,7 +1037,7 @@ function openShortcutsModal() {
727
1037
  const box = document.createElement("div");
728
1038
  box.style.cssText = "min-width:320px;max-width:90vw;padding:16px 20px;background:#161b22;border:1px solid #30363d;border-radius:8px;color:#e6edf3;";
729
1039
  const h = document.createElement("h2");
730
- h.textContent = t7("shortcuts.title", "\u5FEB\u6377\u9375");
1040
+ h.textContent = t6("shortcuts.title", "\u5FEB\u6377\u9375");
731
1041
  h.style.cssText = "font-size:16px;margin:0 0 12px;";
732
1042
  const list = document.createElement("div");
733
1043
  list.style.cssText = "font-size:13px;line-height:1.8;";
@@ -738,7 +1048,7 @@ function openShortcutsModal() {
738
1048
  }
739
1049
  const closeBtn = document.createElement("button");
740
1050
  closeBtn.type = "button";
741
- closeBtn.textContent = t7("shortcuts.close", "\u95DC\u9589");
1051
+ closeBtn.textContent = t6("shortcuts.close", "\u95DC\u9589");
742
1052
  closeBtn.style.cssText = "margin-top:14px;padding:6px 14px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;";
743
1053
  const close = () => backdrop.remove();
744
1054
  closeBtn.onclick = close;
@@ -792,7 +1102,11 @@ function mountChartLayout(root, opts = {}) {
792
1102
  root.style.minWidth = "0";
793
1103
  root.style.boxSizing = "border-box";
794
1104
  root.style.overflow = "visible";
795
- root.style.background = "#0d1117";
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
+ });
796
1110
  let activeTool = opts.activeDrawingTool ?? "cursor";
797
1111
  let setActiveDesktop = null;
798
1112
  let setActiveMobile = null;
@@ -870,7 +1184,13 @@ function mountChartLayout(root, opts = {}) {
870
1184
  headerSlot,
871
1185
  Object.assign(opts, {
872
1186
  symbolInput: f.symbolInput,
873
- 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
+ }
874
1194
  })
875
1195
  );
876
1196
  topBar = mounted.el;
@@ -916,7 +1236,7 @@ function mountChartLayout(root, opts = {}) {
916
1236
  }
917
1237
 
918
1238
  // src/drawing-context-menu.ts
919
- import { t as t8 } from "@coderyo/i18n";
1239
+ import { t as t7 } from "@coderyo/i18n";
920
1240
  function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
921
1241
  const menu = document.createElement("div");
922
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;";
@@ -938,16 +1258,16 @@ function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
938
1258
  };
939
1259
  if (drawing) {
940
1260
  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);
1261
+ add(t7("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
1262
+ add(t7("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
943
1263
  add(
944
- 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"),
945
1265
  handlers.onToggleLock
946
1266
  );
947
1267
  if (drawing.type === "text") {
948
- add(t8("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
1268
+ add(t7("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
949
1269
  }
950
- add(t8("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
1270
+ add(t7("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
951
1271
  }
952
1272
  document.body.appendChild(menu);
953
1273
  const close = () => {
@@ -999,7 +1319,7 @@ import {
999
1319
  lineNumbers,
1000
1320
  placeholder
1001
1321
  } from "@codemirror/view";
1002
- import { t as t9 } from "@coderyo/i18n";
1322
+ import { t as t8 } from "@coderyo/i18n";
1003
1323
 
1004
1324
  // src/pine-language.ts
1005
1325
  import { StreamLanguage } from "@codemirror/language";
@@ -1059,10 +1379,10 @@ function createPineLinter(onStatus) {
1059
1379
  const src = view.state.doc.toString();
1060
1380
  const result = compilePineLite(src);
1061
1381
  if (result.ok) {
1062
- onStatus?.(t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1382
+ onStatus?.(t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
1063
1383
  return [];
1064
1384
  }
1065
- 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);
1066
1386
  return pineDiagnosticsToCm(src, result.diagnostics ?? []);
1067
1387
  });
1068
1388
  }
@@ -1091,16 +1411,16 @@ function mountPineEditorPanel(parent, opts = {}) {
1091
1411
  wrap.open = true;
1092
1412
  wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
1093
1413
  const summary = document.createElement("summary");
1094
- 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");
1095
1415
  summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
1096
1416
  const toolbar = document.createElement("div");
1097
1417
  toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
1098
1418
  const status = document.createElement("span");
1099
1419
  status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
1100
- status.textContent = t9("pine.status.idle", "\u5C31\u7DD2");
1420
+ status.textContent = t8("pine.status.idle", "\u5C31\u7DD2");
1101
1421
  const applyBtn = document.createElement("button");
1102
1422
  applyBtn.type = "button";
1103
- applyBtn.textContent = t9("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1423
+ applyBtn.textContent = t8("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
1104
1424
  applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
1105
1425
  const host = document.createElement("div");
1106
1426
  host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
@@ -1115,10 +1435,10 @@ function mountPineEditorPanel(parent, opts = {}) {
1115
1435
  savePineScriptPreference(src);
1116
1436
  if (r.ok) {
1117
1437
  status.style.color = "#3fb950";
1118
- status.textContent = t9("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1438
+ status.textContent = t8("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
1119
1439
  } else {
1120
1440
  status.style.color = "#f85149";
1121
- 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");
1122
1442
  }
1123
1443
  };
1124
1444
  const extensions = [
@@ -1136,7 +1456,7 @@ function mountPineEditorPanel(parent, opts = {}) {
1136
1456
  status.textContent = msg;
1137
1457
  }),
1138
1458
  keymap.of([...defaultKeymap, ...historyKeymap]),
1139
- placeholder(t9("pine.placeholder", "// plot(sma(close, 20))")),
1459
+ placeholder(t8("pine.placeholder", "// plot(sma(close, 20))")),
1140
1460
  EditorView.updateListener.of((u) => {
1141
1461
  if (!u.docChanged) return;
1142
1462
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -1169,23 +1489,32 @@ export {
1169
1489
  GRID_SETTING_KEY,
1170
1490
  PINE_SCRIPT_STORAGE_KEY,
1171
1491
  RETURN_CURSOR_KEY,
1492
+ THEME_STORAGE_KEY,
1493
+ applyThemeToDocument,
1172
1494
  attachChartContextMenu,
1173
1495
  bindShortcutsModal,
1174
1496
  createDemoLayoutOptions,
1497
+ createI18nProvider,
1498
+ createSymbolSearchDialog,
1499
+ createThemeProvider,
1175
1500
  loadIndicatorConfig,
1176
1501
  loadPineScriptPreference,
1177
1502
  loadReturnToCursorPreference,
1178
1503
  loadShowGridPreference,
1504
+ loadTheme,
1179
1505
  mergeLayoutFeatures,
1180
1506
  mountChartLayout,
1181
1507
  mountCodeSnippetPanel,
1182
1508
  mountCrosshairLegend,
1183
1509
  mountDrawingPropertiesPanel,
1510
+ mountLogoSlot,
1184
1511
  mountPineEditorPanel,
1185
1512
  mountSettingsPanel as mountSettingsMenu,
1186
1513
  mountSettingsPanel,
1187
1514
  mountStatusBar,
1188
1515
  mountSymbolSearch,
1516
+ mountSymbolSearchDialogTrigger,
1517
+ mountThemeToggle,
1189
1518
  mountTopBar,
1190
1519
  openDrawingContextMenu,
1191
1520
  openShortcutsModal,
@@ -1194,6 +1523,7 @@ export {
1194
1523
  saveIndicatorConfig,
1195
1524
  savePineScriptPreference,
1196
1525
  saveReturnToCursorPreference,
1197
- saveShowGridPreference
1526
+ saveShowGridPreference,
1527
+ saveTheme
1198
1528
  };
1199
1529
  //# sourceMappingURL=index.js.map