@deepblend/dsh-blender-ui 0.3.1 → 0.3.2

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/lib/client.js +235 -93
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -108,6 +108,19 @@ window.__ModuleLoader__.load({
108
108
  // `contract/workbench-copy.test.mjs`, so a missing key cannot leave a hole in either direction.
109
109
  const STRINGS = {
110
110
  zh: {
111
+ 'workbench.appearance': '外观',
112
+ 'workbench.system': '跟随系统',
113
+ 'workbench.light': '浅色',
114
+ 'workbench.dark': '深色',
115
+ 'workbench.navigation': '工作台页面',
116
+ 'workbench.unsaved': '有未保存的修改',
117
+ 'projects.name': '作品名称',
118
+ 'projects.storage': '项目存储位置',
119
+ 'projects.startHint': '选择一个作品起点,调整参数,再创建自己的项目。',
120
+ 'guide.notes': '使用说明与当前进度',
121
+ 'scene.outline': '场景内容',
122
+ 'scene.inspector': '编辑与素材',
123
+
111
124
  'inspection.mismatch': '检查回执与请求的版本、相机或帧不一致,未标记为成功。',
112
125
  'inspection.cameraUnavailable': '{camera}(当前版本不可用)',
113
126
  "inspection.title": "固定视角检查",
@@ -513,9 +526,10 @@ window.__ModuleLoader__.load({
513
526
  'jobs.provenanceMissing': '{count} 帧尚未完成',
514
527
  'jobs.cancel': '取消',
515
528
  'jobs.resume': '继续渲染',
516
- 'jobs.startDelivery': '启动一次交付渲染(写操作经 Host)',
517
- 'jobs.frameStart': '帧起',
518
- 'jobs.frameEnd': '帧止',
529
+ 'jobs.startDelivery': '启动交付渲染',
530
+ 'jobs.frameStart': '起始帧',
531
+ 'jobs.frameEnd': '结束帧',
532
+ 'jobs.profile': '渲染配置',
519
533
  'jobs.start': '启动',
520
534
  'qa.passed': '技术校验通过',
521
535
  'qa.none': '无技术校验记录',
@@ -621,6 +635,19 @@ window.__ModuleLoader__.load({
621
635
  'revisions.title': '版本({count})',
622
636
  },
623
637
  en: {
638
+ 'workbench.appearance': 'Appearance',
639
+ 'workbench.system': 'System',
640
+ 'workbench.light': 'Light',
641
+ 'workbench.dark': 'Dark',
642
+ 'workbench.navigation': 'Workbench pages',
643
+ 'workbench.unsaved': 'Unsaved changes',
644
+ 'projects.name': 'Project name',
645
+ 'projects.storage': 'Project storage',
646
+ 'projects.startHint': 'Choose a starting point, adjust its parameters, and create your own project.',
647
+ 'guide.notes': 'Guidance and current progress',
648
+ 'scene.outline': 'Scene contents',
649
+ 'scene.inspector': 'Editing and assets',
650
+
624
651
  'inspection.mismatch': 'The inspection receipt does not match the requested revision, camera or frame; success was not recorded.',
625
652
  'inspection.cameraUnavailable': '{camera} (unavailable in this revision)',
626
653
  "inspection.title": "Inspect a fixed view",
@@ -1026,9 +1053,10 @@ window.__ModuleLoader__.load({
1026
1053
  'jobs.provenanceMissing': '{count} frames not completed',
1027
1054
  'jobs.cancel': 'Cancel',
1028
1055
  'jobs.resume': 'Resume render',
1029
- 'jobs.startDelivery': 'Start a delivery render (the write goes through the Host)',
1056
+ 'jobs.startDelivery': 'Start a delivery render',
1030
1057
  'jobs.frameStart': 'from frame',
1031
1058
  'jobs.frameEnd': 'to frame',
1059
+ 'jobs.profile': 'Render profile',
1032
1060
  'jobs.start': 'Start',
1033
1061
  'qa.passed': 'technical checks passed',
1034
1062
  'qa.none': 'no technical check recorded',
@@ -1292,55 +1320,130 @@ window.__ModuleLoader__.load({
1292
1320
  // one <style> tagged with this package, deduped by querySelector.
1293
1321
  // =========================================================================
1294
1322
 
1323
+ // Existing project mark: deepblend/docs/brand/logo.svg (MIT), embedded for offline loading.
1324
+ const BRAND_MARK = 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20role%3D%22img%22%20aria-label%3D%22DeepBlend%20Studio%20logo%22%3E%0A%20%20%3Ctitle%3EDeepBlend%20Studio%3C%2Ftitle%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3ClinearGradient%20id%3D%22mark%22%20x1%3D%220.1%22%20y1%3D%220%22%20x2%3D%220.85%22%20y2%3D%221%22%3E%0A%20%20%20%20%20%20%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23FFC078%22%2F%3E%0A%20%20%20%20%20%20%3Cstop%20offset%3D%220.45%22%20stop-color%3D%22%23F2802A%22%2F%3E%0A%20%20%20%20%20%20%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23C85C08%22%2F%3E%0A%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Crect%20width%3D%22512%22%20height%3D%22512%22%20rx%3D%22112%22%20fill%3D%22url%28%23mark%29%22%2F%3E%0A%20%20%3Cg%20stroke%3D%22%23FFFFFF%22%20stroke-width%3D%2211%22%20stroke-linejoin%3D%22round%22%20stroke-linecap%3D%22round%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20106%20L386%20181%20L386%20331%20L256%20406%20L126%20331%20L126%20181%20Z%22%20fill%3D%22%23FFFFFF%22%20fill-opacity%3D%220.14%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20106%20L386%20181%20L256%20256%20L126%20181%20Z%22%20fill%3D%22%23FFFFFF%22%20fill-opacity%3D%220.34%22%20stroke%3D%22none%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20256%20L126%20181%20L126%20331%20L256%20406%20Z%22%20fill%3D%22%237A3200%22%20fill-opacity%3D%220.16%22%20stroke%3D%22none%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20256%20L386%20181%20L386%20331%20L256%20406%20Z%22%20fill%3D%22%235C2400%22%20fill-opacity%3D%220.22%22%20stroke%3D%22none%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20106%20L386%20181%20L386%20331%20L256%20406%20L126%20331%20L126%20181%20Z%22%20fill%3D%22none%22%20opacity%3D%220.93%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M256%20256%20L256%20106%20M256%20256%20L386%20331%20M256%20256%20L126%20331%22%20fill%3D%22none%22%20opacity%3D%220.93%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A'
1325
+
1295
1326
  const CSS = `
1296
- .db-root{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-size:13px}
1297
- .db-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;flex-wrap:wrap}
1298
- .db-title{font-weight:600}
1299
- .db-muted{color:var(--dsw-alias-label-tertiary)}
1300
- .db-nav{display:flex;gap:2px;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;flex-wrap:wrap}
1301
- .db-nav button{background:0 0;border:0;border-radius:8px;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;padding:5px 10px}
1302
- .db-nav button:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}
1303
- .db-nav button[data-active=true]{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}
1304
- .db-tabs{display:flex;gap:8px;padding:2px 0 8px;flex:none;flex-wrap:wrap;align-items:center}
1305
- .db-body{flex:1;min-height:0;overflow:auto;padding:12px 14px 24px}
1306
- .db-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;margin-bottom:10px}
1307
- .db-card h4{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}
1308
- .db-row{display:flex;gap:8px;align-items:baseline;padding:2px 0;line-height:18px}
1309
- .db-row>span:first-child{color:var(--dsw-alias-label-tertiary);flex:none;min-width:82px}
1327
+ .db-root{container-type:inline-size;container-name:deepblend;--db-good:#23704e;--db-warn:#946109;--db-bad:#b0303c;--db-input-border:#8492a5;--db-accent:#b95516;--db-accent-soft:#f8e9de;--db-canvas:var(--dsw-alias-bg-layer-1);--db-surface:var(--dsw-alias-bg-layer-1);--db-border:var(--dsw-alias-border-l1);--db-fill:var(--dsw-alias-fill-l2);--db-text:var(--dsw-alias-label-primary);--db-subtle:var(--dsw-alias-label-secondary);--db-muted:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;height:100%;min-height:0;min-width:0;background:var(--db-canvas);color:var(--db-text);font:13px/1.55 var(--dsw-font-sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif)}
1328
+ .db-root[data-theme=light],.db-root[data-theme=system]{color-scheme:light;--db-canvas:#f1f3f6;--db-surface:#fff;--db-border:#dce1e8;--db-fill:#f0f3f7;--db-text:#202b3c;--db-subtle:#4e5d70;--db-muted:#637186}
1329
+ .db-root[data-theme=dark]{color-scheme:dark;--db-canvas:#10151d;--db-surface:#19212d;--db-border:#334155;--db-fill:#253142;--db-text:#edf2f8;--db-subtle:#c2ccda;--db-muted:#a2afc1;--db-good:#77c9a0;--db-warn:#efbc73;--db-bad:#ff979c;--db-input-border:#657692;--db-accent:#f3a365;--db-accent-soft:#3c2d23}
1330
+ .db-root *{box-sizing:border-box}
1331
+ .db-head{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--db-border,var(--dsw-alias-border-l1));background:var(--db-surface,var(--dsw-alias-bg-layer-1));flex:none;flex-wrap:wrap}
1332
+ .db-brand{display:flex;align-items:center;gap:10px;flex:none}
1333
+ .db-logo{width:34px;height:34px;border-radius:9px}
1334
+ .db-title{margin:0;font-size:16px;font-weight:650;line-height:1.25;letter-spacing:-.3px}
1335
+ .db-brand small{display:block;font-size:11px;color:var(--db-muted);margin-top:3px}
1336
+ .db-project-context{display:flex;align-items:center;gap:8px;min-width:0;max-width:35%;padding-left:16px;border-left:1px solid var(--db-border)}
1337
+ .db-project-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
1338
+ .db-head-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}
1339
+ .db-theme{width:auto!important;min-width:94px}
1340
+ .db-muted{color:var(--db-muted,var(--dsw-alias-label-secondary))}
1341
+ .db-mono{font-family:var(--dsw-font-mono,ui-monospace,monospace)}
1342
+ .db-nav{display:flex;gap:6px;padding:8px 20px;border-bottom:1px solid var(--db-border);background:var(--db-surface);flex:none;overflow-x:auto;scrollbar-width:thin}
1343
+ .db-nav button{background:transparent;border:1px solid transparent;border-radius:7px;color:var(--db-subtle);cursor:pointer;font:inherit;min-height:34px;padding:6px 14px;white-space:nowrap;flex:none}
1344
+ .db-nav button:hover{background:var(--db-fill);color:var(--db-text)}
1345
+ .db-nav button[data-active=true]{background:var(--db-accent-soft);border-color:var(--db-accent);color:var(--db-accent);font-weight:650}
1346
+ .db-tabs{display:flex;gap:8px;padding:2px 0 12px;flex:none;flex-wrap:wrap;align-items:center}
1347
+ .db-body{flex:1;min-height:0;overflow:auto;padding:20px 24px 32px;scrollbar-width:thin;scrollbar-color:var(--db-border) transparent;scroll-padding:16px}
1348
+ .db-body>[data-view],.db-body>.db-guide{max-width:1440px;margin-left:auto;margin-right:auto}
1349
+ .db-card{background:var(--db-surface,var(--dsw-alias-bg-layer-1));border:1px solid var(--db-border,var(--dsw-alias-border-l1));border-radius:12px;padding:16px 18px;margin-bottom:14px;min-width:0}
1350
+ .db-card h4{margin:0 0 12px;font-size:14px;font-weight:650;color:var(--db-text,var(--dsw-alias-label-primary))}
1351
+ .db-card h5{margin:10px 0 8px;font-size:13px;font-weight:650}
1352
+ .db-card p{margin:8px 0 12px}
1353
+ .db-row{display:flex;gap:12px;align-items:baseline;padding:4px 0;line-height:20px}
1354
+ .db-row>span:first-child{color:var(--db-muted,var(--dsw-alias-label-tertiary));flex:none;min-width:90px}
1310
1355
  .db-row>span:last-child{min-width:0;overflow-wrap:anywhere;font-family:var(--dsw-font-mono);font-size:12px}
1311
- .db-btn{background:var(--dsw-alias-fill-l2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;font-size:12px;padding:4px 9px}
1312
- .db-btn:disabled{opacity:.5;cursor:default}
1313
- .db-btn[data-tone=primary]{background:var(--dsw-alias-brand-primary,#3b6ef5);border-color:transparent;color:#fff}
1314
- .db-btn[data-tone=danger]{color:var(--dsw-alias-label-error,#e5484d)}
1315
- .db-input,.db-area{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:inherit;font:inherit;font-size:12px;padding:5px 8px;width:100%;box-sizing:border-box}
1356
+ .db-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--db-fill,var(--dsw-alias-fill-l2));border:1px solid var(--db-border,var(--dsw-alias-border-l1));border-radius:7px;color:var(--db-text,var(--dsw-alias-label-primary));cursor:pointer;font:inherit;font-size:12px;min-height:34px;padding:6px 12px;text-decoration:none;line-height:1.4}
1357
+ .db-btn:hover:not(:disabled){border-color:var(--db-accent,var(--dsw-alias-brand-primary));background:var(--db-accent-soft,var(--dsw-alias-fill-l2))}
1358
+ .db-btn:active:not(:disabled){filter:brightness(.95)}
1359
+ .db-btn:disabled{opacity:.48;cursor:not-allowed}
1360
+ .db-btn[data-tone=primary]{background:var(--db-accent,var(--dsw-alias-brand-primary,#3b6ef5));border-color:transparent;color:#fff;font-weight:650}
1361
+ .db-root[data-theme=dark] .db-btn[data-tone=primary]{color:#20170f}
1362
+ .db-btn[data-tone=danger]{color:var(--db-bad,var(--dsw-alias-label-error,#d13438))}
1363
+ .db-btn[aria-pressed=true]{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent)}
1364
+ .db-root :is(button,a,input,select,textarea,summary):focus-visible{outline:2px solid var(--db-accent);outline-offset:3px}
1365
+ .db-input,.db-area{background:var(--db-surface,var(--dsw-alias-bg-layer-1));border:1px solid var(--db-input-border,var(--dsw-alias-border-l1));border-radius:7px;color:inherit;font:inherit;font-size:13px;min-height:34px;padding:7px 10px;width:100%;box-sizing:border-box}
1366
+ .db-input:hover,.db-area:hover{border-color:var(--db-muted,var(--dsw-alias-label-secondary))}
1367
+ .db-input:disabled,.db-area:disabled{opacity:.55;background:var(--db-fill,var(--dsw-alias-fill-l2));cursor:not-allowed}
1316
1368
  .db-area{font-family:var(--dsw-font-mono);min-height:150px;white-space:pre;overflow:auto}
1317
- .db-inline{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
1318
- .db-badge{border-radius:6px;font-size:11px;line-height:16px;padding:1px 6px;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);white-space:nowrap}
1319
- .db-badge[data-tone=ok]{color:var(--dsw-alias-label-success,#2f9e44);border-color:currentColor}
1320
- .db-badge[data-tone=warn]{color:var(--dsw-alias-label-warning,#e8a33d);border-color:currentColor}
1321
- .db-badge[data-tone=bad]{color:var(--dsw-alias-label-error,#e5484d);border-color:currentColor}
1322
- .db-badge[data-tone=live]{color:var(--dsw-alias-brand-primary,#3b6ef5);border-color:currentColor}
1323
- .db-bar{background:var(--dsw-alias-fill-l2);border-radius:999px;height:6px;overflow:hidden;width:100%;margin:4px 0}
1324
- .db-bar>i{background:var(--dsw-alias-brand-primary,#3b6ef5);display:block;height:100%}
1369
+ .db-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
1370
+ .db-inline>label{display:flex;flex-direction:column;gap:5px;min-width:0}
1371
+ .db-number-control{display:flex;flex-direction:column;gap:4px;min-width:0;max-width:160px}
1372
+ .db-input[aria-invalid=true]{border-color:var(--db-bad,var(--dsw-alias-label-error));background:var(--db-surface)}
1373
+ .db-field-feedback{font:11px/1.4 var(--dsw-font-sans,system-ui,sans-serif);color:var(--db-bad,var(--dsw-alias-label-error));white-space:normal}
1374
+ .db-field{display:flex;flex-direction:column;gap:6px;min-width:0;font-size:12px;font-weight:550}
1375
+ .db-field .db-input{font-weight:400}
1376
+ .db-badge{display:inline-flex;align-items:center;border-radius:5px;font-size:11px;line-height:18px;padding:2px 7px;border:1px solid var(--db-border,var(--dsw-alias-border-l1));color:var(--db-subtle,var(--dsw-alias-label-secondary));white-space:nowrap}
1377
+ .db-badge[data-tone=ok]{color:var(--db-good,var(--dsw-alias-label-success,#2f9e44));border-color:currentColor}
1378
+ .db-badge[data-tone=warn]{color:var(--db-warn,var(--dsw-alias-label-warning,#b8791b));border-color:currentColor}
1379
+ .db-badge[data-tone=bad]{color:var(--db-bad,var(--dsw-alias-label-error,#d13438));border-color:currentColor}
1380
+ .db-badge[data-tone=live]{color:var(--db-accent,var(--dsw-alias-brand-primary));border-color:currentColor}
1381
+ .db-bar{background:var(--db-fill,var(--dsw-alias-fill-l2));border-radius:999px;height:7px;overflow:hidden;width:100%;margin:8px 0}
1382
+ .db-bar>i{background:var(--db-accent,var(--dsw-alias-brand-primary));display:block;height:100%}
1325
1383
  .db-list{list-style:none;margin:0;padding:0}
1326
- .db-list>li{border-bottom:1px solid var(--dsw-alias-border-l1);padding:7px 0}
1384
+ .db-list>li{border-bottom:1px solid var(--db-border,var(--dsw-alias-border-l1));padding:10px 0}
1327
1385
  .db-list>li:last-child{border-bottom:0}
1328
- .db-kind{color:var(--dsw-alias-label-tertiary);min-width:96px;display:inline-block}
1329
- .db-error{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-label-error,#e5484d);border-radius:10px;color:var(--dsw-alias-label-error,#e5484d);padding:10px 12px;margin-bottom:10px;white-space:pre-wrap}
1330
- .db-grid{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
1331
- .db-shot{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px}
1332
- .db-shot img{display:block;width:100%;height:auto;border-radius:6px;background:#000}
1333
- .db-shot h5{margin:0 0 6px;font-size:12px;font-weight:600}
1334
- .db-chip{display:inline-flex;align-items:center;gap:5px;background:0 0;border:0;border-radius:8px;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:12px;padding:3px 6px}
1335
- .db-chip:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}
1336
- .db-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary);display:inline-block}
1337
- .db-dot[data-tone=live]{background:var(--dsw-alias-brand-primary,#3b6ef5)}
1386
+ .db-kind{color:var(--db-muted,var(--dsw-alias-label-tertiary));min-width:96px;display:inline-block}
1387
+ .db-error{background:var(--db-surface,var(--dsw-alias-bg-layer-1));border:1px solid var(--db-bad,var(--dsw-alias-label-error,#d13438));border-radius:9px;color:var(--db-bad,var(--dsw-alias-label-error,#d13438));padding:12px 14px;margin-bottom:12px;white-space:pre-wrap;overflow-wrap:anywhere}
1388
+ .db-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr))}
1389
+ .db-shot{background:var(--db-surface,var(--dsw-alias-bg-layer-1));border:1px solid var(--db-border,var(--dsw-alias-border-l1));border-radius:12px;padding:12px;min-width:0}
1390
+ .db-shot img{display:block;width:100%;height:auto;border-radius:8px;background:#0d1117}
1391
+ .db-shot h5{margin:0 0 10px;font-size:13px;font-weight:600}
1392
+ .db-chip{display:inline-flex;align-items:center;gap:6px;background:transparent;border:0;border-radius:7px;color:var(--db-subtle,var(--dsw-alias-label-secondary));cursor:pointer;font:inherit;font-size:12px;padding:6px 8px}
1393
+ .db-chip:hover{background:var(--db-fill,var(--dsw-alias-fill-l2));color:var(--db-text,var(--dsw-alias-label-primary))}
1394
+ .db-dot{width:7px;height:7px;border-radius:50%;background:var(--db-muted,var(--dsw-alias-label-tertiary));display:inline-block}
1395
+ .db-dot[data-tone=live]{background:var(--db-accent,var(--dsw-alias-brand-primary))}
1338
1396
  .db-dot[data-tone=ok]{background:var(--dsw-alias-label-success,#2f9e44)}
1339
- .db-dot[data-tone=bad]{background:var(--dsw-alias-label-error,#e5484d)}
1340
- .db-kv{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;font-size:12px;margin:0}
1341
- .db-kv dt{color:var(--dsw-alias-label-tertiary)}
1397
+ .db-dot[data-tone=bad]{background:var(--dsw-alias-label-error,#d13438)}
1398
+ .db-kv{display:grid;grid-template-columns:auto minmax(0,1fr);gap:7px 14px;font-size:12px;margin:0}
1399
+ .db-kv dt{color:var(--db-muted,var(--dsw-alias-label-secondary))}
1342
1400
  .db-kv dd{margin:0;font-family:var(--dsw-font-mono);overflow-wrap:anywhere}
1343
- .db-pre{background:var(--dsw-alias-bg-layer-1);border-radius:8px;font-family:var(--dsw-font-mono);font-size:11px;margin:6px 0 0;max-height:200px;overflow:auto;padding:8px;white-space:pre-wrap}
1401
+ .db-pre{background:var(--db-fill,var(--dsw-alias-bg-layer-1));border-radius:8px;font-family:var(--dsw-font-mono);font-size:11px;margin:8px 0 0;max-height:200px;overflow:auto;padding:12px;white-space:pre-wrap}
1402
+ .db-root details>summary{cursor:pointer;min-height:34px;padding:6px 0;font-weight:550;color:var(--db-subtle)}
1403
+ .db-root details[open]>summary{margin-bottom:8px;color:var(--db-text)}
1404
+ .db-guide{padding:12px 16px}
1405
+ .db-guide-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
1406
+ .db-guide-top>strong{font-size:12px;flex:none}
1407
+ .db-guide .db-btn{min-height:30px;padding:5px 9px;font-size:11px}
1408
+ .db-guide-notes{font-size:12px;margin-top:5px}
1409
+ .db-guide-notes>summary{font-size:11px;min-height:26px!important;padding:4px 0!important;font-weight:400!important}
1410
+ .db-guide-notes p{margin:4px 0 8px}
1411
+ .db-create-footer{position:sticky;bottom:0;z-index:2;background:var(--db-surface);border-top:1px solid var(--db-border);display:grid;grid-template-columns:minmax(140px,1fr) minmax(160px,1.2fr) auto;gap:12px;align-items:end;margin:16px -18px -16px;padding:14px 18px;border-radius:0 0 12px 12px;box-shadow:0 -5px 18px #00000008}
1412
+ .db-create-footer .db-btn{min-height:36px}
1413
+ .db-recipes{margin-bottom:16px}
1414
+ .db-recipe-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:14px}
1415
+ .db-recipe{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--db-border);border-radius:10px;padding:12px;background:var(--db-surface)}
1416
+ .db-recipe[data-selected=true]{border-color:var(--db-accent);box-shadow:0 0 0 1px var(--db-accent);background:var(--db-accent-soft)}
1417
+ .db-recipe img{display:block;width:100%;aspect-ratio:4/3;object-fit:contain;border-radius:7px;background:#10151d}
1418
+ .db-recipe h5{margin:2px 0;font-size:13px}
1419
+ .db-recipe p{margin:0;font-size:12px}
1420
+ .db-recipe .db-btn{margin-top:auto;align-self:stretch}
1421
+ .db-recipe-description{line-height:1.6}
1422
+ .db-recipe-license{font-size:10px!important}
1423
+ .db-recipe-parameters{margin-top:16px;padding:14px;border:1px solid var(--db-border);border-radius:9px}
1424
+ .db-recipe-parameters legend{font-size:12px;font-weight:600;padding:0 6px}
1425
+ .db-recipe-parameters .db-inline{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin-top:12px}
1426
+ .db-recipe-parameters label{padding:8px;background:var(--db-fill);border-radius:7px}
1427
+ .db-recipe-parameters input[type=color]{height:34px;width:64px;border:1px solid var(--db-border);border-radius:6px;padding:3px;background:var(--db-surface)}
1428
+ .db-storage{margin-top:8px;font-size:11px;overflow-wrap:anywhere}
1429
+ .db-scene-layout{display:grid;grid-template-columns:minmax(220px,300px) minmax(0,1fr);gap:20px;align-items:start}
1430
+ .db-scene-outline{min-width:0;position:sticky;top:0;max-height:calc(100dvh - 180px);overflow:auto;scrollbar-width:thin;scrollbar-color:var(--db-border) transparent}
1431
+ .db-scene-outline>.db-grid{display:block}
1432
+ .db-scene-outline .db-card{padding:12px 14px}
1433
+ .db-scene-outline .db-kind{min-width:0;display:inline;margin:0 6px}
1434
+ .db-scene-outline .db-list>li>div{overflow-wrap:anywhere;font-size:12px}
1435
+ .db-scene-inspector{min-width:0}
1436
+ .db-notice{border-left:3px solid var(--db-good,var(--dsw-alias-label-success,#2f9e44));background:var(--db-fill,var(--dsw-alias-fill-l2));border-radius:6px;padding:9px 12px;color:var(--db-text,var(--dsw-alias-label-primary))}
1437
+ .db-editor-actions{position:sticky;bottom:0;z-index:2;background:var(--db-surface);border-top:1px solid var(--db-border);margin:16px -18px -16px;padding:12px 18px;border-radius:0 0 12px 12px;box-shadow:0 -5px 18px #00000008}
1438
+ @media(prefers-color-scheme:dark){.db-root[data-theme=system]{color-scheme:dark;--db-accent:#f3a365;--db-accent-soft:#3c2d23;--db-canvas:#10151d;--db-surface:#19212d;--db-border:#334155;--db-fill:#253142;--db-text:#edf2f8;--db-subtle:#c2ccda;--db-muted:#a2afc1;--db-good:#77c9a0;--db-warn:#efbc73;--db-bad:#ff979c;--db-input-border:#657692}.db-root[data-theme=system] .db-btn[data-tone=primary]{color:#20170f}}
1439
+ @media(max-width:1000px){.db-recipe-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.db-head{padding:12px 16px}.db-body{padding:16px}.db-project-context{max-width:45%}.db-scene-layout{grid-template-columns:minmax(190px,240px) minmax(0,1fr);gap:14px}}
1440
+ @media(max-width:700px){.db-scene-layout{grid-template-columns:minmax(0,1fr)}.db-scene-outline{position:static;max-height:none;overflow:visible}.db-scene-outline>.db-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.db-project-context{max-width:100%;border-left:0;padding-left:0}.db-head-actions{margin-left:0;width:100%}.db-head-actions .db-theme{margin-left:auto}.db-head{gap:8px}.db-nav{padding:8px 12px}.db-create-footer{grid-template-columns:1fr 1fr}.db-create-footer .db-btn{grid-column:1/-1}.db-card{padding:14px}.db-body{padding:12px}.db-create-footer{margin:14px -14px -14px;padding:12px 14px}}
1441
+ @media(max-width:440px){.db-recipe-grid{grid-template-columns:minmax(0,1fr)}.db-create-footer{position:static;grid-template-columns:minmax(0,1fr)}.db-scene-outline>.db-grid{grid-template-columns:minmax(0,1fr)}.db-badge[data-badge=unsaved]{white-space:normal}.db-nav button{padding:6px 10px}.db-head{padding:10px 12px}.db-body{padding-top:8px}.db-tabs{gap:6px;padding:0 0 6px}}
1442
+ @container deepblend (max-width:1000px){.db-recipe-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.db-head{padding:12px 16px}.db-body{padding:16px}.db-project-context{max-width:45%}.db-scene-layout{grid-template-columns:minmax(190px,240px) minmax(0,1fr);gap:14px}}
1443
+ @container deepblend (max-width:700px){.db-scene-layout{grid-template-columns:minmax(0,1fr)}.db-scene-outline{position:static;max-height:none;overflow:visible}.db-scene-outline>.db-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.db-project-context{max-width:100%;border-left:0;padding-left:0}.db-head-actions{margin-left:0;width:100%}.db-head-actions .db-theme{margin-left:auto}.db-head{gap:8px}.db-nav{padding:8px 12px}.db-create-footer{grid-template-columns:1fr 1fr}.db-create-footer .db-btn{grid-column:1/-1}.db-card{padding:14px}.db-body{padding:12px}.db-create-footer{margin:14px -14px -14px;padding:12px 14px}}
1444
+ @container deepblend (max-width:440px){.db-recipe-grid{grid-template-columns:minmax(0,1fr)}.db-create-footer{position:static;grid-template-columns:minmax(0,1fr)}.db-scene-outline>.db-grid{grid-template-columns:minmax(0,1fr)}.db-badge[data-badge=unsaved]{white-space:normal}.db-nav button{padding:6px 10px}.db-head{padding:10px 12px}.db-body{padding-top:8px}.db-tabs{gap:6px;padding:0 0 6px}}
1445
+ @media(pointer:coarse){.db-root .db-btn,.db-root .db-input,.db-nav button{min-height:42px}}
1446
+ @media(prefers-reduced-motion:reduce){.db-root *{scroll-behavior:auto!important;transition:none!important}}
1344
1447
  `
1345
1448
 
1346
1449
  /**
@@ -1442,7 +1545,7 @@ window.__ModuleLoader__.load({
1442
1545
 
1443
1546
  function ErrorBox(props) {
1444
1547
  if (props.error === null || props.error === undefined) return null
1445
- return el('div', { className: 'db-error', 'data-deepblend-error': props.error.code || 'error' },
1548
+ return el('div', { className: 'db-error', role: 'alert', 'data-deepblend-error': props.error.code || 'error' },
1446
1549
  el('div', null, `${props.error.code || 'ERROR'}: ${props.error.message || ''}`),
1447
1550
  props.error.detail ? el('pre', { className: 'db-pre' }, JSON.stringify(props.error.detail, null, 2)) : null)
1448
1551
  }
@@ -2222,13 +2325,23 @@ window.__ModuleLoader__.load({
2222
2325
  *
2223
2326
  * @param {{ fetch?: typeof fetch, pollLiveMs?: number, pollIdleMs?: number }} [options]
2224
2327
  */
2328
+ const APPEARANCE_KEY = 'deepblend.workbench.appearance'
2329
+ const APPEARANCES = ['system', 'light', 'dark']
2330
+ function appearanceStorage(settings) {
2331
+ try { return settings.preferenceStorage ?? globalThis.localStorage } catch { return undefined }
2332
+ }
2333
+ function savedAppearance(storage) {
2334
+ try { const value = storage?.getItem(APPEARANCE_KEY); return APPEARANCES.includes(value) ? value : 'system' } catch { return 'system' }
2335
+ }
2336
+
2225
2337
  function createWorkbenchStore(options) {
2226
2338
  const settings = options || {}
2227
2339
  const fetchImpl = settings.fetch ?? ((...args) => fetch(...args))
2228
2340
  const pollLiveMs = settings.pollLiveMs ?? POLL_LIVE_MS
2229
2341
  const pollIdleMs = settings.pollIdleMs ?? POLL_IDLE_MS
2230
2342
 
2231
- let data = emptySnapshot()
2343
+ const preferenceStorage = appearanceStorage(settings)
2344
+ let data = { ...emptySnapshot(), appearance: savedAppearance(preferenceStorage) }
2232
2345
  let snapshot = { ...data }
2233
2346
  const listeners = new Set()
2234
2347
  let timer = null
@@ -2405,6 +2518,11 @@ window.__ModuleLoader__.load({
2405
2518
  && FILE_FIELDS.includes(field) && source === fileChoiceKey(data, field) && fileChoiceAllowed(data, field)
2406
2519
  const actions = {
2407
2520
  acceptsFileChoice,
2521
+ setAppearance: value => {
2522
+ if (!APPEARANCES.includes(value)) return
2523
+ try { preferenceStorage?.setItem(APPEARANCE_KEY, value) } catch { /* A blocked preference must not block a project. */ }
2524
+ set({ appearance: value })
2525
+ },
2408
2526
  /**
2409
2527
  * Switch tabs, and drop the notice the tab being ENTERED was carrying.
2410
2528
  *
@@ -3188,7 +3306,7 @@ window.__ModuleLoader__.load({
3188
3306
  const actions = ctx.actions
3189
3307
  return el('div', { 'data-view': 'projects' },
3190
3308
  ErrorBox({ error: state.error }),
3191
- el('div', { className: 'db-card' },
3309
+ state.projects.length ? el('div', { className: 'db-card db-project-list' },
3192
3310
  el('h4', null, t('tab.projects')),
3193
3311
  state.projects.length === 0
3194
3312
  ? el('div', { className: 'db-muted' }, t('projects.empty'))
@@ -3207,13 +3325,14 @@ window.__ModuleLoader__.load({
3207
3325
  el('div', { className: 'db-muted db-mono' }, t('projects.updated', { revision: project.currentRevision || '—', when: formatTime(project.updatedAt) })),
3208
3326
  project.goal ? el('div', { className: 'db-muted' }, project.goal) : null,
3209
3327
  ))),
3210
- el('div', { className: 'db-muted db-mono', style: { marginTop: '6px' } }, `projectsRoot: ${state.projectsRoot || '—'}`),
3211
- ),
3328
+ ) : null,
3212
3329
 
3213
- el('div', { className: 'db-card' },
3330
+ el('div', { className: 'db-card db-project-create' },
3214
3331
  el('h4', null, t('projects.create')),
3332
+ !state.projects.length ? el('p', { className: 'db-muted' }, t('projects.startHint')) : null,
3215
3333
  RecipesView(ctx),
3216
- el('div', { className: 'db-inline' },
3334
+ el('div', { className: 'db-create-footer' },
3335
+ el('label', { className: 'db-field' }, t('projects.name'),
3217
3336
  el('input', {
3218
3337
  className: 'db-input',
3219
3338
  'data-field': 'project-title',
@@ -3221,15 +3340,15 @@ window.__ModuleLoader__.load({
3221
3340
  placeholder: t('projects.titlePlaceholder'),
3222
3341
  value: state.forms.title,
3223
3342
  onChange: event => actions.setForm('title', event.target.value),
3224
- }),
3225
- el('input', {
3343
+ })),
3344
+ el('label', { className: 'db-field' }, t('projects.goal'), el('input', {
3226
3345
  className: 'db-input',
3227
3346
  'data-field': 'project-goal',
3228
3347
  'aria-label': t('projects.goal'),
3229
3348
  placeholder: t('projects.goal'),
3230
3349
  value: state.forms.goal,
3231
3350
  onChange: event => actions.setForm('goal', event.target.value),
3232
- }),
3351
+ })),
3233
3352
  Button({
3234
3353
  tone: 'primary',
3235
3354
  action: 'create-project',
@@ -3240,6 +3359,8 @@ window.__ModuleLoader__.load({
3240
3359
  ),
3241
3360
  state.notices.projects ? Notice(state.notices.projects, { marginTop: '8px' }) : null,
3242
3361
  ),
3362
+ el('details', { className: 'db-storage' }, el('summary', null, t('projects.storage')),
3363
+ el('span', { className: 'db-muted db-mono' }, `projectsRoot: ${state.projectsRoot || '—'}`)),
3243
3364
 
3244
3365
  BriefEditor({ state, actions }),
3245
3366
  state.selected ? el('div', { className: 'db-card' },
@@ -3257,7 +3378,7 @@ window.__ModuleLoader__.load({
3257
3378
  ].join(' / ') },
3258
3379
  ] }),
3259
3380
  el('div', { className: 'db-muted', style: { marginTop: '6px' } }, state.selected.qa.summary),
3260
- ) : el('div', { className: 'db-card db-muted' }, t('common.noProject')),
3381
+ ) : null,
3261
3382
  )
3262
3383
  }
3263
3384
 
@@ -3289,26 +3410,25 @@ window.__ModuleLoader__.load({
3289
3410
  function RecipesView({ state, actions }) {
3290
3411
  const catalog = state.recipeCatalog || { recipes: [], errors: [] }
3291
3412
  const selected = state.forms.recipe
3292
- return el('section', { 'aria-label': t('recipes.heading'), style: { marginBottom: '16px' } },
3413
+ return el('section', { className: 'db-recipes', 'aria-label': t('recipes.heading') },
3293
3414
  el('h5', null, t('recipes.heading')),
3294
3415
  el('p', { className: 'db-muted' }, t('recipes.previewNote')),
3295
3416
  !recipeSelectionCurrent(state) ? el('p', { role: 'alert', className: 'db-error' }, t('recipes.stale')) : null,
3296
3417
  catalog.errors.length ? el('p', { className: 'db-error' }, t('recipes.unavailable')) : null,
3297
- Button({ action: 'recipe-blank', tone: !selected ? 'primary' : undefined,
3418
+ Button({ action: 'recipe-blank',
3298
3419
  onClick: () => actions.selectRecipe(null), children: t('recipes.blank') }),
3299
- el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px', marginTop: '10px' } },
3420
+ el('div', { className: 'db-recipe-grid' },
3300
3421
  catalog.recipes.map(recipe => {
3301
3422
  const chosen = selected?.id === recipe.id && selected?.version === recipe.version && selected?.digest === recipe.digest
3302
- return el('article', { key: `${recipe.id}@${recipe.version}`, 'data-recipe': recipe.id,
3303
- style: { border: chosen ? '2px solid var(--db-accent, #4f8cff)' : '1px solid var(--db-border, #555)', borderRadius: '8px', padding: '10px' } },
3304
- el('img', { src: recipe.previewUrl, alt: recipe.title, loading: 'lazy', style: { width: '100%', aspectRatio: '4 / 3', objectFit: 'contain', borderRadius: '4px' } }),
3305
- el('h5', null, recipe.title), el('p', { className: 'db-muted' }, recipe.description),
3306
- el('p', { className: 'db-muted' }, `${t('recipes.license')}: ${recipe.author.name} · ${recipe.license} · v${recipe.version}`),
3423
+ return el('article', { key: `${recipe.id}@${recipe.version}`, 'data-recipe': recipe.id, className: 'db-recipe', 'data-selected': String(chosen) },
3424
+ el('img', { src: recipe.previewUrl, alt: recipe.title, loading: 'lazy' }),
3425
+ el('h5', null, recipe.title), el('p', { className: 'db-muted db-recipe-description' }, recipe.description),
3426
+ el('p', { className: 'db-muted db-recipe-license' }, `${t('recipes.license')}: ${recipe.author.name} · ${recipe.license} · v${recipe.version}`),
3307
3427
  Button({ action: `select-recipe:${recipe.id}@${recipe.version}`, tone: chosen ? 'primary' : undefined,
3308
3428
  onClick: () => actions.selectRecipe(recipe), children: chosen ? t('recipes.selected') : t('recipes.select') }),
3309
3429
  )
3310
3430
  })),
3311
- selected ? el('fieldset', { style: { marginTop: '12px', borderRadius: '6px' } },
3431
+ selected ? el('fieldset', { className: 'db-recipe-parameters' },
3312
3432
  el('legend', null, t('recipes.parameters')),
3313
3433
  Button({ action: 'reset-recipe', onClick: () => actions.selectRecipe(selected), children: t('recipes.reset') }),
3314
3434
  el('div', { className: 'db-inline', style: { flexWrap: 'wrap' } }, selected.parameters.map(parameter =>
@@ -3405,7 +3525,10 @@ window.__ModuleLoader__.load({
3405
3525
  return el('div', {
3406
3526
  'data-result': result.ok ? 'ok' : 'error',
3407
3527
  'data-result-kind': result.kind || undefined,
3408
- className: result.ok ? 'db-muted' : 'db-error',
3528
+ className: result.ok ? 'db-notice' : 'db-error',
3529
+ role: result.ok ? 'status' : 'alert',
3530
+ 'aria-live': result.ok ? 'polite' : 'assertive',
3531
+ 'aria-atomic': 'true',
3409
3532
  style: style || undefined,
3410
3533
  }, result.message)
3411
3534
  }
@@ -3441,11 +3564,15 @@ window.__ModuleLoader__.load({
3441
3564
  const numeric = (label, field, value, update, options = {}) => {
3442
3565
  const factor = options.factor ?? 1
3443
3566
  const displayed = Number.isFinite(value) ? Math.round(value * factor * 1e9) / 1e9 : ''
3444
- return el('label', { className: 'db-row', key: field }, el('span', null, label), el('input', {
3445
- className: 'db-input', type: 'number', 'data-field': field, value: displayed, step: options.step ?? (options.integer ? 1 : 'any'),
3567
+ const invalid = !Number.isFinite(value) && !disabled && options.disabled !== true
3568
+ const errorId = `${draft.projectId}-${draft.entityId}-${field}-error`
3569
+ return el('label', { className: 'db-row', key: field }, el('span', null, label), el('span', { className: 'db-number-control' }, el('input', {
3570
+ className: 'db-input', type: 'number', 'data-field': field, 'aria-label': label,
3571
+ 'aria-invalid': invalid ? 'true' : undefined, 'aria-describedby': invalid ? errorId : undefined,
3572
+ value: displayed, step: options.step ?? (options.integer ? 1 : 'any'),
3446
3573
  min: options.min, max: options.max, disabled: disabled || options.disabled === true,
3447
3574
  style: { width: '125px' }, onChange: event => update(event.target.value === '' ? null : Number(event.target.value) / factor),
3448
- }))
3575
+ }), invalid ? el('span', { className: 'db-field-feedback', id: errorId }, t('editor.invalid', { field: label })) : null))
3449
3576
  }
3450
3577
  const choose = (label, field, value, items, update, off = false) => el('label', { className: 'db-row', key: field }, el('span', null, label), el('select', {
3451
3578
  className: 'db-input', 'data-field': field, value, disabled: disabled || off, style: { maxWidth: '65%' }, onChange: event => update(event.target.value),
@@ -3590,7 +3717,7 @@ window.__ModuleLoader__.load({
3590
3717
  el('div', { className: 'db-inline' }, Object.entries(modifierNames).map(([type, name]) => Button({ action: `editor-modifier-add-${type}`, disabled: disabled || !canModify || draft.entity.modifiers.length >= 8,
3591
3718
  children: t('editor.add', { name }), onClick: () => actions.editEditorList('modifiers', 0, 'add', type) })))) : null,
3592
3719
  errors.length ? el('div', { className: 'db-error', 'data-editor-errors': true }, errors.join(' ')) : null,
3593
- el('div', { className: 'db-inline' }, Button({ action: 'editor-apply', tone: 'primary', disabled: disabled || !dirty || errors.length > 0, onClick: actions.applyEditor, children: t('editor.apply') }),
3720
+ el('div', { className: 'db-inline db-editor-actions' }, Button({ action: 'editor-apply', tone: 'primary', disabled: disabled || !dirty || errors.length > 0, onClick: actions.applyEditor, children: t('editor.apply') }),
3594
3721
  Button({ action: 'editor-reset', disabled: state.busy.editor, onClick: actions.resetEditor, children: t('editor.reset') }),
3595
3722
  Button({ action: 'editor-review-subject', disabled: disabled || draft.entity.visible === false || draft.entity.kind === 'empty'
3596
3723
  || state.briefWork[state.activeProjectId]?.saving || state.visualRuns[state.activeProjectId]?.busy,
@@ -3827,10 +3954,12 @@ window.__ModuleLoader__.load({
3827
3954
  Badge({ children: `world ${scene.world ? `${(scene.world.color || []).join(',')} × ${scene.world.strength}` : t('common.default')}` }),
3828
3955
  ),
3829
3956
  ),
3957
+ el('div', { className: 'db-scene-layout' },
3958
+ el('aside', { className: 'db-scene-outline', 'data-scroll-key': JSON.stringify([state.activeProjectId, 'scene-outline']), 'aria-label': t('scene.outline') },
3830
3959
  el('div', { className: 'db-grid' },
3831
3960
  section(t('scene.entities'), scene.nodes.entities, entity => el('div', null,
3832
3961
  el('button', { type: 'button', className: 'db-btn', 'data-action': `select-entity:${entity.id}`, 'data-entity-id': entity.id,
3833
- 'aria-pressed': state.editorEntityId === entity.id, onClick: () => actions.selectEditorEntity(entity.id) }, entity.id), ' ',
3962
+ 'aria-pressed': String(state.editorEntityId === entity.id), onClick: () => actions.selectEditorEntity(entity.id) }, entity.id), ' ',
3834
3963
  el('span', { className: 'db-mono', 'data-node': `entity:${entity.id}` }, entity.id), ' ',
3835
3964
  el('span', { className: 'db-kind' }, entity.shape || entity.kind),
3836
3965
  entity.materialId ? el('span', { className: 'db-muted' }, t('scene.material', { id: entity.materialId })) : null,
@@ -3863,10 +3992,11 @@ window.__ModuleLoader__.load({
3863
3992
  el('span', { className: 'db-mono', 'data-node': `asset:${asset.id}` }, asset.id), ' ',
3864
3993
  el('span', { className: 'db-kind' }, asset.type),
3865
3994
  el('span', { className: 'db-muted db-mono' }, String(asset.path || '')))),
3866
- ),
3995
+ )),
3996
+ el('section', { className: 'db-scene-inspector', 'aria-label': t('scene.inspector') },
3997
+ SceneEditor(ctx),
3867
3998
  PhotographyEditor(ctx),
3868
3999
  AssetsView(ctx),
3869
- SceneEditor(ctx),
3870
4000
  el('details', { key: advancedDisclosure, 'data-disclosure': advancedDisclosure, open: state.disclosures?.[advancedDisclosure] ?? false,
3871
4001
  onToggle: event => actions.setDisclosure(advancedDisclosure, event.currentTarget.open), className: 'db-card' }, el('summary', null, t('editor.advanced')),
3872
4002
  el('h4', null, t('scene.patch')),
@@ -3888,7 +4018,7 @@ window.__ModuleLoader__.load({
3888
4018
  Button({ action: 'reset-patch', onClick: () => actions.setForm('patch', null), children: t('scene.resetTemplate') }),
3889
4019
  Notice(state.notices.scene, { border: 0, padding: '0 6px', marginBottom: 0 }),
3890
4020
  ),
3891
- ),
4021
+ ))),
3892
4022
  )
3893
4023
  }
3894
4024
 
@@ -4050,15 +4180,18 @@ window.__ModuleLoader__.load({
4050
4180
  { id: 'appearance', label: t('guide.appearance'), hint: t('guide.appearanceHint') },
4051
4181
  { id: 'delivery', label: t('guide.delivery'), hint: t('guide.deliveryHint') },
4052
4182
  ]
4053
- return el('section', { className: 'db-card', 'data-creation-guide': projectId || 'new' },
4054
- el('strong', null, t('guide.title')),
4055
- el('div', { className: 'db-inline', style: { flexWrap: 'wrap', marginTop: '8px' } }, steps.map(step =>
4056
- Button({ action: `guide-${step.id}`, disabled: !projectId && !['goal', 'route'].includes(step.id), onClick: () => actions.openCreationStep(step.id), children: step.label }))),
4183
+ const disclosure = JSON.stringify([projectId || 'new', 'guide-notes'])
4184
+ return el('section', { className: 'db-card db-guide', 'data-creation-guide': projectId || 'new' },
4185
+ el('div', { className: 'db-guide-top' }, el('strong', null, t('guide.title')),
4186
+ el('div', { className: 'db-inline' }, steps.map(step =>
4187
+ Button({ action: `guide-${step.id}`, disabled: !projectId && !['goal', 'route'].includes(step.id), onClick: () => actions.openCreationStep(step.id), children: step.label })))),
4188
+ el('details', { className: 'db-guide-notes', 'data-disclosure': disclosure, open: state.disclosures?.[disclosure] ?? false,
4189
+ onToggle: event => actions.setDisclosure(disclosure, event.currentTarget.open) }, el('summary', null, t('guide.notes')),
4057
4190
  el('p', { className: 'db-muted', 'data-guide-hint': focus }, steps.find(step => step.id === focus)?.hint || steps[0].hint),
4058
4191
  el('p', { className: 'db-muted' }, scene ? t('guide.evidence', { revision: scene.revision,
4059
4192
  parts: scene.nodes.entities.filter(entity => entity.kind !== 'empty' && !entity.tags?.includes('environment')).length,
4060
4193
  references: scene.project.referenceImages?.length || 0, images: evidence?.diagnostics?.length || 0 }) : t('guide.noProject')),
4061
- el('small', { className: 'db-muted' }, t('guide.help')))
4194
+ el('small', { className: 'db-muted' }, t('guide.help'))))
4062
4195
  }
4063
4196
  function InspectionPanel({ state, actions }) {
4064
4197
  const projectId = state.activeProjectId, scene = state.selected?.scene, form = inspectionForm(state), work = state.inspectionWork?.[projectId] || {}
@@ -4329,12 +4462,12 @@ window.__ModuleLoader__.load({
4329
4462
  el('h4', null, t('jobs.startDelivery')),
4330
4463
  el('div', { className: 'db-inline' },
4331
4464
  el('label', { className: 'db-muted' }, t('jobs.frameStart')),
4332
- el('input', { className: 'db-input', 'data-field': 'frame-start', style: { width: '90px' }, value: state.forms.frameStart, onChange: event => actions.setForm('frameStart', event.target.value) }),
4465
+ el('input', { className: 'db-input', 'data-field': 'frame-start', 'aria-label': t('jobs.frameStart'), style: { width: '90px' }, value: state.forms.frameStart, onChange: event => actions.setForm('frameStart', event.target.value) }),
4333
4466
  el('label', { className: 'db-muted' }, t('jobs.frameEnd')),
4334
- el('input', { className: 'db-input', 'data-field': 'frame-end', style: { width: '90px' }, value: state.forms.frameEnd, onChange: event => actions.setForm('frameEnd', event.target.value) }),
4335
- el('label', { className: 'db-muted' }, 'profile'),
4467
+ el('input', { className: 'db-input', 'data-field': 'frame-end', 'aria-label': t('jobs.frameEnd'), style: { width: '90px' }, value: state.forms.frameEnd, onChange: event => actions.setForm('frameEnd', event.target.value) }),
4468
+ el('label', { className: 'db-muted' }, t('jobs.profile')),
4336
4469
  el('select', {
4337
- className: 'db-input', 'data-field': 'render-profile', style: { width: 'auto' }, value: state.forms.profile,
4470
+ className: 'db-input', 'data-field': 'render-profile', 'aria-label': t('jobs.profile'), style: { width: 'auto' }, value: state.forms.profile,
4338
4471
  onChange: event => actions.setForm('profile', event.target.value),
4339
4472
  },
4340
4473
  el('option', { value: 'preview' }, 'preview'),
@@ -4503,21 +4636,28 @@ window.__ModuleLoader__.load({
4503
4636
  : state.status === 'loading'
4504
4637
  ? el('div', { className: 'db-body db-muted' }, t('host.reading'))
4505
4638
  : el('div', { className: 'db-body', 'data-scroll-key': JSON.stringify([state.activeProjectId, state.view]) },
4506
- state.view === 'preview' ? renderView(ctx) : CreationGuide(ctx),
4507
- state.view === 'preview' ? CreationGuide(ctx) : renderView(ctx))
4639
+ state.view === 'preview' || !state.activeProjectId ? renderView(ctx) : CreationGuide(ctx),
4640
+ state.view === 'preview' || !state.activeProjectId ? CreationGuide(ctx) : renderView(ctx))
4508
4641
 
4509
- return el('div', { className: 'db-root', 'data-deepblend-panel': PANEL_ID },
4642
+ return el('div', { className: 'db-root', 'data-theme': state.appearance || 'system', 'data-deepblend-panel': PANEL_ID },
4510
4643
  el('div', { className: 'db-head' },
4511
- el('span', { className: 'db-title' }, t('workbench.title', { panel: PANEL_LABEL })),
4512
- state.selected ? el('span', { className: 'db-muted' }, state.selected.project.title) : null,
4513
- state.selected ? Badge({ children: state.selected.currentRevision || '—' }) : null,
4644
+ el('div', { className: 'db-brand' },
4645
+ el('img', { className: 'db-logo', src: BRAND_MARK, alt: '', width: 34, height: 34 }),
4646
+ el('div', null, el('h1', { className: 'db-title' }, 'DeepBlend Studio'), el('small', null, t('workbench.title', { panel: PANEL_LABEL })))),
4647
+ state.selected ? el('div', { className: 'db-project-context' },
4648
+ el('span', { className: 'db-project-name', title: state.selected.project.title }, state.selected.project.title),
4649
+ Badge({ children: state.selected.currentRevision || '—' })) : null,
4650
+ projectHasUnsavedDrafts(state) ? Badge({ tone: 'warn', name: 'unsaved', children: t('workbench.unsaved') }) : null,
4514
4651
  state.unfinishedJobs.length > 0
4515
4652
  ? Badge({ tone: 'live', name: 'unfinished', children: t('jobs.unfinished', { count: state.unfinishedJobs.length }) })
4516
4653
  : null,
4517
4654
  typeof state.hostApiVersion === 'number' && state.hostApiVersion < EXPECTED_HOST_API
4518
4655
  ? Badge({ tone: 'warn', name: 'api', children: `hostApiVersion ${state.hostApiVersion} < ${EXPECTED_HOST_API}` })
4519
4656
  : null,
4520
- el('span', { style: { flex: 1 } }),
4657
+ el('div', { className: 'db-head-actions' },
4658
+ el('select', { className: 'db-input db-theme', 'data-field': 'workbench-appearance', 'aria-label': t('workbench.appearance'), value: state.appearance || 'system',
4659
+ onChange: event => actions.setAppearance(event.target.value) },
4660
+ [['system', t('workbench.system')], ['light', t('workbench.light')], ['dark', t('workbench.dark')]].map(([theme, label]) => el('option', { value: theme }, label))),
4521
4661
  // AN ANCHOR, NOT A BUTTON. The export is a download, and a download is what an anchor with
4522
4662
  // `download` does: the browser fetches the route and writes the file itself, in both faces
4523
4663
  // (React passes `href`/`download` through, the DOM binding sets them as attributes). A button
@@ -4529,13 +4669,15 @@ window.__ModuleLoader__.load({
4529
4669
  'data-action': 'export-diagnostics',
4530
4670
  title: t('diagnostics.exportHint'),
4531
4671
  }, t('diagnostics.export')),
4532
- Button({ action: 'reload', onClick: actions.reload, children: t('common.refresh') }),
4672
+ Button({ action: 'reload', onClick: actions.reload, children: t('common.refresh') })),
4533
4673
  ),
4534
- el('div', { className: 'db-nav' }, VIEWS.map(entry => el('button', {
4674
+ el('nav', { className: 'db-nav', 'aria-label': t('workbench.navigation') }, VIEWS.map(entry => el('button', {
4535
4675
  key: entry.id,
4536
4676
  type: 'button',
4537
4677
  'data-view-tab': entry.id,
4538
4678
  'data-active': String(state.view === entry.id),
4679
+ 'aria-current': state.view === entry.id ? 'page' : undefined,
4680
+ 'data-field': `nav-${entry.id}`,
4539
4681
  onClick: () => actions.setView(entry.id),
4540
4682
  }, entry.label))),
4541
4683
  state.error !== null && state.error !== undefined && !stale ? el('div', { style: { padding: '8px 14px 0' } }, ErrorBox({ error: state.error })) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "DeepBlend workbench UI: Host half (typed HTTP API) and Web Client half (sidebar, panels, tool cards).",
6
6
  "main": "lib/index.js",
@@ -19,7 +19,7 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@deepblend/dsh-blender-contracts": "0.3.1"
22
+ "@deepblend/dsh-blender-contracts": "0.3.2"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {