@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.
- package/README.md +11 -14
- package/client.js +159 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
# @philomelcc/dsh-workbench
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
small API modules register against.
|
|
3
|
+
外壳本体:顶部第二行的标签切换、每个模块一整页,以及模块注册用的那套小 API。
|
|
5
4
|
|
|
6
|
-
|
|
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
|
-
|
|
15
|
-
|
|
12
|
+
契约:[`docs/SPEC.md`](../../docs/SPEC.md) · 模板:[`template/`](../../template) ·
|
|
13
|
+
现成模块:[状态监控](../status-monitor)、[下载中心](../download-center)。
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
外壳提供:
|
|
18
16
|
|
|
19
|
-
- `registerView({ id, title, icon, component, order, subtitle, badge, tab })`
|
|
20
|
-
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
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`
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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: '#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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 (
|
|
563
|
+
if (typeof ctx.on !== 'function') return undefined;
|
|
482
564
|
let off = null;
|
|
483
565
|
try {
|
|
484
|
-
off = ctx.on('theme/change', () =>
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
617
|
+
width: 17, height: 17, padding: 0, borderRadius: '50%', flex: '0 0 auto',
|
|
509
618
|
background: swatch.color,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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: '
|
|
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
|
-
|
|
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(
|
|
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