@philomelcc/dsh-workbench 0.1.3 → 0.1.5

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.
Files changed (3) hide show
  1. package/README.md +11 -14
  2. package/client.js +159 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,26 +1,23 @@
1
1
  # @philomelcc/dsh-workbench
2
2
 
3
- The shell: a second top-bar row of workbench tabs, one page per module, plus the
4
- small API modules register against.
3
+ 外壳本体:顶部第二行的标签切换、每个模块一整页,以及模块注册用的那套小 API。
5
4
 
6
- This package does almost nothing on its own — install it together with at least
7
- one module:
5
+ 它单独装没什么用——至少配一个模块:
8
6
 
9
7
  ```powershell
10
8
  dsh plugin --profile web add @philomelcc/dsh-workbench
11
9
  dsh plugin --profile web add @philomelcc/dsh-status-monitor
12
10
  ```
13
11
 
14
- Contract: [`docs/SPEC.md`](../../docs/SPEC.md) · module template: [`template/`](../../template) ·
15
- ready-made modules: [status monitor](../status-monitor), [download center](../download-center).
12
+ 契约:[`docs/SPEC.md`](../../docs/SPEC.md) · 模板:[`template/`](../../template) ·
13
+ 现成模块:[状态监控](../status-monitor)、[下载中心](../download-center)。
16
14
 
17
- What the shell provides:
15
+ 外壳提供:
18
16
 
19
- - `registerView({ id, title, icon, component, order, subtitle, badge, tab })` —
20
- one tab, one full page;
21
- - `components` (StatusDot, Card, SectionHeader, EmptyState, Spinner, Button, Field);
22
- - `tokens` (theme colors that follow light/dark);
23
- - `apiVersion` — `1` today;
24
- - `refresh()` — repaint the tab row after a badge changed.
17
+ - `registerView({ id, title, icon, component, order, subtitle, badge, tab })` —— 一个标签、一整页;
18
+ - `components`(StatusDot / Card / SectionHeader / EmptyState / Spinner / Button / Field);
19
+ - `tokens`(跟随明暗主题的颜色 token);
20
+ - `apiVersion` —— 目前是 `1`;
21
+ - `refresh()` —— 角标变化后重绘标签行。
25
22
 
