@philomelcc/dsh-workbench 0.1.2 → 0.1.4

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 (2) hide show
  1. package/client.js +153 -1
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -434,7 +434,159 @@ window.__ModuleLoader__.load({
434
434
  h('path', { d: 'M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H9l-4 3.5V6z', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round' }));
435
435
  }
436
436
 
437
+ // ---------- theme switcher ----------
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
+ };
460
+
461
+ // Swatches double as both the current-state indicator and the toggle.
462
+ const THEME_SWATCHES = [
463
+ { id: 'dark', label: '深色', color: '#15171a', ring: 'rgba(255,255,255,0.28)' },
464
+ { id: 'light', label: '浅色', color: '#f7f8fa', ring: 'rgba(0,0,0,0.35)' },
465
+ { id: 'paper', label: '纸质', color: '#efe4cd', ring: 'rgba(0,0,0,0.28)' },
466
+ ];
467
+
468
+ function createThemeSwitcher(ctx) {
469
+ const readPreference = () => {
470
+ try {
471
+ const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
472
+ const snapshot = theme && typeof theme.getTheme === 'function' ? theme.getTheme() : null;
473
+ return snapshot ? snapshot.preference : null;
474
+ } catch (_error) {
475
+ return null;
476
+ }
477
+ };
478
+ const readPaper = () => {
479
+ try { return window.localStorage.getItem(PAPER_KEY) === '1'; } catch (_error) { return false; }
480
+ };
481
+ const writePaper = (on) => {
482
+ try { window.localStorage.setItem(PAPER_KEY, on ? '1' : '0'); } catch (_error) { /* storage unavailable */ }
483
+ };
484
+
485
+ let paperDispose = null;
486
+ function setPaper(on) {
487
+ const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
488
+ if (!theme) return;
489
+ if (on) {
490
+ if (paperDispose) return;
491
+ // Legal persisted preference first, then paint the paper layer on top.
492
+ try { theme.setTheme('light'); } catch (error) { console.warn('[workbench] setTheme light failed', error); }
493
+ const modes = {};
494
+ Object.keys(PAPER_TOKENS).forEach((name) => {
495
+ modes[name] = { light: PAPER_TOKENS[name], dark: PAPER_TOKENS[name] };
496
+ });
497
+ try {
498
+ paperDispose = typeof theme.overrideTokens === 'function' ? theme.overrideTokens('workbench:paper', modes) : null;
499
+ } catch (error) {
500
+ console.warn('[workbench] paper override failed', error);
501
+ paperDispose = null;
502
+ }
503
+ } else if (paperDispose) {
504
+ try { paperDispose(); } catch (_error) { /* already gone */ }
505
+ paperDispose = null;
506
+ }
507
+ }
508
+
509
+ function ThemeSwitcher() {
510
+ const [paper, setPaperState] = React.useState(readPaper);
511
+ const [preference, setPreference] = React.useState(readPreference);
512
+
513
+ // Re-apply the saved paper layer after a reload / remount.
514
+ React.useEffect(() => {
515
+ if (readPaper()) setPaper(true);
516
+ }, []);
517
+
518
+ // Swatches only re-render on real preference changes; the paper layer is
519
+ // local state because the theme service does not know about it.
520
+ React.useEffect(() => {
521
+ if (typeof ctx.on !== 'function') return undefined;
522
+ let off = null;
523
+ try {
524
+ off = ctx.on('theme/change', () => {
525
+ const next = readPreference();
526
+ setPreference(next);
527
+ // Somebody picked a concrete palette in DSH's own settings: drop the layer.
528
+ if (next && next !== 'light' && paper) {
529
+ setPaper(false);
530
+ writePaper(false);
531
+ setPaperState(false);
532
+ }
533
+ });
534
+ } catch (_error) {
535
+ off = null;
536
+ }
537
+ return () => { if (typeof off === 'function') off(); };
538
+ }, [paper]);
539
+
540
+ const activeId = paper ? 'paper' : (preference === 'dark' ? 'dark' : 'light');
541
+
542
+ const choose = (id) => {
543
+ const theme = typeof ctx.get === 'function' ? ctx.get('theme') : ctx.theme;
544
+ if (!theme) return;
545
+ if (id === 'paper') {
546
+ setPaper(true);
547
+ writePaper(true);
548
+ setPaperState(true);
549
+ return;
550
+ }
551
+ setPaper(false);
552
+ writePaper(false);
553
+ setPaperState(false);
554
+ try { theme.setTheme(id); } catch (error) { console.warn('[workbench] setTheme failed', error); }
555
+ };
556
+
557
+ return h('div', {
558
+ style: { display: 'inline-flex', alignItems: 'center', gap: 8, flex: '0 0 auto' },
559
+ title: '主题',
560
+ },
561
+ THEME_SWATCHES.map((swatch) => {
562
+ const active = activeId === swatch.id;
563
+ return h('button', {
564
+ key: swatch.id,
565
+ type: 'button',
566
+ title: swatch.label,
567
+ 'aria-label': swatch.label,
568
+ 'aria-pressed': active ? 'true' : 'false',
569
+ onClick: () => choose(swatch.id),
570
+ style: {
571
+ width: 17, height: 17, padding: 0, borderRadius: '50%', flex: '0 0 auto',
572
+ background: swatch.color,
573
+ // A hairline ring keeps the dark swatch readable on a dark bar;
574
+ // the selected one only thickens and takes the brand color.
575
+ border: (active ? '1.5px' : '1px') + ' solid ' + (active ? 'var(--dsw-alias-brand-primary)' : swatch.ring),
576
+ boxSizing: 'border-box',
577
+ cursor: 'pointer',
578
+ transition: 'border-color .12s, border-width .12s',
579
+ },
580
+ });
581
+ }),
582
+ );
583
+ }
584
+ return ThemeSwitcher;
585
+ }
586
+
437
587
  function createTopBar(ctx, balance) {
588
+ const ThemeSwitcher = createThemeSwitcher(ctx);
589
+
438
590
  function BalanceChip() {
439
591
  const snap = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot);
440
592
  const [hover, setHover] = React.useState(false);
@@ -637,7 +789,7 @@ window.__ModuleLoader__.load({
637
789
  iconBtn('plugins', '插件', IconPlugins),
638
790
  iconBtn('schedules', '自动化任务', IconClock),
639
791
  ),
640
- null,
792
+ h('div', { style: { flex: '0 0 auto', marginRight: 150 } }, h(ThemeSwitcher)),
641
793
  ),
642
794
  h('div', { style: { height: 45, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid var(--dsw-alias-border-l1)' } },
643
795
  h('div', { ref: tabsRef, style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 4, flex: 1, overflowX: 'auto', overflowY: 'hidden' } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@philomelcc/dsh-workbench",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",