26
- MIT.
23
+ MIT。
package/client.js CHANGED
@@ -435,84 +435,193 @@ window.__ModuleLoader__.load({
435
435
  }
436
436
 
437
437
  // ---------- theme switcher ----------
438
- // `paper` builds on the light palette and overrides the alias layer only.
439
- const PAPER_THEME = {
440
- id: 'paper',
441
- colorScheme: 'light',
442
- tokens: {
443
- '--dsw-alias-bg-base': '#f4efe3',
444
- '--dsw-alias-bg-layer-1': '#efe9db',
445
- '--dsw-alias-bg-layer-2': '#e7dfcd',
446
- '--dsw-alias-bg-overlay': '#fbf7ee',
447
- '--dsw-alias-border-l1': '#d8cfba',
448
- '--dsw-alias-border-l2': '#c6bba2',
449
- '--dsw-alias-brand-primary': '#8a6a45',
450
- '--dsw-alias-label-primary': '#3b342a',
451
- '--dsw-alias-label-secondary': '#6f6557',
452
- '--dsw-alias-state-error-primary': '#a34a3c',
453
- '--dsw-alias-state-warn-primary': '#a8792c',
454
- '--dsw-alias-state-success-primary': '#4f7a5a',
455
- '--dsw-alias-state-idle-primary': '#a49a89',
456
- '--dsw-specific-sidebar-fill': '#efe9db',
457
- },
438
+ // `paper` cannot be a persisted preference: DSH only accepts light / dark /
439
+ // system there, and anything else is dropped when settings resync (that is
440
+ // why switching models used to fall back to dark). So the paper palette is
441
+ // an override layer on top of the legal `light` preference, remembered in
442
+ // localStorage by us.
443
+ const PAPER_KEY = 'dsh-workbench:paper';
444
+ const PAPER_TOKENS = {
445
+ '--dsw-alias-bg-base': '#f4efe3',
446
+ '--dsw-alias-bg-layer-1': '#efe9db',
447
+ '--dsw-alias-bg-layer-2': '#e7dfcd',
448
+ '--dsw-alias-bg-overlay': '#fbf7ee',
449
+ '--dsw-alias-border-l1': '#d8cfba',
450
+ '--dsw-alias-border-l2': '#c6bba2',
451
+ '--dsw-alias-brand-primary': '#8a6a45',
452
+ '--dsw-alias-label-primary': '#3b342a',
453
+ '--dsw-alias-label-secondary': '#6f6557',
454
+ '--dsw-alias-state-error-primary': '#a34a3c',
455
+ '--dsw-alias-state-warn-primary': '#a8792c',
456
+ '--dsw-alias-state-success-primary': '#4f7a5a',
457
+ '--dsw-alias-state-idle-primary': '#a49a89',
458
+ '--dsw-specific-sidebar-fill': '#efe9db',
459
+ // Chat bubbles: the light base uses a pale DeepSeek blue, which looks
460
+ // wrong on paper. These two are outside the alias layer.
461
+ '--dsw-specific-bubble': '#efe7d6',
462
+ '--dsw-specific-bubble-highlight': '#e4d8c0',
458
463
  };
459
464
 
460
465
  // Swatches double as both the current-state indicator and the toggle.
461
466
  const THEME_SWATCHES = [
462
- { id: 'dark', label: '深色', color: '#15171a' },
463
- { id: 'light', label: '浅色', color: '#f7f8fa' },
464
- { id: 'paper', label: '纸质', color: '#f4efe3' },
467
+ { id: 'dark', label: '深色', color: '#15171a', ring: 'rgba(255,255,255,0.28)' },
468
+ { id: 'light', label: '浅色', color: '#f7f8fa', ring: 'rgba(0,0,0,0.35)' },
469
+ { id: 'paper', label: '纸质', color: '#efe4cd', ring: 'rgba(0,0,0,0.28)' },
465
470
  ];
466
471
 
467
472
  function createThemeSwitcher(ctx) {
468
- function ThemeSwitcher() {
473
+ const readPreference = () => {
474
+ try {
475
+ const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
476
+ const snapshot = theme && typeof theme.getTheme === 'function' ? theme.getTheme() : null;
477
+ return snapshot ? snapshot.preference : null;
478
+ } catch (_error) {
479
+ return null;
480
+ }
481
+ };
482
+ const readPaper = () => {
483
+ try { return window.localStorage.getItem(PAPER_KEY) === '1'; } catch (_error) { return false; }
484
+ };
485
+ const writePaper = (on) => {
486
+ try { window.localStorage.setItem(PAPER_KEY, on ? '1' : '0'); } catch (_error) { /* storage unavailable */ }
487
+ };
488
+
489
+ let paperDispose = null;
490
+ function setPaper(on) {
469
491
  const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
470
- const read = () => {
492
+ if (!theme) return;
493
+ if (on) {
494
+ if (paperDispose) return;
495
+ // Legal persisted preference first, then paint the paper layer on top.
496
+ try { theme.setTheme('light'); } catch (error) { console.warn('[workbench] setTheme light failed', error); }
497
+ const modes = {};
498
+ Object.keys(PAPER_TOKENS).forEach((name) => {
499
+ modes[name] = { light: PAPER_TOKENS[name], dark: PAPER_TOKENS[name] };
500
+ });
471
501
  try {
472
- const snapshot = theme && typeof theme.getTheme === 'function' ? theme.getTheme() : null;
473
- return snapshot ? snapshot.preference : null;
474
- } catch (_error) {
475
- return null;
502
+ paperDispose = typeof theme.overrideTokens === 'function' ? theme.overrideTokens('workbench:paper', modes) : null;
503
+ } catch (error) {
504
+ console.warn('[workbench] paper override failed', error);
505
+ paperDispose = null;
476
506
  }
477
- };
478
- const [preference, setPreference] = React.useState(read);
507
+ } else if (paperDispose) {
508
+ try { paperDispose(); } catch (_error) { /* already gone */ }
509
+ paperDispose = null;
510
+ }
511
+ }
512
+
513
+ function ThemeSwitcher() {
514
+ const boxRef = React.useRef(null);
515
+ const [paper, setPaperState] = React.useState(readPaper);
516
+ const [preference, setPreference] = React.useState(readPreference);
517
+
518
+ // The account block is fixed to the right and changes width (a reconnect
519
+ // banner widens it), so park the swatches against its measured left edge
520
+ // instead of a guessed margin.
521
+ React.useEffect(() => {
522
+ const box = boxRef.current;
523
+ if (!box) return undefined;
524
+ const place = () => {
525
+ const settings = document.querySelector('[class*="settingsArea"]');
526
+ if (!settings) {
527
+ box.style.right = '150px';
528
+ return;
529
+ }
530
+ const rect = settings.getBoundingClientRect();
531
+ if (rect.width > 0) box.style.right = (rect.width + 16 + 12) + 'px';
532
+ // Same box as the account block, so `align-items: center` lands on
533
+ // the account's optical center instead of a guessed offset.
534
+ if (rect.height > 0) {
535
+ box.style.top = rect.top + 'px';
536
+ box.style.height = rect.height + 'px';
537
+ }
538
+ };
539
+ place();
540
+ let observer = null;
541
+ const settings = document.querySelector('[class*="settingsArea"]');
542
+ if (settings && typeof ResizeObserver !== 'undefined') {
543
+ observer = new ResizeObserver(place);
544
+ // border-box: the account's width changes through its chrome, not
545
+ // just its text content.
546
+ observer.observe(settings, { box: 'border-box' });
547
+ }
548
+ window.addEventListener('resize', place);
549
+ return () => {
550
+ if (observer) observer.disconnect();
551
+ window.removeEventListener('resize', place);
552
+ };
553
+ }, []);
554
+
555
+ // Re-apply the saved paper layer after a reload / remount.
556
+ React.useEffect(() => {
557
+ if (readPaper()) setPaper(true);
558
+ }, []);
479
559
 
560
+ // Swatches only re-render on real preference changes; the paper layer is
561
+ // local state because the theme service does not know about it.
480
562
  React.useEffect(() => {
481
- if (!theme || typeof theme.getTheme !== 'function' || typeof ctx.on !== 'function') return undefined;
563
+ if (typeof ctx.on !== 'function') return undefined;
482
564
  let off = null;
483
565
  try {
484
- off = ctx.on('theme/change', () => setPreference(read()));
566
+ off = ctx.on('theme/change', () => {
567
+ const next = readPreference();
568
+ setPreference(next);
569
+ // Somebody picked a concrete palette in DSH's own settings: drop the layer.
570
+ if (next && next !== 'light' && paper) {
571
+ setPaper(false);
572
+ writePaper(false);
573
+ setPaperState(false);
574
+ }
575
+ });
485
576
  } catch (_error) {
486
577
  off = null;
487
578
  }
488
579
  return () => { if (typeof off === 'function') off(); };
489
- }, []);
580
+ }, [paper]);
581
+
582
+ const activeId = paper ? 'paper' : (preference === 'dark' ? 'dark' : 'light');
583
+
584
+ const choose = (id) => {
585
+ const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
586
+ if (!theme) return;
587
+ if (id === 'paper') {
588
+ setPaper(true);
589
+ writePaper(true);
590
+ setPaperState(true);
591
+ return;
592
+ }
593
+ setPaper(false);
594
+ writePaper(false);
595
+ setPaperState(false);
596
+ try { theme.setTheme(id); } catch (error) { console.warn('[workbench] setTheme failed', error); }
597
+ };
490
598
 
491
599
  return h('div', {
492
- style: { display: 'inline-flex', alignItems: 'center', gap: 7, flex: '0 0 auto' },
600
+ ref: boxRef,
601
+ style: {
602
+ position: 'fixed', top: 2, height: 40, zIndex: 301,
603
+ display: 'inline-flex', alignItems: 'center', gap: 8,
604
+ },
493
605
  title: '主题',
494
606
  },
495
607
  THEME_SWATCHES.map((swatch) => {
496
- const active = preference === swatch.id;
608
+ const active = activeId === swatch.id;
497
609
  return h('button', {
498
610
  key: swatch.id,
499
611
  type: 'button',
500
612
  title: swatch.label,
501
613
  'aria-label': swatch.label,
502
614
  'aria-pressed': active ? 'true' : 'false',
503
- onClick: () => {
504
- if (!theme || typeof theme.setTheme !== 'function') return;
505
- try { theme.setTheme(swatch.id); } catch (error) { console.warn('[workbench] setTheme failed', error); }
506
- },
615
+ onClick: () => choose(swatch.id),
507
616
  style: {
508
- width: 15, height: 15, padding: 0, borderRadius: '50%', flex: '0 0 auto',
617
+ width: 17, height: 17, padding: 0, borderRadius: '50%', flex: '0 0 auto',
509
618
  background: swatch.color,
510
- border: '1px solid ' + (active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l2)'),
511
- boxShadow: active
512
- ? '0 0 0 2px var(--dsw-alias-bg-base), 0 0 0 3px var(--dsw-alias-brand-primary)'
513
- : 'none',
619
+ // A hairline ring keeps the dark swatch readable on a dark bar;
620
+ // the selected one only thickens and takes the brand color.
621
+ border: (active ? '1.5px' : '1px') + ' solid ' + (active ? 'var(--dsw-alias-brand-primary)' : swatch.ring),
622
+ boxSizing: 'border-box',
514
623
  cursor: 'pointer',
515
- transition: 'box-shadow .12s, border-color .12s',
624
+ transition: 'border-color .12s, border-width .12s',
516
625
  },
517
626
  });
518
627
  }),
@@ -530,7 +639,9 @@ window.__ModuleLoader__.load({
530
639
  const tone = snap.phase === 'ready' ? balanceTone(snap.amount) : 'muted';
531
640
  const value = snap.phase === 'ready' && typeof snap.amount === 'number' ? snap.amount.toFixed(2) : (snap.phase === 'loading' ? '…' : '—');
532
641
  const toneColor = tone === 'green' ? '#4ade80' : tone === 'amber' ? '#e3b341' : tone === 'red' ? '#f87171' : 'var(--dsw-alias-label-secondary)';
533
- const borderBase = tone === 'green' ? '#2f4a37' : tone === 'amber' ? '#4a3d22' : tone === 'red' ? '#4a2a2a' : 'var(--dsw-alias-border-l2)';
642
+ // The border follows the theme: hard-coded dark greens read as a black
643
+ // ring on light/paper backgrounds.
644
+ const borderBase = 'var(--dsw-alias-border-l2)';
534
645
  const at = new Date(snap.updatedAt || Date.now()).toLocaleTimeString('zh-CN', { hour12: false });
535
646
  return h('div', {
536
647
  style: { position: 'relative', display: 'inline-flex' },
@@ -726,7 +837,7 @@ window.__ModuleLoader__.load({
726
837
  iconBtn('plugins', '插件', IconPlugins),
727
838
  iconBtn('schedules', '自动化任务', IconClock),
728
839
  ),
729
- h('div', { style: { flex: '0 0 auto', marginRight: 150 } }, h(ThemeSwitcher)),
840
+ h(ThemeSwitcher),
730
841
  ),
731
842
  h('div', { style: { height: 45, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid var(--dsw-alias-border-l1)' } },
732
843
  h('div', { ref: tabsRef, style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 4, flex: 1, overflowX: 'auto', overflowY: 'hidden' } },
@@ -1329,16 +1440,6 @@ div[data-row-key^="session:"]:has(svg[data-state="ongoing"]) svg[data-state="ong
1329
1440
  layoutService = ctx.get('layout') || null;
1330
1441
  layoutGetter = () => ctx.get('layout') || null;
1331
1442
 
1332
- // Extra palette on top of the built-in light/dark pair.
1333
- try {
1334
- const themeService = typeof ctx.get === 'function' ? ctx.get('theme') : null;
1335
- if (themeService && typeof themeService.register === 'function') {
1336
- ctx.effect(() => themeService.register(PAPER_THEME), 'workbench: paper theme');
1337
- }
1338
- } catch (error) {
1339
- console.warn('[workbench] paper theme registration failed', error);
1340
- }
1341
-
1342
1443
  // The shell's own overview lives above the tab row: it is reachable from
1343
1444
  // the top-bar "工作台" icon, so it registers a page without a tab.
1344
1445
  ctx.effect(() => api.registerView({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@philomelcc/dsh-workbench",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Workbench shell for DeepSeek Harness: a view/page module API, shared UI primitives and a top-bar switcher.",
6
6
  "license": "MIT",