dsh-design-studio 0.11.0 → 0.12.0

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/lib/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dsh-design-studio 的客户端部分
3
3
  *
4
- * 右侧栏标签页「设计工作台」(sidebarRightTabs + sidebar.right.pane.tab)
4
+ * 右侧栏标签页「设计台」(sidebarRightTabs + sidebar.right.pane.tab)
5
5
  * - 工具栏:地址(点一下能改)、选择、画布 / 单页、类型(网页 / App / 平板 / 海报 / 幻灯片 / 邮件)、刷新、在浏览器打开
6
6
  * - 画布:同一个目录里的几屏按生成顺序排成网格,套上对应的外框(手机、浏览器窗口 …),能拖、能缩放;
7
7
  * 模型还在出新的一屏时,末尾留一个「生成中…」的占位
@@ -40,13 +40,47 @@ window.__ModuleLoader__.load({
40
40
  const API = '/dsh-design-studio/api/'
41
41
 
42
42
  const ZH = {
43
- title: '设计工作台',
44
- guideDesc: '实时看页面,点选要改的地方',
43
+ title: '设计台',
44
+ tabTitle: '设计台',
45
+ guideDesc: '做网页、App、海报,点哪改哪,还能转成代码',
45
46
  blockedLink: '已拦下外链:{href}(设计库来的设计不跳到别的网站)',
46
47
  blockedDownload: '已拦下下载(设计库来的设计不能下载文件)',
47
48
  empty: '还没有要预览的页面',
48
- emptyHint: '也可以直接输入 HTML 文件路径或端口:',
49
- emptyHintPick: '已有 HTML?点「浏览…」选一个,或直接拖进来;也能输入路径或端口:',
49
+ emptyHead: '想做点什么?',
50
+ emptySub: '说一句,设计会一屏一屏出现在画布上;点哪改哪,做完直接转成代码。',
51
+ homeTab: '开始',
52
+ askPh: '比如:{ex}',
53
+ askTab: '用这句',
54
+ exAuto: '巷口面馆的点单小程序,三屏,暖色手作风',
55
+ exWeb: '独立咖啡馆的官网首页:招牌豆子、门店地图、在线预订',
56
+ exApp: '社区健身房的会员 App:约课、打卡、会员卡',
57
+ exMini: '巷口面馆的点单小程序:菜单、购物车、取餐号',
58
+ exTablet: '幼儿园老师用的平板 App:点名、课表、家长消息',
59
+ exDesktop: '小团队记账的桌面软件:流水、报表、设置',
60
+ exPoster: '周末城市夜跑活动的海报,清爽运动风',
61
+ exSlides: '新品发布会的五页幻灯片:问题、方案、数字、价格、下一步',
62
+ exEmail: '会员生日月的优惠邮件:一张大图、一段祝福、一个按钮',
63
+ askKindLine: '类型:{k}',
64
+ askMore: '更多开始方式',
65
+ askMoreD: '打开已有的 HTML、用自己的品牌、从设计库打开',
66
+ askOpenHtml: '打开已有的 HTML…',
67
+ askBrand: '用自己的品牌',
68
+ askLib: '从设计库打开',
69
+ askRef: '参考图',
70
+ askRefD: '照一张截图或设计稿做:选一张,也能直接拖进来、粘贴',
71
+ askStyleAuto: '风格:自动',
72
+ askStyle: '风格:{name}',
73
+ askStyleD: '先定个风格;不选就让模型自己定',
74
+ askStylePick: '挑个风格',
75
+ askStyleAutoBtn: '自动(让模型定)',
76
+ askSend: '开始设计',
77
+ askSent: '发出去了,第一屏马上出来',
78
+ homeRecent: '接着做',
79
+ homeRecentAll: '全部作品 →',
80
+ waysHtml: '打开已有的 HTML',
81
+ moreWays: '其他开始方式',
82
+ emptyHint: '输入文件路径,或本地端口(比如 5173)',
83
+ emptyHintPick: '点「浏览…」选一个、直接拖进来,或输入文件路径 / 本地端口(比如 5173)',
50
84
  browse: '浏览…',
51
85
  browseTip: '选一个本地 HTML 文件打开',
52
86
  pathNone: '拿不到这个文件的路径,请直接输入它的路径打开',
@@ -96,7 +130,7 @@ window.__ModuleLoader__.load({
96
130
  refPick: '照参考图改风格…',
97
131
  refChoose: '选一张',
98
132
  refPickTip: '选一张参考图,照它的风格改(也可以把图片拖进预览区,或在这里粘贴)',
99
- refEmpty: '有参考图?拖进来或粘贴,照它的风格做',
133
+ refEmpty: '照参考图做:拖进来、粘贴,或',
100
134
  refDrop: '松开,作为参考图',
101
135
  refDropSub: '存进设计目录的 refs/,提取配色放进输入框',
102
136
  refNotImage: '只认图片(PNG / JPG / WebP / GIF)',
@@ -142,9 +176,10 @@ window.__ModuleLoader__.load({
142
176
  gShadow: '阴影',
143
177
  gOther: '其他',
144
178
  exportT: '导出',
145
- exFrame: '这一屏 · 带外框',
146
- exRaw: '这一屏 · 纯画面',
147
- exLong: '这一屏 · 整页长图',
179
+ exFrame: '{name} · 带外框',
180
+ exRaw: '{name} · 纯画面',
181
+ exLong: '{name} · 整页长图',
182
+ exThisName: '这一屏',
148
183
  exShow: '整套展示图',
149
184
  exPdf: 'PDF · 每屏一页',
150
185
  exPptx: 'PPT · 每屏一页(整页图片)',
@@ -155,7 +190,8 @@ window.__ModuleLoader__.load({
155
190
  checking: '正在体检…',
156
191
  checkNone: '没发现问题',
157
192
  checkSum: '{s} 屏 · {n} 个问题',
158
- checkFix: '交给模型修',
193
+ checkFix: '发到对话',
194
+ checkFixTip: '把这些问题发到对话,让模型修',
159
195
  checkAgain: '再查一次',
160
196
  checkFail: '没查成:{msg}',
161
197
  checkNote: '只量得出排版上的硬伤,好不好看还得你看',
@@ -164,7 +200,7 @@ window.__ModuleLoader__.load({
164
200
  checkTail: '请逐个修好:改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量。',
165
201
  ckToolHead: '量了 {s} 屏,发现 {n} 个问题。逐个改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量:',
166
202
  ckToolOk: '量了 {s} 屏({labels}),没发现问题。',
167
- ckToolOff: '用户正在看旧版本、演示或多尺寸,右侧栏现在量不了;过一会儿再调,或照「自查」清单自己过一遍。',
203
+ ckToolOff: '用户正在看旧版本、试用或多尺寸,右侧栏现在量不了;过一会儿再调,或照「自查」清单自己过一遍。',
168
204
  ckToolErr: '{label}:没量成({msg})',
169
205
  ck_hscroll: '超出屏幕',
170
206
  ck_clipped: '文字被截断',
@@ -232,7 +268,7 @@ window.__ModuleLoader__.load({
232
268
  exportedCopied: '已导出 {name},也复制到剪贴板了',
233
269
  exportFail: '导出失败:{msg}',
234
270
  reveal: '打开文件夹',
235
- madeWith: '用 DSH 设计工作台做的',
271
+ madeWith: '用 DSH 设计台做的',
236
272
  showSub: '{n} 屏 · {kind} · {date}',
237
273
  history: '版本',
238
274
  histTip: '每轮对话前后各存一版。点一版看当时的样子,还能和现在左右对比',
@@ -254,19 +290,21 @@ window.__ModuleLoader__.load({
254
290
  cmpNow: '现在',
255
291
  dark: '深色模式',
256
292
  variants: '再出两版',
257
- pickThis: '就要这个:其他版本挪进 drafts/,接着做全套',
258
- pickShort: '就要这个',
293
+ pickThis: '用这版:其他版本挪进 drafts/,接着做全套',
294
+ pickShort: '用这版',
295
+ takeN: '第 {l} 版',
259
296
  pickText: '〔选定方向〕就用「{name}」({where})。先把同组的 {others} 挪进 drafts/ 子目录(别删),再把它改名成 {base}.html;它的设计变量搬进 tokens.css(没有就新建)、改用 tokens.css。然后接着做全套。\n',
260
297
  pickKeep: '〔选定方向〕留原版「{name}」({where})。同组的 {others} 挪进 drafts/ 子目录(别删),其他不动。\n',
261
298
  variantText: '给「{name}」再出两版不同方向的设计(布局或视觉风格明显不同),存成同目录下的 {b}-b.html 和 {b}-c.html(原版:{where}),会紧挨着原版排在画布上。原版和 tokens.css 别动;新版要换配色就在自己文件里覆盖变量。\n',
262
299
  exThis: '导出这一屏',
263
- demo: '演示:点页面里的按钮跳到别的屏',
264
- demoOn: '演示中 · 点按钮跳转',
300
+ demo: '试用:点页面里的按钮跳到别的屏',
301
+ demoOn: '试用中',
302
+ demoHow: '点按钮跳屏',
265
303
  demoBack: '返回上一屏',
266
304
  demoRestart: '从头开始',
267
- demoExit: '退出演示(Esc)',
305
+ demoExit: '退出试用',
268
306
  textEdit: '改字:点页面里的文字直接改,回车保存(Esc 退出)',
269
- mark: '画笔标注:在页面上画圈、箭头、写字,拼成一张图交给模型',
307
+ mark: '画笔标注:在页面上画圈、箭头、写字,拼成一张图发到对话',
270
308
  mkPen: '画笔',
271
309
  mkHl: '荧光笔',
272
310
  mkArrow: '箭头',
@@ -279,7 +317,7 @@ window.__ModuleLoader__.load({
279
317
  mkUndo: '撤销',
280
318
  mkRedo: '重做',
281
319
  mkClear: '全部清掉',
282
- mkHint: '在页面上画,画完点「交给模型」',
320
+ mkHint: '在页面上画,画完点「发到对话」',
283
321
  mkHow_pen: '按住拖动,随手画',
284
322
  mkHow_hl: '按住拖过去,把要改的地方涂亮',
285
323
  mkHow_arrow: '从起点按住,拖到要指的地方',
@@ -290,13 +328,13 @@ window.__ModuleLoader__.load({
290
328
  mkCoach: '直接在页面上画',
291
329
  mkCoach1: '上面挑个工具:画笔、箭头、圆圈、文字…',
292
330
  mkCoach2: '在页面上按住拖动,画出要改的地方',
293
- mkCoach3: '画完点「交给模型」,再补一句要怎么改',
331
+ mkCoach3: '画完点「发到对话」,再补一句要怎么改',
294
332
  mkCancel: '取消',
295
- mkSend: '交给模型',
333
+ mkSend: '发到对话',
296
334
  mkCopy: '复制',
297
335
  mkCopyTip: '把标注图复制到剪贴板,贴到别的地方用',
298
336
  mkCopied: '标注图复制好了,贴到别处就行',
299
- mkCopyFail: '没能复制到剪贴板,再点一次「复制」,或者「交给模型」',
337
+ mkCopyFail: '没能复制到剪贴板,再点一次「复制」,或者「发到对话」',
300
338
  mkSending: '正在拼图…',
301
339
  mkTextPh: '写点什么,回车完成',
302
340
  mkEmpty: '还没画东西',
@@ -307,14 +345,14 @@ window.__ModuleLoader__.load({
307
345
  mkIn: '标注图放进输入框了,补一句要怎么改再发',
308
346
  mkMany: '画到的屏太多,只拼了前 {n} 屏,其余留成灰块',
309
347
  mkMoved: '设计换了,这次标注没交出去',
310
- mkNoInput: '没能放进输入框,点一下聊天输入框再点「交给模型」',
311
- textSaved: '已改好,写回 {file}',
348
+ mkNoInput: '没能放进输入框,点一下聊天输入框再点「发到对话」',
349
+ textSaved: '已改好,保存到 {file}',
312
350
  textAsk: '把页面上的「{before}」改成「{after}」({where};只改这一处文字,别的不动)。\n',
313
351
  styleEdit: '调样式:点页面里的元素,拖滑块改字号、颜色、圆角、间距(Esc 退出)',
314
352
  styleAny: '点任意一屏里的元素调样式 · Esc 退出',
315
353
  styleOne: '点页面里的元素调样式 · Esc 退出',
316
354
  style: '调样式',
317
- styleNone: '在页面上点一个元素。改的时候页面上先看效果,满意了再写回文件。',
355
+ styleNone: '在页面上点一个元素。改的时候页面上先看效果,满意了再保存。',
318
356
  styleFont: '字号',
319
357
  styleWeight: '字重',
320
358
  styleColor: '文字',
@@ -331,14 +369,14 @@ window.__ModuleLoader__.load({
331
369
  styleVar: '设计变量 {name}',
332
370
  styleGradient: '现在是渐变,选一个颜色会换成纯色',
333
371
  styleReset: '还原',
334
- styleAsk: '交给模型',
335
- styleSave: '写回文件',
372
+ styleAsk: '发到对话',
373
+ styleSave: '直接保存',
336
374
  styleSaveTip: '写进这个元素在源码里的 style 属性(改前改后各存一版)',
337
375
  styleAskTip: '让模型改进样式表(能用设计变量就用)',
338
- styleNoWrite: '开发服务器的页面不在这里写回,交给模型改',
339
- styleBusyTip: '模型在干活,等它停了再写回',
340
- styleSaved: '样式已写回 {file}',
341
- styleMiss: '源码里没找到这个元素(可能是脚本生成的),改成交给模型',
376
+ styleNoWrite: '开发服务器的页面不能直接保存,发到对话让模型改',
377
+ styleBusyTip: '模型在干活,等它停了再保存',
378
+ styleSaved: '样式已保存到 {file}',
379
+ styleMiss: '源码里没找到这个元素(可能是脚本生成的),改成发到对话',
342
380
  styleN: '改了 {n} 项',
343
381
  styleAskText: '〔调样式〕{name}({sel}):{list}。{rules}把这些写进它的样式规则({where};能用设计变量就用设计变量),别写成行内 style。\n',
344
382
  ruleFrom: '- {props}:现在由 {sel} 决定({at})',
@@ -349,11 +387,13 @@ window.__ModuleLoader__.load({
349
387
  styleArrow: '{prop} {from} → {to}',
350
388
  styleJoin: ';',
351
389
  lang: 'zh',
352
- stylesHead: '先挑一个风格开工',
353
- stylesTip: '点一个看样板,满意了点「用这个风格」,整套配色、字体、圆角放进输入框;再写要做什么',
390
+ stylesHead: '或者先挑个风格',
391
+ stylesTip: '点开看样板,满意了点「用这个风格」,再说要做什么',
392
+ stylesMore: '更多风格({n})',
393
+ stylesLess: '收起',
354
394
  stylesOpen: '换个风格…',
355
395
  stylesTitle: '换个风格',
356
- stylesSwap: '点一个看样板;「用这个风格」让模型把现在的设计换成它(先改 tokens.css,布局和文案不动)',
396
+ stylesSwap: '点一个看样板;「用这个风格」让模型把现在的设计换成它(只换配色、字体、圆角,布局和文字不动)',
357
397
  stylesAll: '全部风格',
358
398
  stylePrev: '上一个(←)',
359
399
  styleNext: '下一个(→)',
@@ -396,29 +436,111 @@ window.__ModuleLoader__.load({
396
436
  phoneAuto: '只有扫了这个码的手机能打开,30 分钟没人看自动关。',
397
437
  phoneNoLan: '电脑没连上 Wi-Fi 或有线网,手机连不过来。',
398
438
  phoneFail: '没打开:{msg}',
399
- phoneHelp: '打不开?确认在同一个 Wi-Fi;手机开着代理 / VPN 的先关掉;电脑问「是否允许传入连接」时点允许。',
439
+ phoneHelp: '确认在同一个 Wi-Fi;手机开着代理 / VPN 的先关掉;电脑问「是否允许传入连接」时点允许。',
440
+ phoneTrouble: '打不开?',
400
441
  zoomIn: '放大',
401
442
  zoomOut: '缩小',
402
443
  fit: '适合画布',
403
444
  openScreen: '双击只看这一屏',
404
- generating: '生成中…',
445
+ generating: '设计中…',
405
446
  waiting: '模型正在工作,页面写出来就显示在这里',
447
+ waitingLive: '页面正在一段段写出来,写完就能点着改',
406
448
  refresh: '刷新',
407
449
  external: '在浏览器里打开',
408
450
  more: '更多',
409
- demoShort: '演示',
451
+ demoShort: '试用',
452
+ // 浮块按钮悬停说明:名字下面这一句
453
+ dPick: '点一个元素或拖出一块区域,发给模型改',
454
+ dText: '点页面里的字直接改,回车保存',
455
+ dStyle: '点一个元素,拖滑块调字号、颜色、圆角和间距',
456
+ dMark: '画圈、画箭头、写字,拼成一张图发给模型',
457
+ dPlay: '像真的 App 一样点,按钮会跳到对应的屏',
458
+ dLib: '存过的设计、风格和模板,也能逛设计市场',
459
+ dLook: '改配色、字体,或整套换个风格',
460
+ dCheck: '找出文字溢出、重叠、看不清、按钮太小',
461
+ dCheckN: '上次找到 {n} 个问题,点开看',
462
+ dExport: '转成代码、存图片 / PDF / PPT、打包分享',
463
+ dMore: '版本、在手机上看、刷新、在浏览器里打开',
464
+ dZoom: '按住 {mod} 滚动也能缩放',
465
+ dScreen: '换一屏、看全部、多尺寸、换类型',
466
+ toolsT: '工具',
410
467
  editAddr: '换一个地址',
468
+ textShort: '改字',
469
+ styleShort: '样式',
470
+ markShort: '画笔',
471
+ viewT: '视图',
472
+ canvasShort: '画布',
473
+ singleShort: '单页',
474
+ sizesShort: '多尺寸',
475
+ look: '外观',
476
+ lookTip: '外观:配色、字体、整套风格',
477
+ deliver: '导出',
478
+ deliverTip: '导出:转成代码、存成图片 / PDF / PPT、打包分享',
479
+ exFiles: '存成文件',
480
+ checkShort: '体检',
481
+ zoomT: '缩放',
482
+ zoomReal: '实际大小',
483
+ actX: '导出',
484
+ actV: '再出两版',
485
+ actO: '单看',
486
+ deliverCode: '代码',
487
+ deliverShare: '分享',
488
+ screensT: '换一屏 / 打开别的页面',
489
+ screensHd: '屏幕',
490
+ screensAll: '全部 {n} 屏',
491
+ screensN: '{name} · {n} 屏',
492
+ openOther: '打开别的页面…',
493
+ phoneOnTip: '手机预览开着',
494
+ lookSet: '整套',
495
+ lookStyle: '换风格',
496
+ lookBrand: '品牌',
497
+ lookColorTip: '点色块直接改,所有屏一起变',
498
+ lookTokTip: '点一下放进输入框,跟模型说怎么改',
499
+ lookTw: '调节滑块',
500
+ lookTwNone: '还没有专属滑块(留白、圆角、排法……)。',
501
+ lookN: '改了 {n} 处,还没保存',
502
+ lookSave: '直接保存',
503
+ lookSaveTip: '写进设计文件(不经过模型;改前改后各存一版)',
504
+ lookAskHead: '〔外观〕我在右侧外观面板里把这几个设计变量改成了:',
505
+ lookAskTail: '照这个改:改 tokens.css(或样式里 :root 上)的默认值,页面里写死的同一种颜色也换成变量。',
506
+ lookInspire: '灵感库',
507
+ lookRef: '照参考图做…',
508
+ el_button: '按钮', el_a: '链接', el_img: '图片', el_h: '标题', el_p: '段落', el_input: '输入框', el_li: '列表项', el_ul: '列表', el_nav: '导航', el_header: '顶栏', el_footer: '底栏', el_text: '文字', el_icon: '图标', el_card: '卡片', el_tag: '标签', el_avatar: '头像', el_box: '区块',
509
+ tkn_bg: '背景', tkn_surface: '卡片底', tkn_text: '正文字', tkn_muted: '次要字', tkn_line: '分隔线', tkn_primary: '主色', tkn_accent: '强调色', tkn_onPrimary: '主色上的字', tkn_link: '链接', tkn_danger: '警示色', tkn_success: '成功色', tkn_warning: '提醒色',
510
+ tkn_xs: '注释', tkn_sm: '小字', tkn_md: '中号字', tkn_body: '正文', tkn_lg: '大字', tkn_title: '标题', tkn_display: '大标题',
511
+ tkn_rSm: '小圆角', tkn_rMd: '中圆角', tkn_rLg: '大圆角', tkn_rFull: '全圆', tkn_r: '圆角',
512
+ tkn_space: '间距', tkn_gutter: '页边距',
513
+ tkn_fHead: '标题字体', tkn_fBody: '正文字体', tkn_fMono: '等宽字体',
514
+ tkn_shadow: '阴影', tkn_light: '浅', tkn_dark: '深', tkn_hover: '悬停',
515
+ phoneOnShort: '开着',
516
+ sizesNoShort: '固定画幅',
411
517
  working: '模型正在工作',
412
518
  asking: '模型在等你回答(在对话里)',
413
519
  writing: '正在写 {file}',
520
+ curLayout: '排版',
521
+ curColor: '配色',
522
+ curCopy: '文案',
523
+ stThink: '在想…',
524
+ stNew: '正在做第 {n} 屏',
525
+ stEdit: '正在改「{name}」',
526
+ stWriteAny: '正在改文件',
527
+ stCheck: '正在体检',
528
+ stLook: '正在看效果',
529
+ stPreview: '正在打开预览',
530
+ stBrand: '正在提取品牌',
531
+ stCode: '正在转成代码',
532
+ stCompare: '正在逐像素对比',
533
+ stShell: '正在跑命令',
534
+ stRead: '正在看文件',
535
+ stWeb: '正在查资料',
536
+ stSkill: '正在看设计规范',
537
+ stTime: '已用时 {time}',
538
+ stLines: '已写 {n} 行',
414
539
  born: '已生成 {name}',
415
540
  updated: '已更新{file}',
416
541
  updatedN: '已更新{file} · {n} 处改动',
417
542
  updatedWhole: '已更新{file} · 整页重排',
418
- step1: '让模型做一个网页、App 界面或海报',
419
- step2: '点「选择」,点元素或拖出一块区域',
420
- step3: '在输入框补一句要怎么改,发出去',
421
- tryHead: '不知道怎么开口?点一句试试',
543
+ tryHead: '试试这样说',
422
544
  try1: '帮我设计一个社区健身房的会员 App',
423
545
  try2: '做一个精品咖啡店的落地页',
424
546
  try3: '做一张周末市集的活动海报',
@@ -427,7 +549,7 @@ window.__ModuleLoader__.load({
427
549
  copied: '已复制,粘贴到输入框里',
428
550
  failed: '没放进去:{msg}',
429
551
  openFailed: '打不开:{msg}',
430
- headerTip: '设计工作台',
552
+ headerTip: '设计台',
431
553
  tweaks: '调节滑块',
432
554
  twTip: '模型给这套设计做的几个维度:拖的时候所有屏一起变,还没写进文件。',
433
555
  twEmpty: '这套设计还没有专属滑块。让模型挑几个最值得试的维度(留白、圆角、主色、排法……),做成滑块放在这里。',
@@ -439,15 +561,16 @@ window.__ModuleLoader__.load({
439
561
  twAskTail: '照这个改设计(改样式源文件,tweaks.json 里对应的 value 也改成新值)。',
440
562
  twAskTip: '让模型照这几个值改设计(可以顺手调整相关的地方)',
441
563
  twSaveTip: 'CSS 变量写进 tokens.css 的默认值,属性写到每屏的 <html> 上(改前改后各存一版)',
442
- twNoWrite: '开发服务器的页面不能直接写回,交给模型改',
564
+ twNoWrite: '开发服务器的页面不能直接保存,发到对话让模型改',
565
+ twVer: '正在看旧版本:回到当前版本再保存',
443
566
  twMoved: '设计已经换了,没写回;再拖一次',
444
- twSaved: '已写回 {n} 项',
445
- twMissed: '{names} 在样式里没找到默认值,没写回:交给模型改',
567
+ twSaved: '已保存 {n} 项',
568
+ twMissed: '{names} 在样式里没找到默认值,没保存:发到对话让模型改',
446
569
  systems: '品牌设计系统',
447
570
  sysOpen: '品牌设计系统…',
448
- sysEmptyLine: '有自己的品牌?',
571
+ sysEmptyLine: '用自己的品牌:',
449
572
  sysTip: '从你的网站或项目里提取的颜色、字体、圆角和组件。设为默认后,做新设计都照它来。',
450
- sysEmpty: '还没有。在下面填你的网站地址或项目文件夹,让模型提一套。',
573
+ sysEmpty: '还没有。在上面填你的网站地址或项目文件夹,让模型提一套。',
451
574
  sysDefault: '默认',
452
575
  sysSet: '设为默认',
453
576
  sysUnset: '不再默认',
@@ -461,7 +584,9 @@ window.__ModuleLoader__.load({
461
584
  handoff: '转成代码',
462
585
  hoTip: '把设计做成真项目的代码:先整理交接说明和截图,再一屏一屏写,每屏写完和设计稿逐像素比,对上了再做下一屏。',
463
586
  hoFw: '用什么做',
464
- hoSame: '跟现有项目一样({label})',
587
+ hoSame: '跟现有项目一样:{label}',
588
+ hoIn: '在 {dir} 里',
589
+ hoEdit: '点一下改路径',
465
590
  hoHtml: '纯 HTML / CSS',
466
591
  hoUni: 'uni-app(小程序 + 网页版)',
467
592
  hoWeapp: '微信原生小程序',
@@ -480,10 +605,16 @@ window.__ModuleLoader__.load({
480
605
  // 设计库
481
606
  library: '设计库',
482
607
  libBack: '回到画布',
608
+ homeBtn: '首页',
609
+ dHome: '回首页:新做一套、接着做别的、逛设计库',
610
+ homeBack: '回到「{name}」',
611
+ askNewLine: '(另起一套新设计,放新文件夹里,别改现在这套)',
612
+ askNewNote: '新做一套,现在这套不会被改',
613
+ wkHere: '正在看',
483
614
  libMarket: '市场',
484
615
  libWorks: '我的作品',
485
616
  libInspire: '灵感',
486
- libLine: '以前做的设计、存下的灵感、朋友给的设计包',
617
+ libLine: '以前做的设计、存下的灵感、朋友给的设计包:',
487
618
  libOpen: '打开设计库',
488
619
  libClose: '返回',
489
620
  libFail: '没办成:{msg}',
@@ -497,7 +628,10 @@ window.__ModuleLoader__.load({
497
628
  mkOffline: '连不上市场,下面是上次看到的。',
498
629
  mkOff: '「设计库不联网」开着:不访问任何网址,只显示下载过的。要联网,去「插件」页关掉「设计库不联网」。',
499
630
  mkNeedUpdate: '市场里有新东西要新版设计台才能看:去「插件」页点「更新」。',
500
- mkAt: '目录 {ago} 取的',
631
+ mkAt: '目录更新于 {ago}',
632
+ mkUse0: '可商用',
633
+ mkUseBY: '可商用 · 要署名',
634
+ mkSortT: '排序',
501
635
  mkSeedNote: '这是设计台自带的一批',
502
636
  mkRefresh: '刷新',
503
637
  mkTypeTpl: '模板',
@@ -554,6 +688,9 @@ window.__ModuleLoader__.load({
554
688
  wkNow: '正在看',
555
689
  wkOpen: '打开',
556
690
  wkReveal: '在文件夹里显示',
691
+ wkRevealShort: '文件夹',
692
+ wkZipShort: 'zip',
693
+ wkShareShort: '分享',
557
694
  wkPin: '置顶',
558
695
  wkUnpin: '取消置顶',
559
696
  wkZip: '打成 zip',
@@ -572,7 +709,7 @@ window.__ModuleLoader__.load({
572
709
  agoHour: '{n} 小时前',
573
710
  agoDay: '{n} 天前',
574
711
  inMyStyles: '我的风格',
575
- inSaveStyle: '把当前设计存成风格',
712
+ inSaveStyle: '存当前设计',
576
713
  inSaveStyleTip: '存下现在这套的配色、字体、圆角,以后做新设计一键用',
577
714
  inStyleSaved: '已存成风格「{name}」',
578
715
  inStyleNone: '这套设计没用设计变量(tokens.css),存不了风格',
@@ -644,7 +781,8 @@ window.__ModuleLoader__.load({
644
781
  exZip: '打成 zip(发给朋友)',
645
782
  exShare: '分享到设计市场',
646
783
  shTitle: '分享到设计市场',
647
- shTip: '交上去的东西是公开的:别人能看、能拿去用。先查一遍,过了打成 zip,再到 GitHub 上交(要有 GitHub 账号)。',
784
+ shGh: '投稿要有 GitHub 账号',
785
+ shTip: '交上去的东西是公开的:别人能看、能拿去用。先查一遍,过了打成 zip,再到 GitHub 上交。',
648
786
  shName: '名字',
649
787
  shDesc: '一句话介绍',
650
788
  shDescPh: '比如:街角茶饮店的三屏点单 App',
@@ -653,7 +791,7 @@ window.__ModuleLoader__.load({
653
791
  shAgreePrivate: '里面没有真实的个人信息(手机号、邮箱、证件号、地址、真人照片)',
654
792
  shAgreeRules: '同意按选的授权公开;上架后随时能撤,但别人已经复制走的收不回',
655
793
  shBYNote: 'CC BY 的署名就是你的 GitHub 用户名。',
656
- shGo: '检查并打包',
794
+ shGo: '检查并投稿',
657
795
  shChecking: '在查…',
658
796
  shOk: '查过了,打好了:{name}(在「下载」里)',
659
797
  shSteps: '还差三步:',
@@ -673,10 +811,44 @@ window.__ModuleLoader__.load({
673
811
  title: 'Design Studio',
674
812
  blockedLink: 'Blocked an outside link: {href} (designs from the gallery cannot leave the page)',
675
813
  blockedDownload: 'Blocked a download (designs from the gallery cannot download files)',
676
- guideDesc: 'Live page, pick what to change',
814
+ tabTitle: 'Design',
815
+ guideDesc: 'Design pages, apps and posters; click to change, then turn into code',
677
816
  empty: 'Nothing to preview yet',
678
- emptyHint: 'Or enter an HTML path or a port:',
679
- emptyHintPick: 'Have HTML already? Click "Browse…" or drop it here, or enter a path or a port:',
817
+ emptyHead: 'What do you want to make?',
818
+ emptySub: 'Describe it in a sentence. Screens appear on the canvas one by one; click anything to change it, then turn it into code.',
819
+ homeTab: 'Start',
820
+ askPh: 'e.g. {ex}',
821
+ askTab: 'use this',
822
+ exAuto: 'An ordering mini program for a corner noodle shop, 3 screens, warm hand-made style',
823
+ exWeb: 'Home page for an independent coffee roaster: signature beans, store map, booking',
824
+ exApp: 'Member app for a neighborhood gym: classes, check-ins, membership card',
825
+ exMini: 'Ordering mini program for a noodle shop: menu, cart, pickup number',
826
+ exTablet: 'Tablet app for kindergarten teachers: roll call, timetable, parent messages',
827
+ exDesktop: 'Desktop bookkeeping app for a small team: transactions, reports, settings',
828
+ exPoster: 'Poster for a weekend night run in the city, fresh sporty style',
829
+ exSlides: 'Five-slide launch deck: problem, solution, numbers, pricing, next steps',
830
+ exEmail: 'Birthday-month offer email for members: one big image, a short note, one button',
831
+ askKindLine: 'Type: {k}',
832
+ askMore: 'More ways to start',
833
+ askMoreD: 'Open existing HTML, use your own brand, open from the library',
834
+ askOpenHtml: 'Open existing HTML…',
835
+ askBrand: 'Use your own brand',
836
+ askLib: 'Open from the library',
837
+ askRef: 'Reference',
838
+ askRefD: 'Design from a screenshot or mockup: pick one, or drop / paste it',
839
+ askStyleAuto: 'Style: auto',
840
+ askStyle: 'Style: {name}',
841
+ askStyleD: 'Pick a style first, or let the model decide',
842
+ askStylePick: 'Pick a style',
843
+ askStyleAutoBtn: 'Auto (let the model decide)',
844
+ askSend: 'Start designing',
845
+ askSent: 'Sent. The first screen is on its way',
846
+ homeRecent: 'Continue',
847
+ homeRecentAll: 'All designs →',
848
+ waysHtml: 'Open existing HTML',
849
+ moreWays: 'Other ways to start',
850
+ emptyHint: 'Enter a file path or a local port (like 5173)',
851
+ emptyHintPick: 'Click "Browse…", drop it here, or enter a file path / local port (like 5173)',
680
852
  browse: 'Browse…',
681
853
  browseTip: 'Pick a local HTML file to open',
682
854
  pathNone: "Couldn't get this file's path. Type its path to open it.",
@@ -726,7 +898,7 @@ window.__ModuleLoader__.load({
726
898
  refPick: 'Restyle from a reference…',
727
899
  refChoose: 'Choose one',
728
900
  refPickTip: 'Pick a reference image to restyle after (or drop / paste an image into the preview)',
729
- refEmpty: 'Have a reference image? Drop or paste it here to design in its style',
901
+ refEmpty: 'Design from a reference image: drop it, paste it, or',
730
902
  refDrop: 'Drop to use as a reference',
731
903
  refDropSub: 'Saved to refs/ in the design folder; its colors go into the input',
732
904
  refNotImage: 'Images only (PNG / JPG / WebP / GIF)',
@@ -772,9 +944,10 @@ window.__ModuleLoader__.load({
772
944
  gShadow: 'Shadows',
773
945
  gOther: 'Other',
774
946
  exportT: 'Export',
775
- exFrame: 'This screen · in frame',
776
- exRaw: 'This screen · plain',
777
- exLong: 'This screen · full page',
947
+ exFrame: '{name} · in frame',
948
+ exRaw: '{name} · plain',
949
+ exLong: '{name} · full page',
950
+ exThisName: 'This screen',
778
951
  exShow: 'Showcase of all screens',
779
952
  exPdf: 'PDF · one page per screen',
780
953
  exPptx: 'PowerPoint · one slide per screen (as images)',
@@ -785,7 +958,8 @@ window.__ModuleLoader__.load({
785
958
  checking: 'Checking…',
786
959
  checkNone: 'No problems found',
787
960
  checkSum: '{s} screens · {n} problems',
788
- checkFix: 'Ask the model to fix',
961
+ checkFix: 'Send to chat',
962
+ checkFixTip: 'Send these issues to the chat for the model to fix',
789
963
  checkAgain: 'Check again',
790
964
  checkFail: 'Check failed: {msg}',
791
965
  checkNote: 'Only measurable layout problems; whether it looks good is still up to you',
@@ -884,17 +1058,19 @@ window.__ModuleLoader__.load({
884
1058
  cmpNow: 'Now',
885
1059
  dark: 'Dark mode',
886
1060
  variants: 'Two more takes',
887
- pickThis: 'Use this one: move the other takes to drafts/ and build the rest',
1061
+ pickThis: 'Use this take: move the other takes to drafts/ and build the rest',
888
1062
  pickShort: 'Use this',
1063
+ takeN: 'Take {l}',
889
1064
  pickText: '[Direction picked] Use "{name}" ({where}). First move the other takes in its group ({others}) into a drafts/ subfolder (don\'t delete them), then rename it to {base}.html; move its design tokens into tokens.css (create it if missing) and switch it to tokens.css. Then build the rest.\n',
890
1065
  pickKeep: '[Direction picked] Keep the original "{name}" ({where}). Move the other takes in its group ({others}) into a drafts/ subfolder (don\'t delete them) and leave everything else as is.\n',
891
1066
  variantText: 'Make two alternative designs of "{name}" in clearly different directions (layout or visual style). Save them next to it as {b}-b.html and {b}-c.html (original: {where}); they will sit right after the original on the canvas. Leave the original and tokens.css unchanged; override variables inside the new files if they need other colors.\n',
892
1067
  exThis: 'Export this screen',
893
1068
  demo: 'Play: tap buttons in the page to move between screens',
894
- demoOn: 'Playing · tap to navigate',
1069
+ demoOn: 'Playing',
1070
+ demoHow: 'tap buttons to move',
895
1071
  demoBack: 'Back',
896
1072
  demoRestart: 'Start over',
897
- demoExit: 'Stop playing (Esc)',
1073
+ demoExit: 'Stop playing',
898
1074
  textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
899
1075
  mark: 'Mark up: draw circles, arrows and notes on the page, then hand it to the model as one image',
900
1076
  mkPen: 'Pen',
@@ -909,7 +1085,7 @@ window.__ModuleLoader__.load({
909
1085
  mkUndo: 'Undo',
910
1086
  mkRedo: 'Redo',
911
1087
  mkClear: 'Clear all',
912
- mkHint: 'Draw on the page, then send it to the model',
1088
+ mkHint: 'Draw on the page, then click Send to chat',
913
1089
  mkHow_pen: 'Press and drag to draw freehand',
914
1090
  mkHow_hl: 'Drag over what needs changing to highlight it',
915
1091
  mkHow_arrow: 'Press at the start and drag to what you mean',
@@ -920,13 +1096,13 @@ window.__ModuleLoader__.load({
920
1096
  mkCoach: 'Draw right on the page',
921
1097
  mkCoach1: 'Pick a tool above: pen, arrow, circle, text…',
922
1098
  mkCoach2: 'Press and drag on the page to mark what to change',
923
- mkCoach3: 'Then click "Send to model" and add a line on what to do',
1099
+ mkCoach3: 'Then click "Send to chat" and add a line on what to do',
924
1100
  mkCancel: 'Cancel',
925
- mkSend: 'Send to model',
1101
+ mkSend: 'Send to chat',
926
1102
  mkCopy: 'Copy',
927
1103
  mkCopyTip: 'Copy the marked-up image to paste somewhere else',
928
1104
  mkCopied: 'Markup image copied. Paste it anywhere',
929
- mkCopyFail: 'Could not copy to the clipboard. Click Copy again, or Send to model',
1105
+ mkCopyFail: 'Could not copy to the clipboard. Click Copy again, or Send to chat',
930
1106
  mkSending: 'Building the image…',
931
1107
  mkTextPh: 'Type a note, Enter to finish',
932
1108
  mkEmpty: 'Nothing drawn yet',
@@ -937,7 +1113,7 @@ window.__ModuleLoader__.load({
937
1113
  mkIn: 'Markup added to the input. Say what to change, then send',
938
1114
  mkMany: 'Too many screens marked: captured the first {n}, the rest are gray',
939
1115
  mkMoved: 'The design changed, so the markup was not sent',
940
- mkNoInput: 'Could not add it to the input. Click the chat input, then Send to model again',
1116
+ mkNoInput: 'Could not add it to the input. Click the chat input, then Send to chat again',
941
1117
  textSaved: 'Saved to {file}',
942
1118
  textAsk: 'Change the text "{before}" to "{after}" on the page ({where}; only this text, nothing else).\n',
943
1119
  styleEdit: 'Style: click an element, then drag sliders for size, colors, radius and spacing (Esc to stop)',
@@ -961,14 +1137,14 @@ window.__ModuleLoader__.load({
961
1137
  styleVar: 'Token {name}',
962
1138
  styleGradient: 'It is a gradient now; picking a color makes it solid',
963
1139
  styleReset: 'Reset',
964
- styleAsk: 'Ask the model',
965
- styleSave: 'Save to file',
1140
+ styleAsk: 'Send to chat',
1141
+ styleSave: 'Save',
966
1142
  styleSaveTip: 'Writes into this element\'s style attribute in the source (a version is kept before and after)',
967
1143
  styleAskTip: 'Let the model move it into the stylesheet (using tokens where it can)',
968
- styleNoWrite: 'Dev-server pages are not written here; ask the model',
1144
+ styleNoWrite: 'Dev-server pages cannot be saved here; send it to the chat',
969
1145
  styleBusyTip: 'The model is working; save when it stops',
970
1146
  styleSaved: 'Style saved to {file}',
971
- styleMiss: 'Could not find this element in the source (maybe a script made it); asking the model instead',
1147
+ styleMiss: 'Could not find this element in the source (maybe a script made it); sending it to the chat instead',
972
1148
  styleN: '{n} changed',
973
1149
  styleAskText: '[Style] {name} ({sel}): {list}. {rules}Put it in its stylesheet rule ({where}), using design tokens where you can, not an inline style.\n',
974
1150
  ruleFrom: '- {props}: currently set by {sel} ({at})',
@@ -979,11 +1155,13 @@ window.__ModuleLoader__.load({
979
1155
  styleArrow: '{prop} {from} → {to}',
980
1156
  styleJoin: '; ',
981
1157
  lang: 'en',
982
- stylesHead: 'Start from a style',
983
- stylesTip: 'Pick one to see a sample; "Use this style" puts its colors, fonts and shapes in the chat box, then say what to make',
1158
+ stylesHead: 'Or pick a style first',
1159
+ stylesTip: 'Open one to preview, click "Use this style", then say what to make',
1160
+ stylesMore: 'More styles ({n})',
1161
+ stylesLess: 'Show less',
984
1162
  stylesOpen: 'Try another style…',
985
1163
  stylesTitle: 'Change the style',
986
- stylesSwap: 'Pick one to see a sample; "Use this style" has the model restyle the current design (tokens.css first; layout and copy stay)',
1164
+ stylesSwap: 'Pick one to see a sample; "Use this style" has the model restyle the current design (colors, fonts and corners only; layout and copy stay)',
987
1165
  stylesAll: 'All styles',
988
1166
  stylePrev: 'Previous (←)',
989
1167
  styleNext: 'Next (→)',
@@ -1026,29 +1204,110 @@ window.__ModuleLoader__.load({
1026
1204
  phoneAuto: 'Only a phone that scanned this code can open it. Closes after 30 minutes idle.',
1027
1205
  phoneNoLan: 'This computer is not on Wi-Fi or a wired network, so a phone cannot reach it.',
1028
1206
  phoneFail: 'Could not start: {msg}',
1029
- phoneHelp: "Won't open? Check both are on the same Wi-Fi, turn off any VPN or proxy on the phone, and allow incoming connections if your computer asks.",
1207
+ phoneHelp: 'Check both are on the same Wi-Fi, turn off any VPN or proxy on the phone, and allow incoming connections if your computer asks.',
1208
+ phoneTrouble: "Won't open?",
1030
1209
  zoomIn: 'Zoom in',
1031
1210
  zoomOut: 'Zoom out',
1032
1211
  fit: 'Fit',
1033
1212
  openScreen: 'Double-click to view this screen alone',
1034
- generating: 'Generating…',
1213
+ generating: 'Designing…',
1035
1214
  waiting: 'The model is working. The page shows up here once it is written',
1215
+ waitingLive: 'The page is being written piece by piece—click to edit once it’s done',
1036
1216
  refresh: 'Reload',
1037
1217
  external: 'Open in browser',
1038
1218
  more: 'More',
1039
1219
  demoShort: 'Play',
1220
+ dPick: 'Click an element or drag a region to send it to the model',
1221
+ dText: 'Click any text in the page and type, Enter to save',
1222
+ dStyle: 'Click an element, then drag sliders for size, colors, radius and spacing',
1223
+ dMark: 'Draw circles, arrows and notes, sent to the model as one image',
1224
+ dPlay: 'Click through it like a real app; buttons jump to their screens',
1225
+ dLib: 'Saved designs, styles and templates, plus the design market',
1226
+ dLook: 'Change colors and fonts, or switch the whole style',
1227
+ dCheck: 'Find overflowing or overlapping text, low contrast and tiny tap targets',
1228
+ dCheckN: '{n} problems last time. Click to see them',
1229
+ dExport: 'Build as code, save images / PDF / PPT, or pack and share',
1230
+ dMore: 'Versions, view on phone, refresh, open in browser',
1231
+ dZoom: 'Hold {mod} and scroll to zoom too',
1232
+ dScreen: 'Switch screens, see all, sizes or type',
1233
+ toolsT: 'Tools',
1040
1234
  editAddr: 'Change address',
1235
+ textShort: 'Text',
1236
+ styleShort: 'Style',
1237
+ markShort: 'Draw',
1238
+ viewT: 'View',
1239
+ canvasShort: 'Canvas',
1240
+ singleShort: 'Single',
1241
+ sizesShort: 'Sizes',
1242
+ look: 'Look',
1243
+ lookTip: 'Look: colors, fonts and overall style',
1244
+ deliver: 'Export',
1245
+ deliverTip: 'Export: build as code, save images / PDF / PPT, pack and share',
1246
+ exFiles: 'Save as file',
1247
+ checkShort: 'Check',
1248
+ zoomT: 'Zoom',
1249
+ zoomReal: 'Actual size',
1250
+ actX: 'Export',
1251
+ actV: 'More takes',
1252
+ actO: 'Open',
1253
+ deliverCode: 'Code',
1254
+ deliverShare: 'Share',
1255
+ screensT: 'Switch screen / open another page',
1256
+ screensHd: 'Screens',
1257
+ screensAll: 'All {n}',
1258
+ screensN: '{name} · {n} screens',
1259
+ openOther: 'Open another page…',
1260
+ phoneOnTip: 'Phone preview is on',
1261
+ lookSet: 'Overall',
1262
+ lookStyle: 'Restyle',
1263
+ lookBrand: 'Brand',
1264
+ lookColorTip: 'Click a swatch to change it; every screen follows',
1265
+ lookTokTip: 'Click to put it in the input, then tell the model how to change it',
1266
+ lookTw: 'Tweaks',
1267
+ lookTwNone: 'No tweaks for this design yet (spacing, corners, layout…).',
1268
+ lookN: '{n} change(s), not saved',
1269
+ lookSave: 'Save',
1270
+ lookSaveTip: 'Write into the design files (no model; a version is kept before and after)',
1271
+ lookAskHead: '[Look] I changed these design tokens in the Look panel:',
1272
+ lookAskTail: 'Make it so: change the defaults in tokens.css (or :root in the styles), and replace hard-coded copies of the same colors with the tokens.',
1273
+ lookInspire: 'Inspiration',
1274
+ lookRef: 'From a reference image…',
1275
+ el_button: 'Button', el_a: 'Link', el_img: 'Image', el_h: 'Heading', el_p: 'Paragraph', el_input: 'Input', el_li: 'List item', el_ul: 'List', el_nav: 'Nav', el_header: 'Header', el_footer: 'Footer', el_text: 'Text', el_icon: 'Icon', el_card: 'Card', el_tag: 'Tag', el_avatar: 'Avatar', el_box: 'Block',
1276
+ tkn_bg: 'Background', tkn_surface: 'Card', tkn_text: 'Text', tkn_muted: 'Muted text', tkn_line: 'Divider', tkn_primary: 'Primary', tkn_accent: 'Accent', tkn_onPrimary: 'Text on primary', tkn_link: 'Link', tkn_danger: 'Danger', tkn_success: 'Success', tkn_warning: 'Warning',
1277
+ tkn_xs: 'Caption', tkn_sm: 'Small', tkn_md: 'Medium', tkn_body: 'Body', tkn_lg: 'Large', tkn_title: 'Title', tkn_display: 'Display',
1278
+ tkn_rSm: 'Small corners', tkn_rMd: 'Medium corners', tkn_rLg: 'Large corners', tkn_rFull: 'Pill', tkn_r: 'Corners',
1279
+ tkn_space: 'Spacing', tkn_gutter: 'Page margin',
1280
+ tkn_fHead: 'Heading font', tkn_fBody: 'Body font', tkn_fMono: 'Mono font',
1281
+ tkn_shadow: 'Shadow', tkn_light: 'light', tkn_dark: 'dark', tkn_hover: 'hover',
1282
+ phoneOnShort: 'On',
1283
+ sizesNoShort: 'Fixed size',
1041
1284
  working: 'The model is working',
1042
1285
  asking: 'The model is waiting for your answer (in the chat)',
1043
1286
  writing: 'Writing {file}',
1287
+ curLayout: 'Layout',
1288
+ curColor: 'Color',
1289
+ curCopy: 'Copy',
1290
+ stThink: 'Thinking…',
1291
+ stNew: 'Designing screen {n}',
1292
+ stEdit: 'Editing "{name}"',
1293
+ stWriteAny: 'Editing files',
1294
+ stCheck: 'Checking the design',
1295
+ stLook: 'Looking at the result',
1296
+ stPreview: 'Opening the preview',
1297
+ stBrand: 'Extracting the brand',
1298
+ stCode: 'Turning it into code',
1299
+ stCompare: 'Comparing pixel by pixel',
1300
+ stShell: 'Running a command',
1301
+ stRead: 'Reading files',
1302
+ stWeb: 'Looking things up',
1303
+ stSkill: 'Reading the design guide',
1304
+ stTime: '{time} elapsed',
1305
+ stLines: '{n} lines so far',
1044
1306
  born: 'Generated {name}',
1045
1307
  updated: 'Updated{file}',
1046
1308
  updatedN: 'Updated{file} · {n} changes',
1047
1309
  updatedWhole: 'Updated{file} · whole page',
1048
- step1: 'Ask the model for a web page, app screen or poster',
1049
- step2: 'Click Select, then an element or drag a region',
1050
- step3: 'Say what to change in the input and send',
1051
- tryHead: 'Not sure where to start? Try one',
1310
+ tryHead: 'Try saying',
1052
1311
  try1: 'Design a member app for a neighborhood gym',
1053
1312
  try2: 'Make a landing page for a specialty coffee shop',
1054
1313
  try3: 'Make a poster for a weekend street market',
@@ -1069,15 +1328,16 @@ window.__ModuleLoader__.load({
1069
1328
  twAskTail: 'Change the design to match (edit the style sources and update the matching values in tweaks.json).',
1070
1329
  twAskTip: 'Let the model apply these values (and adjust related details)',
1071
1330
  twSaveTip: 'CSS variables go into the defaults in tokens.css, attributes onto each screen\'s <html> (a version is kept before and after)',
1072
- twNoWrite: 'Dev-server pages cannot be written back. Ask the model instead',
1331
+ twNoWrite: 'Dev-server pages cannot be saved here. Send it to the chat instead',
1332
+ twVer: 'Viewing an old version. Go back to the current one to save',
1073
1333
  twMoved: 'The design changed, so nothing was written. Try again',
1074
- twSaved: 'Wrote back {n}',
1075
- twMissed: 'No default found in the styles for {names}, not written. Ask the model instead',
1334
+ twSaved: 'Saved {n}',
1335
+ twMissed: 'No default found in the styles for {names}, not saved. Send it to the chat instead',
1076
1336
  systems: 'Brand design systems',
1077
1337
  sysOpen: 'Brand design systems…',
1078
- sysEmptyLine: 'Have your own brand?',
1338
+ sysEmptyLine: 'Use your own brand:',
1079
1339
  sysTip: 'Colors, fonts, radii and components pulled from your site or project. The default one is used for every new design.',
1080
- sysEmpty: 'None yet. Enter your website or project folder below and let the model extract one.',
1340
+ sysEmpty: 'None yet. Enter your website or project folder above and let the model extract one.',
1081
1341
  sysDefault: 'Default',
1082
1342
  sysSet: 'Make default',
1083
1343
  sysUnset: 'Stop using',
@@ -1091,7 +1351,9 @@ window.__ModuleLoader__.load({
1091
1351
  handoff: 'Build as code',
1092
1352
  hoTip: 'Turn the design into a real project: a handoff note and screenshots first, then one screen at a time, each pixel-compared with the design before moving on.',
1093
1353
  hoFw: 'Build with',
1094
- hoSame: 'Same as the project ({label})',
1354
+ hoSame: 'Same as the project: {label}',
1355
+ hoIn: 'in {dir}',
1356
+ hoEdit: 'Click to edit the path',
1095
1357
  hoHtml: 'Plain HTML / CSS',
1096
1358
  hoUni: 'uni-app (mini program + H5)',
1097
1359
  hoWeapp: 'WeChat native mini program',
@@ -1109,10 +1371,16 @@ window.__ModuleLoader__.load({
1109
1371
  hoCheckAny: 'After each screen, check it the way HANDOFF.md says (compare_design until the difference is under 3% when it runs in a browser; for a native mini program, ask the user to check it in WeChat DevTools), then move on. ',
1110
1372
  library: 'Library',
1111
1373
  libBack: 'Back to canvas',
1374
+ homeBtn: 'Home',
1375
+ dHome: 'Go home: start a new design, pick up another one, browse the library',
1376
+ homeBack: 'Back to "{name}"',
1377
+ askNewLine: '(Start a new design in a new folder; leave the current one as is.)',
1378
+ askNewNote: 'This starts a new design; the current one stays as is',
1379
+ wkHere: 'Open now',
1112
1380
  libMarket: 'Gallery',
1113
1381
  libWorks: 'My designs',
1114
1382
  libInspire: 'Inspiration',
1115
- libLine: 'Past designs, saved inspiration, design packs from friends',
1383
+ libLine: 'Past designs, saved inspiration, design packs from friends:',
1116
1384
  libOpen: 'Open the library',
1117
1385
  libClose: 'Back',
1118
1386
  libFail: 'That did not work: {msg}',
@@ -1126,7 +1394,10 @@ window.__ModuleLoader__.load({
1126
1394
  mkOffline: 'Cannot reach the gallery; showing what you saw last time.',
1127
1395
  mkOff: '“Design library offline” is on: no network is used and only downloaded designs are shown. Turn it off in the Plugins page to go online.',
1128
1396
  mkNeedUpdate: 'Some gallery items need a newer Design Studio: click Update on the Plugins page.',
1129
- mkAt: 'Catalog fetched {ago}',
1397
+ mkAt: 'Catalog updated {ago}',
1398
+ mkUse0: 'Free to use',
1399
+ mkUseBY: 'Free to use · credit',
1400
+ mkSortT: 'Sort',
1130
1401
  mkSeedNote: 'This is the set bundled with the studio',
1131
1402
  mkRefresh: 'Refresh',
1132
1403
  mkTypeTpl: 'Templates',
@@ -1183,6 +1454,9 @@ window.__ModuleLoader__.load({
1183
1454
  wkNow: 'Open now',
1184
1455
  wkOpen: 'Open',
1185
1456
  wkReveal: 'Show in folder',
1457
+ wkRevealShort: 'Folder',
1458
+ wkZipShort: 'Zip',
1459
+ wkShareShort: 'Share',
1186
1460
  wkPin: 'Pin',
1187
1461
  wkUnpin: 'Unpin',
1188
1462
  wkZip: 'Save as zip',
@@ -1201,7 +1475,7 @@ window.__ModuleLoader__.load({
1201
1475
  agoHour: '{n} h ago',
1202
1476
  agoDay: '{n} d ago',
1203
1477
  inMyStyles: 'My styles',
1204
- inSaveStyle: 'Save this design as a style',
1478
+ inSaveStyle: 'Save current',
1205
1479
  inSaveStyleTip: 'Keep its colors, fonts and corners to reuse in one click',
1206
1480
  inStyleSaved: 'Saved as style "{name}"',
1207
1481
  inStyleNone: 'This design has no design tokens (tokens.css), so there is no style to save',
@@ -1273,7 +1547,8 @@ window.__ModuleLoader__.load({
1273
1547
  exZip: 'Save as zip (to send)',
1274
1548
  exShare: 'Share to the gallery',
1275
1549
  shTitle: 'Share to the gallery',
1276
- shTip: 'What you share is public: anyone can see and reuse it. It is checked first, saved as a zip, then you submit it on GitHub (a GitHub account is needed).',
1550
+ shGh: 'You need a GitHub account to submit',
1551
+ shTip: 'What you share is public: anyone can see and reuse it. It is checked first, saved as a zip, then you submit it on GitHub.',
1277
1552
  shName: 'Name',
1278
1553
  shDesc: 'One-line description',
1279
1554
  shDescPh: 'e.g. A three-screen ordering app for a corner tea shop',
@@ -1282,7 +1557,7 @@ window.__ModuleLoader__.load({
1282
1557
  shAgreePrivate: 'It contains no real personal data (phone, email, ID numbers, address, photos of real people)',
1283
1558
  shAgreeRules: 'I agree to publish it under the chosen license; it can be taken down any time, but copies others already made stay',
1284
1559
  shBYNote: 'For CC BY, the credit is your GitHub username.',
1285
- shGo: 'Check and save',
1560
+ shGo: 'Check and submit',
1286
1561
  shChecking: 'Checking…',
1287
1562
  shOk: 'Checked and saved: {name} (in Downloads)',
1288
1563
  shSteps: 'Three more steps:',
@@ -1315,6 +1590,8 @@ window.__ModuleLoader__.load({
1315
1590
  // ── 样式(DSH 设计变量,深浅主题跟着走)──────────────────────────────────
1316
1591
 
1317
1592
  const ACCENT = '#4d6bfe'
1593
+ const TOP_R = 52 // 画布上面一排浮块占的高度:单页、演示、对比让开它,画布排的时候也让开
1594
+ const BOT_R = 64 // 底下一排(工具坞、视图、缩放)占的高度
1318
1595
  const PICK_SRC = 'dsh-design-studio' // 输入框里点选标签的来源名(样式里也用)
1319
1596
  // 输入框里的各种标签:图标按来源名分(DSH 的标签上只带来源名),消息里和悬停卡片按 data-i
1320
1597
  const CHIP_ICON = {
@@ -1335,11 +1612,73 @@ window.__ModuleLoader__.load({
1335
1612
  const EASE = 'cubic-bezier(.2,.8,.2,1)'
1336
1613
  const CSS = `
1337
1614
  .ddp{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,inherit);font-size:12px;container-type:inline-size}
1338
- .ddp-bar{position:relative;z-index:8;display:flex;align-items:center;gap:4px;height:40px;padding:0 8px;border-bottom:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.18));flex:none}
1339
- .ddp-bar.editing>:not(form){display:none}
1340
- .ddp-addr{flex:1;min-width:0;display:flex;align-items:center;gap:7px;height:26px;padding:0 10px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:text;border:.5px solid transparent;overflow:hidden}
1341
- .ddp-addr:hover{border-color:var(--dsw-alias-border-l3)}
1342
- .ddp-addr span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1615
+ /* 浮在画布上的几块:上面一排(左上 设计库 + 屏名,右上 外观 体检 交付 更多)、底部中间工具坞、左下视图和类型、右下缩放 */
1616
+ .ddp-isl{position:relative;z-index:9;display:flex;align-items:center;gap:2px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1);
1617
+ box-shadow:0 6px 20px rgba(0,0,0,.12),0 0 0 .5px var(--dsw-alias-border-l3)}
1618
+ .ddp-isl .ddp-btn{height:30px;min-width:30px;border-radius:8px}
1619
+ .ddp-isl .ddp-btn svg{width:16px;height:16px}
1620
+ .ddp-isl .ddp-btn .cr{width:10px;height:10px}
1621
+ .ddp-isl .ddp-sep{height:18px;margin:0 3px}
1622
+ .ddp-isl .ddp-menu{top:calc(100% + 8px)}
1623
+ .ddp-top{position:absolute;left:10px;right:10px;top:10px;z-index:9;display:flex;align-items:flex-start;gap:8px;pointer-events:none}
1624
+ .ddp-top>.ddp-isl{pointer-events:auto}
1625
+ .ddp-top>.tl{flex:0 1 auto;min-width:0}
1626
+ .ddp-top>.tl.wide{flex:1 1 auto;max-width:560px}
1627
+ .ddp-top>.tl.wide .ddp-input{height:30px}
1628
+ .ddp-top>.tr{margin-left:auto;flex:none}
1629
+ .ddp-isl .ddp-btn.pri{padding:0 8px 0 9px;background:${ACCENT};color:#fff;font-weight:500}
1630
+ .ddp-isl .ddp-btn.pri:hover,.ddp-isl .ddp-btn.pri.sel{background:#3d5bf0;color:#fff}
1631
+ .ddp-isl .ddp-btn.pri .cr{opacity:.8}
1632
+ .ddp-dock{position:absolute;left:50%;bottom:10px;transform:translateX(-50%);padding:4px;gap:2px}
1633
+ .ddp-dock .ddp-sep{height:26px}
1634
+ /* 悬停说明:深底浅字(跟提示消息一个配色,深色模式自己反过来);第一行名字 + 快捷键键帽,第二行一句用途
1635
+ 说明控制在两行内,text-wrap:pretty 不让一个字单独掉到下一行 */
1636
+ .ddp-tipbox{position:absolute;left:0;top:0;z-index:30;display:none;flex-direction:column;gap:3px;width:max-content;max-width:min(228px,calc(100% - 16px));padding:7px 10px 8px;border-radius:9px;
1637
+ background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:17px;letter-spacing:.01em;pointer-events:none;box-shadow:0 6px 20px rgba(0,0,0,.2)}
1638
+ .ddp-tipbox.on{display:flex;animation:ddp-tip .12s ease}
1639
+ .ddp-tipbox .nm{display:flex;align-items:center;gap:10px}
1640
+ .ddp-tipbox b{font-weight:600}
1641
+ .ddp-tipbox kbd{margin-left:auto;padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px currentColor;font:500 10.5px/16px ui-monospace,SFMono-Regular,Menlo,monospace;opacity:.6;white-space:nowrap}
1642
+ .ddp-tipbox span{opacity:.72;text-wrap:pretty}
1643
+ @keyframes ddp-tip{from{opacity:0}}
1644
+ .ddp-tool{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:48px;height:42px;padding:0;border:0;border-radius:8px;background:transparent;
1645
+ color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:13px;cursor:pointer;flex:none;white-space:nowrap}
1646
+ .ddp-tool svg{width:18px;height:18px;flex:none}
1647
+ .ddp-tool:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1648
+ .ddp-tool.on,.ddp-tool.on:hover:not(:disabled){background:${ACCENT};color:#fff}
1649
+ .ddp-tool:disabled{opacity:.4;cursor:default}
1650
+ .ddp-isl .ddp-btn.lab{padding:0 9px 0 7px}
1651
+ .ddp-isl .ddp-btn .lb{font-size:12px}
1652
+ /* 左上菜单里的「类型」:四列小格,一点就换 */
1653
+ .ddp-menu.l{min-width:268px}
1654
+ .ddp-kinds{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:2px;padding:0 2px 4px}
1655
+ .ddp-menu .ddp-kinds button{flex-direction:column;justify-content:center;gap:4px;height:50px;padding:0 2px;font-size:11px;text-align:center}
1656
+ .ddp-menu .ddp-kinds button span{max-width:100%}
1657
+ .ddp-menu .ddp-kinds button.sel{background:rgba(77,107,254,.12)}
1658
+ /* 标注时它有自己的上下两条:浮块都收起来 */
1659
+ .ddp-stage:has(>.ddp-mark)>.ddp-top,.ddp-stage:has(>.ddp-mark)>.ddp-isl{display:none}
1660
+ /* 上下两排浮块让出来:提示、消息、横幅、生成中、右边的面板都挪到两排中间 */
1661
+ .ddp-stage.isl>.ddp-hint{top:58px}
1662
+ .ddp-stage.isl>.ddp-toast:not(.mk){top:58px}
1663
+ .ddp-stage.isl>.ddp-toast.low{top:104px}
1664
+ .ddp-stage.isl>.ddp-banner{top:58px}
1665
+ .ddp-stage.isl>.ddp-chip{bottom:76px}
1666
+ .ddp-stage.isl>.ddp-chip.side{left:calc((100% - 296px) / 2)}
1667
+ .ddp-stage.isl>.ddp-tokens{top:58px;max-height:calc(100% - 58px - 74px)}
1668
+ .ddp-kind.ddp-where{flex:0 1 auto;min-width:80px;max-width:320px}
1669
+ .ddp-addr{width:100%;min-width:0;display:flex;align-items:center;gap:7px;height:30px;padding:0 8px 0 9px;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-weight:500;cursor:pointer;border:0;overflow:hidden}
1670
+ .ddp-addr:hover,.ddp-addr.sel{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1671
+ .ddp-addr span.lb2 em{color:var(--dsw-alias-label-tertiary);font-weight:400}
1672
+ .ddp-addr span{flex:1;min-width:0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1673
+ /* 窄了先截名字,「· 3 屏」「· B」留着 */
1674
+ .ddp-addr span.lb2{display:flex;overflow:visible}
1675
+ .ddp-addr span.lb2 b{min-width:0;font-weight:inherit;overflow:hidden;text-overflow:ellipsis}
1676
+ .ddp-addr span.lb2 em{flex:none;font-style:normal;white-space:pre}
1677
+ .ddp-addr .cr{width:10px;height:10px;flex:none;opacity:.55}
1678
+ .ddp-menu.l{left:0;right:auto;max-width:min(320px,calc(100cqw - 16px))}
1679
+ .ddp-menu .ls{max-height:min(360px,55vh);overflow-y:auto}
1680
+ .ddp-menu button>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1681
+ .ddp-menu .sp{height:.5px;margin:4px 6px;background:var(--dsw-alias-border-l3)}
1343
1682
  .ddp-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;flex:none;box-shadow:0 0 0 3px rgba(34,197,94,.15)}
1344
1683
  .ddp-dot.off{background:var(--dsw-alias-label-tertiary);box-shadow:none}
1345
1684
  .ddp-dot.wait{background:#f5a524;box-shadow:0 0 0 3px rgba(245,165,36,.15);animation:ddp-blink 1s ease-in-out infinite}
@@ -1371,7 +1710,6 @@ window.__ModuleLoader__.load({
1371
1710
  .ddp-menu button.sel,.ddp-menu button.sel svg{color:${ACCENT}}
1372
1711
  .ddp-menu em{margin-left:auto;font-style:normal;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
1373
1712
  .ddp-stage{position:relative;flex:1;min-height:0;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
1374
- .ddp-stage.full{background:#fff}
1375
1713
  .ddp-scr{position:absolute;inset:0;overflow:hidden}
1376
1714
  .ddp-away{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;font-size:13px;line-height:1.6;color:var(--dsw-alias-text-secondary,#888);background:var(--dsw-alias-bg-layer-1)}
1377
1715
  .ddp-frame{position:absolute;left:0;top:0;width:100%;height:100%;border:0;background:#fff;opacity:0;z-index:0;pointer-events:none}
@@ -1403,7 +1741,12 @@ window.__ModuleLoader__.load({
1403
1741
  .ddp-cap.w .sp{stroke:rgba(255,255,255,.25)}
1404
1742
  .ddp-cap.w .rg{stroke:#fff}
1405
1743
  .ddp-body{position:absolute;inset:0}
1406
- .ddp-body.vb{top:46px}
1744
+ .ddp-body.pad{top:${TOP_R}px;bottom:${BOT_R}px}
1745
+ .ddp-body.pad.vb{top:${TOP_R + 46}px}
1746
+ .ddp-body.pad.ease{transition:top .42s ${EASE}}
1747
+ .ddp-body.pad.ease .ddp-fit{transition:transform .42s ${EASE}}
1748
+ /* 网页单页夹在上下两排浮块中间:描一条细边,和画布底色分开 */
1749
+ .ddp-body.pad .ddp-dev.fill{box-shadow:0 0 0 .5px var(--dsw-alias-border-l3)}
1407
1750
  .ddp-single{position:absolute;inset:0;overflow:hidden}
1408
1751
  .ddp-fit{position:absolute;left:50%;top:50%;transform-origin:center center}
1409
1752
  .ddp-fit.fill{left:0;top:0;width:100%;height:100%;transform:none}
@@ -1421,15 +1764,64 @@ window.__ModuleLoader__.load({
1421
1764
  .ddp-lab span{overflow:hidden;text-overflow:ellipsis}
1422
1765
  .ddp-cell.sel .ddp-lab{color:${ACCENT};font-weight:500}
1423
1766
  .ddp-lab .sp{width:10px;height:10px;flex:none;border-radius:50%;border:1.5px solid rgba(77,107,254,.25);border-top-color:${ACCENT};animation:ddp-rot .75s linear infinite}
1424
- .ddp-cell.ghost>.ddp-dev{animation:ddp-ghost 1.6s ease-in-out infinite}
1425
1767
  .ddp-cell.ghost .ddp-vp::after{content:"";position:absolute;inset:0;z-index:3;border:calc(2px / var(--z)) dashed rgba(77,107,254,.55);border-radius:inherit}
1426
1768
  .ddp-cell.ghost .ddp-lab{color:${ACCENT}}
1427
1769
  @keyframes ddp-ghost{50%{opacity:.62}}
1428
- .ddp-zoom{position:absolute;right:12px;bottom:12px;z-index:6;display:flex;align-items:center;gap:1px;padding:3px;border-radius:9px;background:var(--dsw-alias-bg-layer-1);
1429
- box-shadow:0 4px 14px rgba(0,0,0,.14),0 0 0 .5px var(--dsw-alias-border-l3)}
1430
- .ddp-zoom .ddp-btn{height:24px;min-width:24px;padding:0 4px}
1431
- .ddp-zoom .ddp-btn svg{width:14px;height:14px}
1432
- .ddp-zoom b{min-width:36px;text-align:center;font-weight:400;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:11px}
1770
+ /* 生成中的占位:鼠标框版面 */
1771
+ .ddp-gd{position:absolute;inset:0;z-index:2;overflow:hidden;background:var(--dsw-alias-bg-layer-1);pointer-events:none}
1772
+ .ddp-gd .b{position:absolute;box-sizing:border-box;border:calc(1.5px / var(--z, 1)) dashed var(--c);border-radius:calc(8px / var(--z, 1));
1773
+ background:color-mix(in srgb,var(--c) 9%,transparent);overflow:hidden}
1774
+ .ddp-gd .b::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--c) 22%,transparent),transparent);animation:ddp-shine 1.3s ease-in-out infinite}
1775
+ .ddp-gd .c{position:absolute;z-index:2;display:block;width:0;height:0}
1776
+ .ddp-gd .c svg{display:block;width:calc(16px / var(--z, 1));height:calc(20px / var(--z, 1));overflow:visible;filter:drop-shadow(0 1px 2px rgba(0,0,0,.28))}
1777
+ .ddp-gd .c path{fill:var(--c);stroke:#fff;stroke-width:1.5;stroke-linejoin:round}
1778
+ .ddp-gd .c em{position:absolute;left:calc(12px / var(--z, 1));top:calc(16px / var(--z, 1));padding:calc(2px / var(--z, 1)) calc(7px / var(--z, 1));
1779
+ border-radius:calc(4px / var(--z, 1)) calc(8px / var(--z, 1)) calc(8px / var(--z, 1)) calc(8px / var(--z, 1));background:var(--c);color:#fff;
1780
+ font-style:normal;font-weight:600;font-size:calc(11px / var(--z, 1));line-height:1.35;white-space:nowrap;box-shadow:0 2px 6px rgba(0,0,0,.18)}
1781
+ .ddp-gd .c.flip em{left:auto;right:calc(-4px / var(--z, 1))}
1782
+ /* 边写边看:正在写的那页盖在占位上,页面报到了才淡入 */
1783
+ .ddp-draft{position:absolute;inset:0;z-index:3;width:100%;height:100%;border:0;display:block;background:transparent;opacity:0;transition:opacity .5s ${EASE};color-scheme:normal;pointer-events:none}
1784
+ .ddp-draft.on{opacity:1}
1785
+ .ddp-fitd{position:absolute;inset:0;z-index:3;overflow:hidden}
1786
+ .ddp-fitd>div{position:absolute;left:50%;top:50%;transform-origin:center center}
1787
+ /* 顶上的状态条:在做什么 · 用了多久;三个点是三只鼠标的颜色 */
1788
+ .ddp-busy{position:absolute;left:50%;top:12px;transform:translateX(-50%);z-index:7;display:flex;align-items:center;gap:8px;max-width:calc(100% - 24px);height:30px;box-sizing:border-box;
1789
+ padding:0 12px 0 11px;border-radius:999px;overflow:hidden;background:rgba(18,18,24,.84);color:#f1f1f4;font-size:12px;line-height:16px;
1790
+ backdrop-filter:blur(10px) saturate(1.4);box-shadow:0 8px 24px rgba(0,0,0,.2),inset 0 0 0 .5px rgba(255,255,255,.14);pointer-events:none;animation:ddp-drop .34s ${EASE}}
1791
+ .ddp-busy.side{left:calc((100% - 296px) / 2);max-width:calc(100% - 320px)}
1792
+ .ddp-stage.isl>.ddp-busy{top:58px}
1793
+ /* 右侧栏够宽:状态条放进上排两块浮块中间,不挡画布 */
1794
+ @container (min-width:860px){.ddp-stage.isl>.ddp-busy:not(.side){top:13px;max-width:calc(100% - 600px)}.ddp-stage.isl.bz:has(>.ddp-busy:not(.side))>.ddp-hint,.ddp-stage.isl.bz:has(>.ddp-busy:not(.side))>.ddp-toast:not(.mk),.ddp-stage.isl.bz:has(>.ddp-busy:not(.side))>.ddp-banner,.ddp-stage.isl.bz:has(>.ddp-busy:not(.side))>.ddp-tokens{top:58px}}
1795
+ .ddp-busy .dots{display:flex;gap:3px;flex:none}
1796
+ .ddp-busy .dots i{width:5px;height:5px;border-radius:50%;background:#6d87ff;animation:ddp-hop 1.2s ease-in-out infinite}
1797
+ .ddp-busy .dots i:nth-child(2){background:#ff8a4c;animation-delay:.15s}
1798
+ .ddp-busy .dots i:nth-child(3){background:#2fd3a0;animation-delay:.3s}
1799
+ .ddp-busy .q{width:8px;height:8px;flex:none;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.25)}
1800
+ .ddp-busy .tx{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
1801
+ background:linear-gradient(90deg,#f1f1f4 0 38%,#a9b8ff 50%,#f1f1f4 62% 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:ddp-txt 2.2s linear infinite}
1802
+ .ddp-busy.ask .tx{background:none;color:#f1f1f4;animation:none}
1803
+ .ddp-busy .sub{min-width:0;flex:0 1000 auto;color:rgba(241,241,244,.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1804
+ .ddp-busy .tm{flex:none;padding-left:8px;border-left:1px solid rgba(255,255,255,.16);color:rgba(241,241,244,.66);font-variant-numeric:tabular-nums}
1805
+ .ddp-busy .bar{position:absolute;left:0;right:0;bottom:0;height:2px;background:linear-gradient(90deg,transparent,#6d87ff,#a78bfa,#5eead4,transparent) no-repeat;background-size:38% 100%;animation:ddp-sweep 1.9s ease-in-out infinite}
1806
+ @keyframes ddp-hop{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-3px)}}
1807
+ @keyframes ddp-sweep{from{background-position:-60% 0}to{background-position:160% 0}}
1808
+ @keyframes ddp-drop{from{opacity:0;transform:translate(-50%,-8px)}}
1809
+ /* 状态条占了顶上那格:提示、消息、横幅往下挪一格 */
1810
+ .ddp-stage.bz>.ddp-hint,.ddp-stage.bz>.ddp-toast:not(.mk),.ddp-stage.bz~.ddp-toast:not(.mk),.ddp-stage.bz>.ddp-banner{top:54px}
1811
+ .ddp-stage.isl.bz>.ddp-hint,.ddp-stage.isl.bz>.ddp-toast:not(.mk),.ddp-stage.isl.bz>.ddp-banner,.ddp-stage.isl.bz>.ddp-tokens{top:100px}
1812
+ .ddp-stage.isl.bz>.ddp-toast.low{top:146px}
1813
+ .ddp-stage.isl.bz>.ddp-tokens{max-height:calc(100% - 100px - 74px)}
1814
+ @media (prefers-reduced-motion:reduce){
1815
+ .ddp-busy,.ddp-busy *,.ddp-gd *,.ddp-gd .b::after{animation:none!important}
1816
+ .ddp-busy .bar{display:none}
1817
+ .ddp-body.pad.ease,.ddp-body.pad.ease .ddp-fit{transition:none}
1818
+ }
1819
+ .ddp-zoom{position:absolute;right:10px;bottom:10px;z-index:6;display:flex;align-items:center;gap:2px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1);
1820
+ box-shadow:0 6px 20px rgba(0,0,0,.12),0 0 0 .5px var(--dsw-alias-border-l3)}
1821
+ .ddp-zoom .ddp-btn{height:30px;min-width:30px;padding:0 4px;border-radius:8px}
1822
+ .ddp-zoom .ddp-btn svg{width:15px;height:15px}
1823
+ .ddp-zoom .ddp-btn .lb{min-width:30px;text-align:right;font-variant-numeric:tabular-nums}
1824
+ .ddp-zoom .ddp-menu{top:auto;bottom:calc(100% + 8px);min-width:168px}
1433
1825
  .ddp-hint{position:absolute;left:50%;top:12px;transform:translateX(-50%);z-index:7;max-width:calc(100% - 24px);padding:6px 13px;border-radius:999px;
1434
1826
  background:rgba(18,18,24,.84);color:#ececf0;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
1435
1827
  box-shadow:0 6px 18px rgba(0,0,0,.22);backdrop-filter:blur(8px);pointer-events:none;animation:ddp-in .2s ease}
@@ -1487,7 +1879,7 @@ window.__ModuleLoader__.load({
1487
1879
  .ddp-mk-bar button.go{background:${ACCENT};color:#fff;font-weight:600}
1488
1880
  .ddp-mk-bar button.go:hover:not(:disabled){background:#3d5bf0}
1489
1881
  .ddp-mk-bar button.go.idle{background:rgba(77,107,254,.45);color:rgba(255,255,255,.75)}
1490
- .ddp-mk-coach{position:absolute;left:50%;top:50%;z-index:2;transform:translate(-50%,-50%);width:max-content;max-width:calc(100% - 32px);padding:14px 18px 12px;border-radius:14px;pointer-events:none;background:rgba(24,24,27,.86);color:#f4f4f5;font-size:13px;line-height:1.55;box-shadow:0 10px 30px rgba(0,0,0,.25);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:ddp-in .2s ease}
1882
+ .ddp-mk-coach{position:absolute;left:50%;bottom:72px;z-index:2;transform:translateX(-50%);width:max-content;max-width:calc(100% - 32px);padding:14px 18px 12px;border-radius:14px;pointer-events:none;background:rgba(24,24,27,.86);color:#f4f4f5;font-size:13px;line-height:1.55;box-shadow:0 10px 30px rgba(0,0,0,.25);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:ddp-in .2s ease}
1491
1883
  .ddp-mk-coach b{display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#fff}
1492
1884
  .ddp-mk-coach ol{margin:0;padding:0 0 0 1.3em;color:rgba(244,244,245,.86)}
1493
1885
  .ddp-mk-coach li+li{margin-top:2px}
@@ -1499,38 +1891,30 @@ window.__ModuleLoader__.load({
1499
1891
  @keyframes ddp-shine{100%{transform:translateX(100%)}}
1500
1892
  .ddp-wait{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px}
1501
1893
  .ddp-wait .card{position:relative;width:min(78%,440px);aspect-ratio:16/11;border-radius:12px;overflow:hidden;box-shadow:0 0 0 .5px var(--dsw-alias-border-l3),0 14px 40px rgba(0,0,0,.14);animation:ddp-ghost 1.6s ease-in-out infinite}
1894
+ .ddp-wait .card.k-app,.ddp-wait .card.k-miniprogram{width:auto;height:min(62%,560px);aspect-ratio:390/844;border-radius:22px}
1895
+ .ddp-wait .card.k-poster{width:auto;height:min(62%,560px);aspect-ratio:3/4}
1896
+ .ddp-wait .card.k-slides{aspect-ratio:16/9}
1897
+ .ddp-wait .card.k-email{width:min(60%,380px);aspect-ratio:3/4}
1898
+ .ddp-wait .card:has(.ddp-gd){animation:none}
1502
1899
  .ddp-wait p{margin:0;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.6}
1503
1900
  .ddp-toast{position:absolute;left:12px;right:12px;top:14px;width:fit-content;max-width:calc(100% - 24px);margin-inline:auto;z-index:7;padding:7px 14px;border-radius:14px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:1.5;text-align:center;overflow-wrap:anywhere;box-shadow:0 6px 20px rgba(0,0,0,.2);animation:ddp-in .2s ease}
1504
- .ddp-empty{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;justify-content:safe center;gap:12px;padding:32px 24px;text-align:center;color:var(--dsw-alias-label-tertiary)}
1505
- .ddp-empty b{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500}
1506
- .ddp-empty p{margin:6px 0 0;max-width:300px;line-height:1.6}
1507
- .ddp-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px;text-align:left;counter-reset:s}
1508
- .ddp-steps li{display:flex;align-items:center;gap:9px;color:var(--dsw-alias-label-secondary);line-height:18px}
1509
- .ddp-steps li::before{counter-increment:s;content:counter(s);flex:none;width:18px;height:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:600;color:${ACCENT};background:rgba(77,107,254,.12)}
1510
- .ddp-art{width:176px;height:120px;overflow:visible;margin-bottom:2px}
1511
- .ddp-art .pg{fill:var(--dsw-alias-bg-layer-1);stroke:var(--dsw-alias-border-l3);stroke-width:1}
1512
- .ddp-art .ln{stroke:var(--dsw-alias-border-l3);stroke-width:1}
1513
- .ddp-art .dt{fill:var(--dsw-alias-border-l3)}
1514
- .ddp-art .sk{fill:var(--dsw-alias-bg-layer-2)}
1515
- .ddp-art .bt{fill:${ACCENT};opacity:.9}
1516
- .ddp-art .sel{fill:rgba(77,107,254,.08);stroke:${ACCENT};stroke-width:1.3;stroke-dasharray:4 3;animation:ddp-ants .9s linear infinite,ddp-sel 3.6s ease-in-out infinite}
1517
- .ddp-art .cur{fill:var(--dsw-alias-label-primary);stroke:var(--dsw-alias-bg-layer-1);stroke-width:1;animation:ddp-cur 3.6s ease-in-out infinite}
1518
- @keyframes ddp-ants{to{stroke-dashoffset:-14}}
1519
- @keyframes ddp-sel{0%,30%{opacity:0}42%,88%{opacity:1}100%{opacity:0}}
1520
- @keyframes ddp-cur{0%,100%{transform:translate(132px,98px)}36%,90%{transform:translate(58px,76px)}}
1521
- .ddp-empty form{display:flex;flex-wrap:wrap;gap:6px;width:100%;max-width:320px;margin-top:4px}
1901
+ .ddp-home form{display:flex;flex-wrap:wrap;gap:6px;width:100%;margin:0}
1522
1902
  .ddp-browse{height:26px;padding:0 12px;flex:none;border:.5px solid var(--dsw-alias-border-l3,rgba(127,127,127,.4));border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}
1523
1903
  .ddp-browse:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1524
1904
  .ddp-drop .in.no{border-color:var(--dsw-alias-label-tertiary)}
1525
1905
  .ddp-drop .in.no svg{color:var(--dsw-alias-label-tertiary)}
1526
- .ddp-go{height:26px;padding:0 12px;border:0;border-radius:999px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font:inherit;cursor:pointer}
1906
+ .ddp-go{height:26px;padding:0 12px;border:0;border-radius:999px;background:${ACCENT};color:#fff;font:inherit;cursor:pointer}
1907
+ .ddp-go:hover{filter:brightness(.94)}
1908
+ .ddp-go.sec{background:transparent;color:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.35))}
1909
+ .ddp-go.sec:hover{filter:none;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1910
+ .ddp-go:disabled{filter:none;background:transparent;color:var(--dsw-alias-label-tertiary);box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.3));cursor:default}
1527
1911
  .ddp-err{color:var(--dsw-alias-state-error-primary,#e5484d);font-size:11.5px}
1528
1912
  .ddp-head{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;position:relative}
1529
1913
  .ddp-head:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1530
1914
  .ddp-head svg{width:16px;height:16px}
1531
1915
  .ddp-head.hint::after{content:"";position:absolute;right:5px;top:5px;width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-accent-primary,#4f6ef7)}
1532
1916
  .ddp-head.live::after{content:"";position:absolute;right:5px;top:5px;width:5px;height:5px;border-radius:50%;background:#22c55e}
1533
- .ddp-tokens{position:absolute;top:10px;right:10px;bottom:10px;width:min(276px,calc(100% - 20px));z-index:7;display:flex;flex-direction:column;border-radius:12px;
1917
+ .ddp-tokens{position:absolute;top:10px;right:10px;max-height:calc(100% - 20px);width:min(276px,calc(100% - 20px));z-index:7;display:flex;flex-direction:column;border-radius:12px;
1534
1918
  background:var(--dsw-alias-bg-layer-1);box-shadow:0 12px 36px rgba(0,0,0,.22),0 0 0 .5px var(--dsw-alias-border-l3);overflow:hidden;animation:ddp-in .2s ${EASE}}
1535
1919
  .ddp-tokens header{display:flex;align-items:center;gap:8px;padding:10px 8px 2px 14px;font-size:13px;font-weight:500}
1536
1920
  .ddp-tokens header b{flex:1;font-weight:500}
@@ -1539,7 +1923,9 @@ window.__ModuleLoader__.load({
1539
1923
  .ddp-tokens .tip{margin:0;padding:0 14px 8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1540
1924
  .ddp-tokens .bd{flex:1;min-height:0;overflow:auto;padding:0 8px 10px}
1541
1925
  .ddp-tokens h4{margin:10px 6px 4px;font-size:11px;font-weight:500;color:var(--dsw-alias-label-tertiary)}
1542
- .ddp-phone{bottom:auto;max-height:calc(100% - 20px)}
1926
+ .ddp-phone details{margin-top:4px;text-align:left}
1927
+ .ddp-phone details summary{cursor:pointer;text-align:center}
1928
+ .ddp-phone details p{margin-top:4px}
1543
1929
  .ddp-phone .bd{display:flex;flex-direction:column;align-items:center;text-align:center;padding:4px 16px 14px;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
1544
1930
  .ddp-phone .bd>*{flex:none}
1545
1931
  .ddp-phone .qr{width:176px;height:176px;margin:6px 0 4px;padding:0;border-radius:12px;background:#fff;box-shadow:0 0 0 .5px rgba(0,0,0,.12)}
@@ -1560,6 +1946,9 @@ window.__ModuleLoader__.load({
1560
1946
  .ddp-tokens footer button{display:flex;align-items:center;gap:7px;width:100%;height:30px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;text-align:left}
1561
1947
  .ddp-tokens footer button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1562
1948
  .ddp-tokens footer svg{width:15px;height:15px;flex:none}
1949
+ /* 所有面板一样的禁用样子:描边、浅字,不像能点;主按钮都是 DSH 的蓝 */
1950
+ .ddp-tokens footer button:disabled{background:transparent;color:var(--dsw-alias-label-tertiary);box-shadow:none;cursor:default;opacity:1;filter:none}
1951
+ .ddp-tokens footer .go:disabled{box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.3))}
1563
1952
  .ddp{position:relative;outline:none}
1564
1953
  .ddp-drop{position:absolute;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;padding:16px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 72%,transparent);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);animation:ddp-in .15s ease}
1565
1954
  .ddp-drop *{pointer-events:none}
@@ -1567,9 +1956,8 @@ window.__ModuleLoader__.load({
1567
1956
  .ddp-drop .in{display:flex;flex-direction:column;align-items:center;gap:6px;width:100%;max-width:340px;padding:28px 20px;border:1.5px dashed ${ACCENT};border-radius:16px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 10px 30px rgba(0,0,0,.18);text-align:center;color:var(--dsw-alias-label-tertiary);line-height:1.5}
1568
1957
  .ddp-drop .in svg{width:28px;height:28px;color:${ACCENT};margin-bottom:4px}
1569
1958
  .ddp-drop .in b{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500}
1570
- .ddp-empty .ddp-refline{margin-top:0}
1571
- .ddp-empty .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
1572
- .ddp-empty .lk:hover{text-decoration:underline;text-underline-offset:2px}
1959
+ .ddp-home .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
1960
+ .ddp-home .lk:hover{text-decoration:underline;text-underline-offset:2px}
1573
1961
  .ddp-ck{align-items:flex-start}
1574
1962
  .ddp-ck .no{width:18px;height:18px;flex:none;margin-top:1px;border-radius:9px;background:#f59e0b;color:#fff;font:700 10px/18px system-ui,sans-serif;text-align:center}
1575
1963
  .ddp-ck .no.hi{background:#e5484d}
@@ -1577,10 +1965,10 @@ window.__ModuleLoader__.load({
1577
1965
  .ddp-tok.ddp-ck .vl{white-space:normal;overflow-wrap:anywhere}
1578
1966
  .ddp-check .ok{display:flex;align-items:center;gap:8px;padding:14px 6px;color:var(--dsw-alias-label-secondary)}
1579
1967
  .ddp-check .ok svg{width:16px;height:16px;flex:none;color:#30a46c}
1580
- .ddp-check footer.row,.ddp-style footer.row{display:flex;gap:6px}
1581
- .ddp-check footer.row button,.ddp-style footer.row button{flex:1;justify-content:center;padding:0 6px}
1582
- .ddp-check footer .go,.ddp-style footer .go{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
1583
- .ddp-check footer .go:hover,.ddp-style footer .go:hover{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);opacity:.88}
1968
+ .ddp-check footer.row,.ddp-style footer.row,.ddp-look footer.row{display:flex;gap:6px}
1969
+ .ddp-check footer.row button,.ddp-style footer.row button,.ddp-look footer.row button{flex:1;justify-content:center;padding:0 6px}
1970
+ .ddp-check footer .go,.ddp-style footer .go,.ddp-look footer .go{background:${ACCENT};color:#fff}
1971
+ .ddp-check footer .go:hover,.ddp-style footer .go:hover,.ddp-look footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
1584
1972
  .ddp-check footer button:disabled,.ddp-style footer button:disabled{opacity:.45;cursor:default}
1585
1973
  .ddp-style .bd{padding:0 14px 10px}
1586
1974
  .ddp-style .who{margin:0;padding:0 14px 6px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1648,14 +2036,114 @@ window.__ModuleLoader__.load({
1648
2036
  .ddp-samview .row em{min-width:40px;font-style:normal;font-size:11px;text-align:center;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
1649
2037
  .ddp-samview .row .ddp-go{margin-left:auto;height:28px;font-size:12px}
1650
2038
  .ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1651
- .ddp-tries{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:440px}
1652
- .ddp-tries>span{flex-basis:100%;font-size:11px;line-height:16px}
1653
- .ddp-try{height:28px;padding:0 12px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:999px;background:transparent;
1654
- color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
1655
- .ddp-try:hover{border-color:${ACCENT};color:${ACCENT}}
1656
- .ddp-stylepick{width:100%;max-width:440px;margin:4px 0 6px;text-align:left}
1657
- .ddp-stylepick>b{display:block;margin-bottom:2px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
1658
- .ddp-stylepick>p{margin:0 0 10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
2039
+ /* ── 首页:顶上页签、一句问话、大输入框、接着做、模板 ── */
2040
+ .ddp-home{position:relative;flex:1;min-height:0;display:flex;flex-direction:column;color:var(--dsw-alias-label-secondary);background:radial-gradient(120% 320px at 50% -80px,color-mix(in srgb,${ACCENT} 14%,transparent),transparent 72%)}
2041
+ .ddp-homebar{flex:none;display:flex;align-items:center;gap:8px;height:48px;padding:0 12px 0 16px}
2042
+ .ddp-homebar .nm{display:flex;align-items:center;gap:8px;min-width:0}
2043
+ .ddp-homebar .nm b{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap}
2044
+ .ddp-homebar .mk{flex:none;width:18px;height:18px;border-radius:6px;background:linear-gradient(135deg,#8ea0ff,${ACCENT} 55%,#3a55e0);box-shadow:0 2px 6px rgba(77,107,254,.35)}
2045
+ .ddp-homebar .seg{margin-left:auto}
2046
+ .ddp-homebar>.ddp-btn{margin-left:-6px}
2047
+ .ddp-homeback{display:flex;align-items:center;gap:3px;min-width:0;max-width:46%;height:30px;margin-left:-8px;padding:0 10px 0 5px;border:0;border-radius:8px;background:transparent;
2048
+ color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;flex:0 1 auto}
2049
+ .ddp-homeback svg{width:15px;height:15px;flex:none}
2050
+ .ddp-homeback span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2051
+ .ddp-homeback:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
2052
+ .ddp-homebar.led .nm{display:none}
2053
+ /* 开着设计时叠上来的首页:淡入、轻轻放大;几块依次浮上来;收起反过来 */
2054
+ .ddp-home.over~.ddp-libwrap{z-index:11}
2055
+ .ddp-home.over{position:absolute;inset:0;z-index:10;background-color:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));animation:ddp-homein .28s ${EASE} both}
2056
+ .ddp-home.over.out{animation:ddp-homeout .19s ease both;pointer-events:none}
2057
+ .ddp-home .ddp-start>*{animation:ddp-rise .5s ${EASE} both}
2058
+ .ddp-home .ddp-start>:nth-child(2){animation-delay:.05s}
2059
+ .ddp-home .ddp-start>:nth-child(3){animation-delay:.1s}
2060
+ .ddp-home .ddp-start>:nth-child(4){animation-delay:.15s}
2061
+ .ddp-home .ddp-start>:nth-child(n+5){animation-delay:.2s}
2062
+ @keyframes ddp-homein{from{opacity:0;transform:scale(.985)}}
2063
+ @keyframes ddp-homeout{to{opacity:0;transform:scale(.985)}}
2064
+ @keyframes ddp-rise{from{opacity:0;transform:translateY(10px)}}
2065
+ .ddp-asknote{display:flex;align-items:center;justify-content:center;gap:5px;margin:-4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
2066
+ .ddp-asknote svg{width:13px;height:13px;flex:none}
2067
+ .ddp-recent .wk{position:relative}
2068
+ .ddp-recent .wk.here .im{box-shadow:0 0 0 2px ${ACCENT}}
2069
+ .ddp-recent .wk .now{position:absolute;left:8px;top:8px;z-index:2;padding:1px 7px;border-radius:999px;background:${ACCENT};color:#fff;font-style:normal;font-size:11px;font-weight:600;line-height:18px;box-shadow:0 2px 6px rgba(0,0,0,.18)}
2070
+ @media (prefers-reduced-motion:reduce){.ddp-home.over,.ddp-home.over.out,.ddp-home .ddp-start>*{animation:none}}
2071
+ .ddp-lib>.ddp-homebar{border-bottom:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
2072
+ .ddp-homebd{flex:1;min-height:0;overflow-y:auto;padding:0 20px 36px}
2073
+ .ddp-start{width:100%;max-width:600px;margin:0 auto;display:flex;flex-direction:column;gap:32px;text-align:left}
2074
+ .ddp-hero{margin:clamp(12px,7vh,52px) 0 -14px;text-align:center}
2075
+ .ddp-hero h1{margin:0;font-size:26px;line-height:34px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary);text-wrap:balance;word-break:keep-all}
2076
+ .ddp-hero p{margin:8px auto 0;max-width:440px;font-size:13px;line-height:20px;text-wrap:balance;word-break:keep-all}
2077
+ .ddp-start>.ddp-go{align-self:center;margin:-16px 0}
2078
+ .ddp-askwrap{display:flex;flex-direction:column;gap:14px}
2079
+ .ddp-ask{position:relative;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.25));border-radius:16px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 10px 30px rgba(0,0,0,.07),0 0 0 4px color-mix(in srgb,${ACCENT} 6%,transparent);transition:border-color .15s,box-shadow .15s}
2080
+ .ddp-ask:focus-within{border-color:color-mix(in srgb,${ACCENT} 50%,transparent);box-shadow:0 10px 30px rgba(0,0,0,.09),0 0 0 4px color-mix(in srgb,${ACCENT} 14%,transparent)}
2081
+ .ddp-askta{display:block;box-sizing:border-box;width:100%;min-height:78px;max-height:200px;field-sizing:content;padding:14px 16px 2px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;resize:none;outline:none}
2082
+ .ddp-askta::placeholder{color:var(--dsw-alias-label-tertiary)}
2083
+ .ddp-ask>.row{display:flex;align-items:center;gap:6px;padding:6px 10px 10px}
2084
+ .ddp-ask .chip{display:inline-flex;align-items:center;gap:5px;min-width:0;height:30px;padding:0 11px;border:0;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
2085
+ .ddp-ask .chip:hover,.ddp-ask .chip[aria-expanded=true]{background:color-mix(in srgb,var(--dsw-alias-label-primary) 9%,transparent);color:var(--dsw-alias-label-primary)}
2086
+ .ddp-ask .chip>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
2087
+ .ddp-ask .chip svg{flex:none;width:14px;height:14px}
2088
+ .ddp-ask .chip .cr{width:10px;height:10px;opacity:.6}
2089
+ .ddp-ask .chip.on{background:color-mix(in srgb,${ACCENT} 13%,transparent);color:${ACCENT}}
2090
+ .ddp-ask .chip .sw{flex:none;width:12px;height:12px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)}
2091
+ .ddp-askplus>.ddp-btn{width:30px;min-width:30px;height:30px;padding:0;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
2092
+ .ddp-askplus>.ddp-btn:hover,.ddp-askplus>.ddp-btn.sel{background:color-mix(in srgb,var(--dsw-alias-label-primary) 9%,transparent)}
2093
+ .ddp-askplus>.ddp-btn svg{width:15px;height:15px}
2094
+ .ddp-askplus .ddp-menu{left:0;right:auto;min-width:200px}
2095
+ .ddp-ask .tabhint{margin-left:auto;display:flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;cursor:pointer;border:0;padding:0 2px;background:transparent;font-family:inherit}
2096
+ .ddp-ask .tabhint:hover{color:var(--dsw-alias-label-secondary)}
2097
+ .ddp-ask kbd{padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px currentColor;font:500 10.5px/16px ui-monospace,SFMono-Regular,Menlo,monospace;opacity:.8}
2098
+ .ddp-ask .send{flex:none;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;margin-left:auto;padding:0;border:0;border-radius:50%;background:${ACCENT};color:#fff;cursor:pointer;transition:filter .15s,opacity .15s}
2099
+ .ddp-ask .tabhint+.send{margin-left:8px}
2100
+ .ddp-ask .send:disabled{opacity:.3;cursor:default}
2101
+ .ddp-ask .send:hover:not(:disabled){filter:brightness(1.08)}
2102
+ .ddp-ask .send svg{width:17px;height:17px}
2103
+ .ddp-askpop{position:absolute;left:-1px;right:-1px;top:calc(100% + 8px);z-index:20;max-height:min(460px,62vh);overflow-y:auto;overscroll-behavior:contain;padding:14px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.25));border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 16px 40px rgba(0,0,0,.18);text-align:left}
2104
+ .ddp-askpop .hd{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
2105
+ .ddp-askpop .hd b{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
2106
+ .ddp-askpop .hd .lk{margin-left:auto;font-size:12px}
2107
+ .ddp-askpop .ddp-styles{grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px}
2108
+ .ddp-askkinds{display:flex;flex-wrap:wrap;justify-content:center;gap:5px}
2109
+ .ddp-askkinds button{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 9px 0 8px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.28));border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;white-space:nowrap;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
2110
+ .ddp-askkinds button:hover{border-color:var(--dsw-alias-label-tertiary);color:var(--dsw-alias-label-primary)}
2111
+ .ddp-askkinds button.on{border-color:${ACCENT};background:color-mix(in srgb,${ACCENT} 10%,transparent);color:${ACCENT}}
2112
+ .ddp-askkinds svg{width:13px;height:13px}
2113
+ .ddp-homesec>.hd{display:flex;align-items:baseline;gap:8px;margin-bottom:12px}
2114
+ .ddp-homesec>.hd b{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
2115
+ .ddp-homesec>.hd .lk{margin-left:auto;font-size:12px}
2116
+ .ddp-recent,.ddp-tplstart .row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 12px}
2117
+ .ddp-recent .wk,.ddp-tplstart .tp{display:flex;flex-direction:column;gap:8px;min-width:0;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
2118
+ .ddp-recent .im,.ddp-tplstart .im{position:relative;display:flex;align-items:center;justify-content:center;border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);box-shadow:0 0 0 .5px rgba(127,127,127,.35);color:var(--dsw-alias-label-tertiary);transition:transform .18s ${EASE},box-shadow .18s}
2119
+ .ddp-recent .im{aspect-ratio:16/10}
2120
+ .ddp-tplstart .im{aspect-ratio:4/5}
2121
+ .ddp-recent .im img,.ddp-tplstart .im img{position:absolute;inset:0;z-index:1;width:100%;height:100%;object-fit:cover;object-position:top}
2122
+ .ddp-recent .im svg,.ddp-tplstart .im svg{width:22px;height:22px}
2123
+ .ddp-recent .wk:hover .im,.ddp-recent .wk:focus-visible .im,.ddp-tplstart .tp:hover .im,.ddp-tplstart .tp:focus-visible .im{transform:translateY(-2px);box-shadow:0 0 0 1.5px ${ACCENT},0 8px 20px rgba(0,0,0,.12)}
2124
+ .ddp-recent .wk:focus-visible,.ddp-tplstart .tp:focus-visible{outline:none}
2125
+ .ddp-recent .tx,.ddp-tplstart .tx{display:flex;flex-direction:column;gap:1px;min-width:0;padding:0 2px}
2126
+ .ddp-recent .tx b,.ddp-tplstart .tx b{font-size:12.5px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2127
+ .ddp-recent .tx span,.ddp-tplstart .tx span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2128
+ @container (max-width:470px){
2129
+ .ddp-homebar .nm b{display:none}
2130
+ .ddp-homebd{padding:0 14px 28px}
2131
+ .ddp-hero h1{font-size:22px;line-height:30px}
2132
+ .ddp-recent,.ddp-tplstart .row{grid-template-columns:repeat(2,minmax(0,1fr))}
2133
+ .ddp-recent .wk:nth-child(3){display:none}
2134
+ .ddp-ask .tabhint{display:none}
2135
+ .ddp-ask .tabhint+.send{margin-left:auto}
2136
+ }
2137
+ @container (max-width:340px){.ddp-ask .chip.ref>span{display:none}.ddp-ask .chip.ref{padding:0 8px}}
2138
+ .ddp-more-st{display:block;margin-top:10px;font-size:12px}
2139
+ .ddp-ways{padding-top:16px;border-top:.5px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3))}
2140
+ .ddp-ways summary{display:inline-flex;align-items:center;gap:6px;list-style:none;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
2141
+ .ddp-ways summary::-webkit-details-marker{display:none}
2142
+ .ddp-ways summary:hover{color:var(--dsw-alias-label-primary)}
2143
+ .ddp-ways summary svg{width:12px;height:12px;transform:rotate(-90deg);transition:transform .15s ${EASE}}
2144
+ .ddp-ways[open] summary svg{transform:none}
2145
+ .ddp-ways .in{display:flex;flex-direction:column;gap:10px;margin-top:12px}
2146
+ .ddp-ways .in p{margin:0;line-height:1.55}
1659
2147
  .ddp-tok{display:flex;align-items:center;gap:9px;width:100%;min-height:32px;padding:4px 6px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}
1660
2148
  .ddp-tok:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1661
2149
  .ddp-tok .pv{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}
@@ -1699,13 +2187,18 @@ window.__ModuleLoader__.load({
1699
2187
  background:var(--dsw-alias-bg-layer-1);box-shadow:0 8px 24px rgba(0,0,0,.18),0 0 0 .5px var(--dsw-alias-border-l3);font-size:12px;white-space:nowrap;animation:ddp-in .2s ${EASE}}
1700
2188
  .ddp-banner>svg{width:14px;height:14px;flex:none;color:${ACCENT}}
1701
2189
  .ddp-banner>span{overflow:hidden;text-overflow:ellipsis;min-width:0;margin-right:2px}
1702
- .ddp-banner .ddp-go{height:24px;padding:0 10px;font-size:12px;flex:none;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1703
- .ddp-banner .ddp-go.on{background:${ACCENT};color:#fff}
1704
- .ddp-banner .ddp-go.pri{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
2190
+ /* 试用横幅:「试用中」蓝字、屏名正文、提示淡;窄了先收提示 */
2191
+ .ddp-banner>b{flex:none;font-weight:600;color:${ACCENT}}
2192
+ .ddp-banner>.nm{color:var(--dsw-alias-label-primary)}
2193
+ .ddp-banner>em{flex:none;margin-right:4px;font-style:normal;color:var(--dsw-alias-label-tertiary)}
2194
+ @container (max-width:420px){.ddp-banner>em{display:none}}
2195
+ .ddp-banner .ddp-go{height:24px;padding:0 10px;font-size:12px;flex:none}
2196
+ .ddp-banner .ddp-go.sec.on{background:rgba(77,107,254,.12);color:${ACCENT};box-shadow:inset 0 0 0 1px ${ACCENT}}
1705
2197
  .ddp-banner .ddp-btn{height:24px;min-width:24px;border-radius:999px}
1706
2198
  .ddp-toast{display:flex;align-items:center;gap:10px}
1707
2199
  .ddp-toast.low{top:56px}
1708
2200
  .ddp-toast.mk{top:auto;bottom:62px}
2201
+ .ddp-toast.side{right:308px}
1709
2202
  .ddp-toast button{border:0;padding:0;background:transparent;color:inherit;font:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;cursor:pointer}
1710
2203
  .ddp-cmp{position:absolute;inset:0;overflow:hidden}
1711
2204
  .ddp-cmp>.ly{position:absolute;inset:0}
@@ -1722,20 +2215,60 @@ window.__ModuleLoader__.load({
1722
2215
  .ddp-cmp .tag{position:absolute;top:10px;z-index:4;padding:3px 9px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;font:500 11px/16px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;pointer-events:none}
1723
2216
  .ddp-cmp .tag.l{left:10px}
1724
2217
  .ddp-cmp .tag.r{right:10px;background:${ACCENT}}
1725
- .ddp-act{margin-left:auto;display:flex;gap:1px;flex:none}
2218
+ /* 屏头小按钮:悬停或选中那屏才出来(平时不占地方,给屏名和「用这版」留位置) */
2219
+ .ddp-act{margin-left:auto;display:none;gap:1px;flex:none}
2220
+ .ddp-cell:hover .ddp-act,.ddp-cell.sel .ddp-act{display:flex}
2221
+ .ddp-vt{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 4px;border-radius:5px;background:rgba(127,127,127,.14);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}
2222
+ .ddp-cell.sel .ddp-vt{background:rgba(77,107,254,.14);color:${ACCENT}}
1726
2223
  .ddp-take{flex:none;display:inline-flex;align-items:center;gap:3px;height:20px;padding:0 8px 0 6px;border:0;border-radius:10px;
1727
2224
  background:rgba(77,107,254,.12);color:${ACCENT};font:inherit;font-size:11px;font-weight:500;cursor:pointer}
1728
2225
  .ddp-take:hover{background:rgba(77,107,254,.2)}
1729
2226
  .ddp-take svg{width:12px;height:12px;flex:none}
1730
2227
  /* 画布缩得很小时只留 ✓,屏名先保住 */
1731
2228
  .ddp-lab{container-type:inline-size}
1732
- .ddp-lab>span:first-child{min-width:3em}
1733
- @container (max-width:170px){.ddp-take{padding:0 4px}.ddp-take span{display:none}}
2229
+ .ddp-lab>span:first-of-type{min-width:3em}
2230
+ @container (max-width:130px){.ddp-take{padding:0 4px}.ddp-take span{display:none}}
2231
+ @container (max-width:230px){.ddp-cell:hover .ddp-take,.ddp-cell.sel .ddp-take{padding:0 4px}.ddp-cell:hover .ddp-take span,.ddp-cell.sel .ddp-take span{display:none}}
1734
2232
  .ddp-act .ddp-btn{height:22px;min-width:22px;padding:0 4px;color:${ACCENT}}
1735
2233
  .ddp-act .ddp-btn svg{width:13px;height:13px}
1736
- @container (max-width:500px){.ddp-btn.pick span,.ddp-kind>.ddp-btn span{display:none}}
2234
+ /* 屏头的小按钮:屏够宽就带字(导出 / 再出两版 / 单看),窄了只剩图标,悬停看说明 */
2235
+ .ddp-act .lb{display:none;font-size:11px}
2236
+ @container (min-width:300px){.ddp-act .lb{display:inline}.ddp-act{gap:4px}}
1737
2237
  .ddp-tw .row:first-child{border-top:0}
1738
2238
  .ddp-sys .bd{display:flex;flex-direction:column;gap:8px;padding:2px 10px 10px}
2239
+ .ddp-sys.bare{flex:1;min-height:0;display:flex;flex-direction:column}
2240
+ .ddp-sys>form{display:flex;gap:6px;flex:none;padding:0 10px 10px}
2241
+ /* 外观面板:三页切换;整套里颜色两列、点色块直接改 */
2242
+ .ddp-lktabs{display:flex;gap:2px;flex:none;margin:6px 12px 8px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1))}
2243
+ .ddp-lktabs button{flex:1;height:26px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/26px inherit;cursor:pointer}
2244
+ .ddp-lktabs button:hover{color:var(--dsw-alias-label-primary)}
2245
+ .ddp-lktabs button.on{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.14)}
2246
+ .ddp-look h4{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px;margin-top:12px}
2247
+ .ddp-look .bd>h4:first-child{margin-top:4px}
2248
+ .ddp-look h4 span{font-weight:400}
2249
+ .ddp-lkc{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px}
2250
+ .ddp-lkc .it{display:flex;align-items:center;gap:8px;min-width:0;padding:4px 6px;border-radius:7px;cursor:pointer}
2251
+ .ddp-lkc .it:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
2252
+ .ddp-lkc .sw{position:relative;width:24px;height:24px;flex:none;border-radius:7px;box-shadow:inset 0 0 0 .5px rgba(127,127,127,.35);overflow:hidden}
2253
+ .ddp-lkc .sw input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;cursor:pointer}
2254
+ .ddp-lkc .it.on .sw{box-shadow:0 0 0 1.5px ${ACCENT}}
2255
+ .ddp-lkc .tx{flex:1;min-width:0;display:flex;flex-direction:column}
2256
+ .ddp-lkc .nm{font-size:12px;line-height:16px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2257
+ .ddp-lkc .vl{font:10.5px/14px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2258
+ .ddp-lkc .it.on .vl{color:${ACCENT}}
2259
+ .ddp-look .ddp-tok .nm{font:12px/16px inherit}
2260
+ .ddp-lktw{padding:0 6px}
2261
+ .ddp-lktw .row:first-child{border-top:0;padding-top:2px}
2262
+ .ddp-lk-none{margin:0 6px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}
2263
+ .ddp-tokens .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
2264
+ .ddp-tokens .lk:hover{text-decoration:underline;text-underline-offset:2px}
2265
+ .ddp-lk-more{display:flex;flex-direction:column;gap:2px;margin-top:12px;padding-top:8px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
2266
+ .ddp-lk-more button{display:flex;align-items:center;gap:7px;height:30px;padding:0 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}
2267
+ .ddp-lk-more button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
2268
+ .ddp-lk-more svg{width:15px;height:15px;flex:none}
2269
+ .ddp-look footer.row{flex-wrap:wrap;align-items:center}
2270
+ .ddp-look footer.row .cnt{flex-basis:100%;padding:0 2px 2px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2271
+ .ddp-style .who b{margin-right:2px;font-weight:500;color:var(--dsw-alias-label-primary)}
1739
2272
  .ddp-sysc{display:flex;flex-direction:column;gap:5px;padding:8px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}
1740
2273
  .ddp-sysc.def{box-shadow:inset 0 0 0 1.5px ${ACCENT}}
1741
2274
  .ddp-sysc img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;object-position:top;border-radius:6px;background:var(--dsw-alias-bg-layer-1)}
@@ -1752,38 +2285,42 @@ window.__ModuleLoader__.load({
1752
2285
  .ddp-sys form{display:flex;gap:6px}
1753
2286
  .ddp-sys form input,.ddp-ho input[type=text]{flex:1;min-width:0;box-sizing:border-box;height:28px;padding:0 9px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;outline:none}
1754
2287
  .ddp-sys form input:focus,.ddp-ho input[type=text]:focus{border-color:${ACCENT}}
1755
- .ddp-tokens.ddp-sys footer form button{width:auto;height:28px;flex:none;justify-content:center;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border-radius:999px;padding:0 12px}
1756
- .ddp-tokens.ddp-sys footer form button:disabled{opacity:.45;cursor:default}
2288
+ .ddp-sys>form button[type=submit]{height:28px;flex:none;padding:0 12px;border:0;border-radius:8px;background:${ACCENT};color:#fff;font:12px inherit;cursor:pointer}
2289
+ .ddp-sys>form button[type=submit]:disabled{background:transparent;color:var(--dsw-alias-label-tertiary);box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.3));cursor:default}
1757
2290
  .ddp-ho .bd{padding:0 14px 10px}
1758
2291
  .ddp-ho .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
1759
2292
  .ddp-ho .row:first-child{border-top:0}
1760
2293
  .ddp-ho label{display:block;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
1761
2294
  .ddp-ho .pathrow{display:flex;gap:6px;align-items:center}
2295
+ .ddp-ho .pathv{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:28px;padding:0 9px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;text-align:left;cursor:text}
2296
+ .ddp-ho .pathv:hover{border-color:var(--dsw-alias-label-tertiary)}
2297
+ .ddp-ho .pathv svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-secondary)}
2298
+ .ddp-ho .pathv b{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none;max-width:60%}
2299
+ .ddp-ho .pathv em{font-style:normal;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1762
2300
  .ddp-ho .moved{margin:6px 0 0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
1763
2301
  .ddp-ho .moved.bad{color:var(--dsw-alias-state-error-primary,#d54941)}
1764
2302
  .ddp-dirbtn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;flex:none;padding:0;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer}
1765
2303
  .ddp-dirbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.5))}
1766
2304
  .ddp-dirbtn:disabled{opacity:.5;cursor:default}
1767
2305
  .ddp-dirbtn svg{width:15px;height:15px}
1768
- .ddp-tokens.ddp-sys footer form button.ddp-dirbtn{width:28px;padding:0;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border-radius:8px}
1769
- .ddp-tokens.ddp-sys footer form button.ddp-dirbtn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.5))}
2306
+ .ddp-sys>form button.ddp-dirbtn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.5))}
1770
2307
  .ddp-ho input[type=text]{width:100%;box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
1771
2308
  .ddp-ho .opt{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;padding:4px 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/18px inherit;text-align:left;cursor:pointer}
1772
2309
  .ddp-ho .opt:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1773
2310
  .ddp-ho .opt i{width:12px;height:12px;flex:none;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--dsw-alias-label-tertiary)}
1774
2311
  .ddp-ho .opt.on{color:var(--dsw-alias-label-primary)}
1775
2312
  .ddp-ho .opt.on i{box-shadow:inset 0 0 0 4px ${ACCENT}}
1776
- .ddp-ho footer .go{justify-content:center;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
1777
- .ddp-ho footer .go:hover{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);opacity:.88}
1778
- .ddp-ho footer .go:disabled{opacity:.45;cursor:default}
1779
- .ddp-libwrap{position:absolute;inset:0;z-index:8;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1))}
2313
+ .ddp-ho footer .go{justify-content:center;background:${ACCENT};color:#fff}
2314
+ .ddp-ho footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
2315
+
2316
+ .ddp-libwrap{position:absolute;inset:0;z-index:10;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1))}
1780
2317
  .ddp-lib{flex:1;min-height:0;display:flex;flex-direction:column;animation:ddp-in .2s ${EASE}}
1781
- .ddp-lib>header{flex:none;display:flex;align-items:center;gap:8px;height:44px;padding:0 10px 0 6px;border-bottom:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
1782
- .ddp-lib>header b{font-size:13px;font-weight:600;white-space:nowrap}
1783
- .ddp-lib .seg{margin-left:auto;display:inline-flex;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
1784
- .ddp-lib .seg button{height:26px;padding:0 12px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap}
1785
- .ddp-lib .seg button:hover{color:var(--dsw-alias-label-primary)}
1786
- .ddp-lib .seg button.on{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.12)}
2318
+ .ddp-lib>header:not(.ddp-homebar){flex:none;display:flex;align-items:center;gap:8px;height:44px;padding:0 10px 0 6px;border-bottom:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
2319
+ .ddp-lib>header:not(.ddp-homebar) b{font-size:13px;font-weight:600;white-space:nowrap}
2320
+ .ddp-lib .seg,.ddp-homebar .seg{margin-left:auto;display:inline-flex;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
2321
+ .ddp-lib .seg button,.ddp-homebar .seg button{height:26px;padding:0 12px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap}
2322
+ .ddp-lib .seg button:hover,.ddp-homebar .seg button:hover{color:var(--dsw-alias-label-primary)}
2323
+ .ddp-lib .seg button.on,.ddp-homebar .seg button.on{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.12)}
1787
2324
  .ddp-lib>.bd{flex:1;min-height:0;overflow:auto;padding:14px}
1788
2325
  .ddp-pack .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
1789
2326
  .ddp-pack .lk:hover{text-decoration:underline;text-underline-offset:2px}
@@ -1796,11 +2333,11 @@ window.__ModuleLoader__.load({
1796
2333
  .ddp-libnone p{margin:0;max-width:320px}
1797
2334
  .ddp-libfoot{margin:18px 0 0;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary);text-align:center}
1798
2335
  .ddp-works{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:16px 14px}
1799
- .ddp-work{display:flex;flex-direction:column;gap:4px;min-width:0}
2336
+ .ddp-work{display:flex;flex-direction:column;gap:4px;min-width:0;container-type:inline-size}
1800
2337
  .ddp-work .th{position:relative;display:block;aspect-ratio:4/3;padding:0;border:0;border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);cursor:pointer;box-shadow:0 0 0 .5px rgba(127,127,127,.35);transition:transform .15s ${EASE},box-shadow .15s}
1801
2338
  .ddp-work .th:hover:not(:disabled),.ddp-work .th:focus-visible{outline:none;transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
1802
2339
  .ddp-work .th:disabled{cursor:default}
1803
- .ddp-work .th img{position:absolute;inset:0;z-index:1;width:100%;height:100%;object-fit:cover;object-position:top}
2340
+ .ddp-work .th img{position:absolute;inset:0;z-index:1;width:100%;height:100%;object-fit:cover;object-position:left top}
1804
2341
  .ddp-work .th .ic{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}
1805
2342
  .ddp-work .th .ic svg{width:26px;height:26px}
1806
2343
  .ddp-work .th em{position:absolute;left:8px;top:8px;z-index:2;padding:1px 7px;border-radius:9px;background:${ACCENT};color:#fff;font-size:11px;font-style:normal}
@@ -1811,13 +2348,17 @@ window.__ModuleLoader__.load({
1811
2348
  .ddp-work .bdg{flex:none;height:17px;padding:0 6px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:10.5px;line-height:17px}
1812
2349
  .ddp-work .pin{flex:none;color:#f5a524;font-size:11px}
1813
2350
  .ddp-work .mt{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1814
- .ddp-work .acts{display:flex;align-items:center;gap:2px;min-width:0;margin-left:-4px}
2351
+ .ddp-work .acts{display:flex;flex-wrap:wrap;align-items:center;gap:2px;min-width:0;margin-left:-4px}
2352
+ /* 卡片窄(侧栏窄、多列):操作只留图标(悬停有说明),别挤进旁边那件 */
2353
+ @container (max-width:259px){.ddp-work .acts button svg+span{display:none}}
1815
2354
  .ddp-work .acts button,.ddp-insp .rm,.ddp-market .rm{display:inline-flex;align-items:center;justify-content:center;flex:none;height:24px;min-width:24px;padding:0 6px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap}
1816
2355
  .ddp-work .acts button:hover:not(:disabled),.ddp-insp .rm:hover,.ddp-market button.rm:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1817
2356
  .ddp-work .acts button:disabled{opacity:.45;cursor:default}
1818
2357
  .ddp-work .acts button.on{color:#f5a524}
1819
2358
  .ddp-work .acts svg,.ddp-insp .rm svg,.ddp-market .rm svg{width:14px;height:14px}
1820
2359
  .ddp-work .acts .rm{margin-left:auto}
2360
+ .ddp-work .acts button span{font-size:11px}
2361
+ .ddp-work .acts button svg+span{margin-left:3px}
1821
2362
  .ddp-work .acts .rm.sure,.ddp-insp .rm.sure{color:var(--dsw-alias-state-error-primary,#d54941);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d54941) 12%,transparent);font-size:11px}
1822
2363
  .ddp-insp h4{margin:18px 0 8px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
1823
2364
  .ddp-insp h4:first-child{margin-top:0}
@@ -1835,11 +2376,23 @@ window.__ModuleLoader__.load({
1835
2376
  .ddp-imgs{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px}
1836
2377
  .ddp-imgs:empty{display:none}
1837
2378
  .ddp-market .ddp-mkbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
1838
- .ddp-market .ddp-mkbar .seg{margin-left:0}
1839
- .ddp-market .ddp-mkbar .q{flex:1 1 140px;min-width:0;height:28px;padding:0 10px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;outline:none}
2379
+ .ddp-market .ddp-mkbar .tabs{display:flex;gap:14px;flex:none}
2380
+ .ddp-market .ddp-mkbar .tabs button{display:flex;align-items:baseline;gap:4px;height:28px;padding:0;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);font:13px inherit;cursor:pointer;white-space:nowrap}
2381
+ .ddp-market .ddp-mkbar .tabs button em{font-style:normal;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2382
+ .ddp-market .ddp-mkbar .tabs button:hover{color:var(--dsw-alias-label-primary)}
2383
+ .ddp-market .ddp-mkbar .tabs button.on{color:var(--dsw-alias-label-primary);font-weight:500;border-bottom-color:${ACCENT}}
2384
+ .ddp-market .ddp-mkbar .q{flex:1 1 140px;min-width:0;height:28px;padding:0 10px 0 28px;background-image:${iconUrl('M11 4a7 7 0 1 0 0 14a7 7 0 1 0 0-14 M20 20l-4.2-4.2').replace('black', '%23999')};background-repeat:no-repeat;background-size:14px;background-position:8px center;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background-color:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;outline:none}
1840
2385
  .ddp-market .ddp-mkbar .q:focus{border-color:${ACCENT}}
1841
- .ddp-market .ddp-mkbar select{height:28px;padding:0 6px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit}
1842
- .ddp-mkkinds{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}
2386
+ /* 窄了:搜索框缩成一个放大镜,点进去再展开 */
2387
+ @container (max-width:480px){.ddp-market .ddp-mkbar .q{flex:0 0 32px;padding-right:0;cursor:pointer;transition:flex-basis .15s ${EASE}}.ddp-market .ddp-mkbar .q::placeholder{color:transparent}.ddp-market .ddp-mkbar .q:focus,.ddp-market .ddp-mkbar .q:not(:placeholder-shown){flex:1 1 140px;cursor:text}.ddp-market .ddp-mkbar .q:focus::placeholder{color:var(--dsw-alias-label-tertiary)}}
2388
+ .ddp-mkkinds{display:flex;align-items:center;gap:6px;margin:0 0 12px;overflow-x:auto;scrollbar-width:none}
2389
+ .ddp-mkkinds::-webkit-scrollbar{display:none}
2390
+ .ddp-mkkinds>button{flex:none}
2391
+ .ddp-mkkinds button:disabled{opacity:.4;cursor:default}
2392
+ .ddp-mkbar .sort{display:flex;gap:2px;flex:none}
2393
+ .ddp-mkbar .sort button{height:24px;padding:0 8px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px inherit;cursor:pointer;white-space:nowrap}
2394
+ .ddp-mkbar .sort button:hover{color:var(--dsw-alias-label-primary)}
2395
+ .ddp-mkbar .sort button.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1843
2396
  .ddp-mkkinds button,.ddp-mkdots button{height:24px;padding:0 10px;border:0;border-radius:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:12px inherit;cursor:pointer;white-space:nowrap}
1844
2397
  .ddp-mkkinds button:hover,.ddp-mkdots button:hover{color:var(--dsw-alias-label-primary)}
1845
2398
  .ddp-mkkinds button.on,.ddp-mkdots button.on{background:${ACCENT};color:#fff}
@@ -1859,25 +2412,14 @@ window.__ModuleLoader__.load({
1859
2412
  .ddp-mkdots{display:flex;flex-wrap:wrap;gap:6px}
1860
2413
  .ddp-mkd .acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
1861
2414
  .ddp-pbtn,.ddp-sbtn{height:32px;padding:0 16px;border:0;border-radius:8px;font:13px inherit;cursor:pointer;white-space:nowrap}
1862
- .ddp-pbtn{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font-weight:600}
2415
+ .ddp-pbtn{background:${ACCENT};color:#fff;font-weight:600}
2416
+ .ddp-pbtn:hover:not(:disabled){filter:brightness(.94)}
1863
2417
  .ddp-sbtn{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1864
2418
  .ddp-pbtn:disabled,.ddp-sbtn:disabled{opacity:.5;cursor:default}
1865
2419
  .ddp-mkd .facts{margin:6px 0 0;padding-left:18px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
1866
2420
  .ddp-mkd .ddp-libfoot a{color:${ACCENT};text-decoration:none}
1867
2421
  .ddp-mkd .ddp-libfoot a:hover{text-decoration:underline}
1868
2422
  .ddp-mkd code{font-size:11px;color:var(--dsw-alias-label-tertiary)}
1869
- .ddp-tplstart{width:100%;max-width:560px;margin:18px auto 0;text-align:left}
1870
- .ddp-tplstart .hd{display:flex;align-items:baseline;gap:8px;margin-bottom:8px}
1871
- .ddp-tplstart .hd b{font-size:13px;font-weight:600}
1872
- .ddp-tplstart .hd .lk{margin-left:auto;border:0;padding:0;background:transparent;color:${ACCENT};font:12px inherit;cursor:pointer}
1873
- .ddp-tplstart .row{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:10px}
1874
- .ddp-tplstart .tp{display:flex;flex-direction:column;gap:4px;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:11px inherit;text-align:left;cursor:pointer;min-width:0}
1875
- .ddp-tplstart .im{position:relative;display:flex;align-items:center;justify-content:center;aspect-ratio:3/4;border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);box-shadow:0 0 0 .5px rgba(127,127,127,.35);color:var(--dsw-alias-label-tertiary);transition:transform .15s ${EASE},box-shadow .15s}
1876
- .ddp-tplstart .tp:hover .im,.ddp-tplstart .tp:focus-visible .im{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT}}
1877
- .ddp-tplstart .tp:focus-visible{outline:none}
1878
- .ddp-tplstart .im img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top}
1879
- .ddp-tplstart .im svg{width:20px;height:20px}
1880
- .ddp-tplstart .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1881
2423
  .ddp-img{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
1882
2424
  .ddp-img img{display:block;width:100%;height:100%;object-fit:cover}
1883
2425
  .ddp-img .acts{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:4px;padding:6px;background:linear-gradient(transparent,rgba(0,0,0,.55));opacity:0;transition:opacity .15s}
@@ -1896,8 +2438,8 @@ window.__ModuleLoader__.load({
1896
2438
  .ddp-urlform{display:flex;gap:6px}
1897
2439
  .ddp-urlform input{flex:1;min-width:0;box-sizing:border-box;height:28px;padding:0 9px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;outline:none}
1898
2440
  .ddp-urlform input:focus{border-color:${ACCENT}}
1899
- .ddp-urlform button{flex:none;height:28px;padding:0 12px;border:0;border-radius:8px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font:12px inherit;cursor:pointer}
1900
- .ddp-urlform button:disabled{opacity:.4;cursor:default}
2441
+ .ddp-urlform button{flex:none;height:28px;padding:0 12px;border:0;border-radius:8px;background:${ACCENT};color:#fff;font:12px inherit;cursor:pointer}
2442
+ .ddp-urlform button:disabled{background:transparent;color:var(--dsw-alias-label-tertiary);box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.3));cursor:default}
1901
2443
  .ddp-pack .which{display:flex;align-items:center;gap:6px;margin:4px 6px 2px;font-size:12px;color:var(--dsw-alias-label-primary);min-width:0}
1902
2444
  .ddp-pack .which svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-tertiary)}
1903
2445
  .ddp-pack .which span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1905,6 +2447,8 @@ window.__ModuleLoader__.load({
1905
2447
  .ddp-pack .lic label{display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:8px;color:var(--dsw-alias-label-secondary);line-height:1.5;cursor:pointer}
1906
2448
  .ddp-pack .lic label.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1907
2449
  .ddp-pack .lic input{flex:none;margin:3px 0 0;accent-color:${ACCENT}}
2450
+ .ddp-share .tip.gh{display:flex;align-items:center;gap:6px;margin-bottom:0;color:var(--dsw-alias-label-primary);font-weight:500}
2451
+ .ddp-share .tip.gh svg{width:14px;height:14px;flex:none;color:${ACCENT}}
1908
2452
  .ddp-share .agree{display:flex;flex-direction:column;gap:2px;margin-top:10px}
1909
2453
  .ddp-share .agree label{display:flex;align-items:flex-start;gap:8px;padding:5px 8px;border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;cursor:pointer}
1910
2454
  .ddp-share .agree label:hover{color:var(--dsw-alias-label-primary)}
@@ -1925,14 +2469,13 @@ window.__ModuleLoader__.load({
1925
2469
  .ddp-pack .note ul{margin:2px 0 0;padding-left:16px}
1926
2470
  .ddp-pack footer.row{display:flex;gap:6px}
1927
2471
  .ddp-pack footer.row button{flex:1;justify-content:center;padding:0 6px}
1928
- .ddp-pack footer .go{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
1929
- .ddp-pack footer .go:hover{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);opacity:.88}
1930
- .ddp-pack footer .go:disabled{opacity:.45;cursor:default}
2472
+ .ddp-pack footer .go{background:${ACCENT};color:#fff}
2473
+ .ddp-pack footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
2474
+
1931
2475
  .ddp-libwrap>.ddp-toast{z-index:3}
1932
- /* 「更多」一直在:设计库、设计系统只放在这个菜单里;窄的时候再把 o1/o2 收进来 */
1933
- .ddp-menu .o1,.ddp-menu .o2{display:none}
1934
- @container (max-width:700px){.ddp-bar>.o1{display:none}.ddp-bar>.ddp-more{display:block}.ddp-menu .o1{display:flex}}
1935
- @container (max-width:580px){.ddp-bar>.o2{display:none}.ddp-menu .o2{display:flex}}
2476
+ /* 浮块窄了:先截屏名,到 440 再收按钮上的字,400 收工具坞里的演示(演示时横幅上能退出) */
2477
+ @container (max-width:440px){.ddp-isl .lab .lb,.ddp-isl .pri .lb{display:none}.ddp-isl .ddp-btn.lab{padding:0 6px}.ddp-tool{width:36px;height:36px}.ddp-tool span{display:none}.ddp-dock .ddp-sep{height:20px}}
2478
+ @container (max-width:380px){.ddp-tool{width:32px}.ddp-kind.ddp-where{min-width:64px}}
1936
2479
  /* 标注底栏:右侧栏窄时「这支笔怎么用」折成两行,不被三个按钮挤成省略号 */
1937
2480
  @container (max-width:640px){.ddp-mk-bar{border-radius:18px}.ddp-mk-bar span{white-space:normal;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}}
1938
2481
  /* 画标注时预览不动,画布右下角的缩放按钮被标注层盖着也点不到:先收起来,免得和底栏叠在一起 */
@@ -2006,9 +2549,11 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2006
2549
  more: () => svg([h('circle', { key: 1, cx: 5.5, cy: 12, r: 1.3 }), h('circle', { key: 2, cx: 12, cy: 12, r: 1.3 }), h('circle', { key: 3, cx: 18.5, cy: 12, r: 1.3 })]),
2007
2550
  play: () => svg([h('path', { key: 1, d: 'M8 5.5v13l10.5-6.5z', strokeLinejoin: 'round' })]),
2008
2551
  back: () => svg([h('path', { key: 1, d: 'M14.5 6l-6 6 6 6', strokeWidth: 2.2 })]),
2552
+ home: () => svg([h('path', { key: 1, d: 'M4.5 10.2 12 4.2l7.5 6V19a1 1 0 0 1-1 1h-4v-5.5h-5V20h-4a1 1 0 0 1-1-1z' })]),
2009
2553
  moon: () => svg([h('path', { key: 1, d: 'M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z' })]),
2010
2554
  sparkle: () => svg([h('path', { key: 1, d: 'M12 3.5l1.8 5.2 5.2 1.8-5.2 1.8L12 17.5l-1.8-5.2L5 10.5l5.2-1.8z' }), h('path', { key: 2, d: 'M18.5 16v4M16.5 18h4' })]),
2011
2555
  plus: () => svg([h('path', { key: 1, d: 'M12 5v14M5 12h14' })]),
2556
+ send: () => svg([h('path', { key: 1, d: 'M12 19V5.5M6 11.5l6-6 6 6', strokeWidth: 2.2 })]),
2012
2557
  minus: () => svg([h('path', { key: 1, d: 'M5 12h14' })]),
2013
2558
  fit: () => svg([h('path', { key: 1, d: 'M4 9V5.5A1.5 1.5 0 0 1 5.5 4H9M15 4h3.5A1.5 1.5 0 0 1 20 5.5V9M20 15v3.5a1.5 1.5 0 0 1-1.5 1.5H15M9 20H5.5A1.5 1.5 0 0 1 4 18.5V15' })]),
2014
2559
  refresh: () => svg([h('path', { key: 1, d: 'M20 11a8 8 0 1 0-2.3 5.7' }), h('path', { key: 2, d: 'M20 5v6h-6' })]),
@@ -2030,6 +2575,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2030
2575
  redo: () => svg([h('path', { key: 1, d: 'M15 14l4.5-4.5L15 5' }), h('path', { key: 2, d: 'M19.5 9.5H9a5 5 0 0 0 0 10h3' })]),
2031
2576
  trash: () => svg([h('path', { key: 1, d: 'M4.5 7h15M9.5 7V4.5h5V7' }), h('path', { key: 2, d: 'M6.5 7l.9 12a1.5 1.5 0 0 0 1.5 1.4h6.2a1.5 1.5 0 0 0 1.5-1.4l.9-12' }), h('path', { key: 3, d: 'M10.5 11v5.5M13.5 11v5.5' })]),
2032
2577
  folder: () => svg([h('path', { key: 1, d: 'M3.5 7.5a2 2 0 0 1 2-2h3.8l2 2.2h7.2a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z' })]),
2578
+ info: () => svg([h('circle', { key: 1, cx: 12, cy: 12, r: 8.5 }), h('path', { key: 2, d: 'M12 11v5.5M12 7.8v.2', strokeWidth: 2 })]),
2033
2579
  box: () => svg([h('path', { key: 1, d: 'M3.5 7.5L12 3.5l8.5 4v9L12 20.5l-8.5-4z' }), h('path', { key: 2, d: 'M3.5 7.5L12 11.5l8.5-4M12 11.5v9' })]),
2034
2580
  share: () => svg([h('path', { key: 1, d: 'M12 15V4' }), h('path', { key: 2, d: 'M7.5 8.5L12 4l4.5 4.5' }), h('path', { key: 3, d: 'M5 13v5.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V13' })]),
2035
2581
  page: (p) => svg([h('rect', { key: 1, x: 3, y: 4, width: 18, height: 16, rx: 2.5 }), h('path', { key: 2, d: 'M3 9h18' }), h('rect', { key: 3, x: 7, y: 12.5, width: 5, height: 4, rx: 1, strokeDasharray: '2 1.6' })], p),
@@ -2612,6 +3158,106 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2612
3158
  }
2613
3159
 
2614
3160
  /** 点了一个变量 → 输入框里的一段字 */
3161
+ /** 设计变量的人话名字:--bg → 背景,--fs-micro → 注释,--radius-md → 中圆角;认不出的把名字去掉 -- 和连字符 */
3162
+ function tokenLabel(t, tok) {
3163
+ const raw = String(tok.name).replace(/^--/, '')
3164
+ const words = raw.toLowerCase().split(/[-_]+/).filter(Boolean)
3165
+ const has = (re) => words.some((w) => re.test(w))
3166
+ const kind = tokenKind(tok)
3167
+ const rest = (re) => words.filter((w) => !re.test(w) && !/^(color|clr|c|token|tk|font|fs|text|size|radius|r|rounded|space|spacing|sp|gap|shadow|elevation|family|ff)$/.test(w))
3168
+ const mod = (left) => {
3169
+ const out = []
3170
+ for (const w of left) {
3171
+ if (/^(soft|light|lighter|pale|tint|weak|subtle|faint|bg|50|100|200)$/.test(w)) out.push(t('tkn_light'))
3172
+ else if (/^(dark|darker|deep|strong|bold|700|800|900)$/.test(w)) out.push(t('tkn_dark'))
3173
+ else if (/^(hover|active|pressed|focus)$/.test(w)) out.push(t('tkn_hover'))
3174
+ else out.push(w)
3175
+ }
3176
+ return [...new Set(out)].join(' ')
3177
+ }
3178
+ const name = (key, re) => { const m = mod(rest(re)); return m ? `${t('tkn_' + key)} · ${m}` : t('tkn_' + key) }
3179
+ if (kind === 'color') {
3180
+ if (words.includes('on') && words.length > 1) return t('tkn_onPrimary')
3181
+ const roles = [
3182
+ [/^(muted|subtle|dim|faint|placeholder|secondary-text|text2|hint)$/, 'muted'],
3183
+ [/^(line|border|divider|stroke|outline|hairline|sep|separator|rule)$/, 'line'],
3184
+ [/^(surface|card|panel|elev|elevated|sheet|raised|layer|paper)$/, 'surface'],
3185
+ [/^(text|fg|ink|foreground|body|copy)$/, 'text'],
3186
+ [/^(bg|background|page|canvas|base|backdrop)$/, 'bg'],
3187
+ [/^(primary|brand|main|theme)$/, 'primary'],
3188
+ [/^(accent|highlight|secondary)$/, 'accent'],
3189
+ [/^(link|links)$/, 'link'],
3190
+ [/^(danger|error|destructive|alert)$/, 'danger'],
3191
+ [/^(success|positive|ok)$/, 'success'],
3192
+ [/^(warning|warn|caution)$/, 'warning'],
3193
+ ]
3194
+ for (const [re, key] of roles) if (has(re)) return name(key, re)
3195
+ } else if (kind === 'size') {
3196
+ const sizes = [[/^(xs|xxs|micro|tiny|caption|note|footnote|mini)$/, 'xs'], [/^(sm|small)$/, 'sm'], [/^(md|medium)$/, 'md'], [/^(base|body|normal|p|regular)$/, 'body'], [/^(lg|large|lead|sub|subtitle)$/, 'lg'], [/^(xl|title|h2|h3|h4|heading|head)$/, 'title'], [/^(2xl|3xl|4xl|xxl|display|hero|h1|huge|jumbo)$/, 'display']]
3197
+ for (const [re, key] of sizes) if (has(re)) return name(key, re)
3198
+ } else if (kind === 'radius') {
3199
+ const rs = [[/^(xs|sm|small)$/, 'rSm'], [/^(md|base|medium|default)$/, 'rMd'], [/^(lg|xl|large|2xl)$/, 'rLg'], [/^(full|pill|round|circle|999)$/, 'rFull']]
3200
+ for (const [re, key] of rs) if (has(re)) return name(key, re)
3201
+ const m = mod(rest(/^$/))
3202
+ return m ? `${t('tkn_r')} ${m}` : t('tkn_r')
3203
+ } else if (kind === 'space') {
3204
+ if (has(/^(gutter|page|edge|inset)$/)) return t('tkn_gutter')
3205
+ const m = rest(/^$/).join(' ')
3206
+ return m ? `${t('tkn_space')} ${m}` : t('tkn_space')
3207
+ } else if (kind === 'font') {
3208
+ if (has(/^(head|heading|display|title|serif|brand)$/)) return t('tkn_fHead')
3209
+ if (has(/^(mono|code)$/)) return t('tkn_fMono')
3210
+ if (has(/^(body|text|sans|base|ui)$/)) return t('tkn_fBody')
3211
+ } else if (kind === 'shadow') {
3212
+ const m = rest(/^$/).join(' ')
3213
+ return m ? `${t('tkn_shadow')} ${m}` : t('tkn_shadow')
3214
+ }
3215
+ return raw.replace(/[-_]+/g, ' ')
3216
+ }
3217
+
3218
+ /** 一套变量的人话名字(名字 → 名字):撞名的后面带上原名的尾巴,比如「卡片底(paper)」 */
3219
+ function tokenLabels(t, tokens) {
3220
+ const out = new Map()
3221
+ const seen = new Map()
3222
+ for (const tok of tokens || []) {
3223
+ const l = tokenLabel(t, tok)
3224
+ out.set(tok.name, l)
3225
+ seen.set(l, (seen.get(l) || 0) + 1)
3226
+ }
3227
+ for (const tok of tokens || []) {
3228
+ const l = out.get(tok.name)
3229
+ if (seen.get(l) > 1) out.set(tok.name, `${l}(${String(tok.name).replace(/^--/, '').replace(/^(color|clr|c|font|fs|text|size|radius|space|spacing|shadow)-/i, '')})`)
3230
+ }
3231
+ return out
3232
+ }
3233
+
3234
+ /** 调样式面板头上:这是个什么(按钮「去预约」),不是一串选择器 */
3235
+ function elemLabel(t, name, text) {
3236
+ const n = String(name || '').toLowerCase()
3237
+ const tag = (/^[a-z][a-z0-9]*/.exec(n) || [''])[0]
3238
+ const cls = n.slice(tag.length)
3239
+ let k = 'box'
3240
+ if (/btn|button/.test(cls) || tag === 'button') k = 'button'
3241
+ else if (/card/.test(cls)) k = 'card'
3242
+ else if (/tag|chip|badge|pill/.test(cls)) k = 'tag'
3243
+ else if (/avatar/.test(cls)) k = 'avatar'
3244
+ else if (/icon|ico\b/.test(cls) || tag === 'svg' || tag === 'i') k = 'icon'
3245
+ else if (tag === 'a') k = 'a'
3246
+ else if (tag === 'img' || tag === 'picture' || tag === 'video') k = 'img'
3247
+ else if (/^h[1-6]$/.test(tag)) k = 'h'
3248
+ else if (tag === 'p') k = 'p'
3249
+ else if (tag === 'input' || tag === 'textarea' || tag === 'select') k = 'input'
3250
+ else if (tag === 'li') k = 'li'
3251
+ else if (tag === 'ul' || tag === 'ol') k = 'ul'
3252
+ else if (tag === 'nav' || /nav|tabbar|tab-bar/.test(cls)) k = 'nav'
3253
+ else if (tag === 'header') k = 'header'
3254
+ else if (tag === 'footer') k = 'footer'
3255
+ else if (/^(span|b|strong|em|small|label|time)$/.test(tag)) k = 'text'
3256
+ const s = String(text || '').replace(/\s+/g, ' ').trim()
3257
+ const short = [...s].length > 14 ? [...s].slice(0, 13).join('') + '…' : s
3258
+ return { role: t('el_' + k), text: short }
3259
+ }
3260
+
2615
3261
  function formatToken(tok, st) {
2616
3262
  return `〔设计变量〕${tok.name}: ${String(tok.value).replace(/\s*\n\s*/g, ' ')}(${whereOf(tok.href, st)})\n`
2617
3263
  }
@@ -3569,6 +4215,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
3569
4215
  const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'desktop', 'poster', 'slides', 'email']
3570
4216
  const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', desktop: 'kindDesktop', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
3571
4217
  const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
4218
+ const HOLD_MAX = 15000 // 草稿放到一半真文件就落地了:最多再放这么久(一般几秒放完自己说一声)
3572
4219
  const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
3573
4220
  const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], desktop: [1280, 800], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
3574
4221
  // 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule 小程序右上角的胶囊、win 桌面软件的窗口按钮
@@ -3662,7 +4309,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
3662
4309
 
3663
4310
  /** 整块居中放进舞台(最大 100%) */
3664
4311
  function fitCam(L, sw, sh) {
3665
- const z = clampZ(Math.min(1, L.z))
4312
+ // 排的时候按整块舞台算的缩放;右边被面板挡着时 sw 变窄,再缩到放得下
4313
+ const z = clampZ(Math.min(1, L.z, (sw - PAD * 2) / Math.max(1, L.bw), (sh - PAD_TOP - PAD) / Math.max(1, L.bh)))
3666
4314
  return { z, x: (sw - L.bw * z) / 2, y: PAD_TOP + Math.max(0, (sh - PAD_TOP - PAD - L.bh * z) / 2) }
3667
4315
  }
3668
4316
 
@@ -4003,6 +4651,226 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4003
4651
 
4004
4652
  // ── 小部件 ───────────────────────────────────────────────────────────────
4005
4653
 
4654
+ // 模型在做什么:按它调的工具说人话(写文件另算,见 busyPhase)
4655
+ const TOOL_PHASE = [
4656
+ [/^check_design$/, 'stCheck'], [/^look_design$/, 'stLook'], [/^preview_page$/, 'stPreview'], [/^extract_design_system$/, 'stBrand'],
4657
+ [/^handoff_design$/, 'stCode'], [/^compare_design$/, 'stCompare'], [/^ask_user_question$/, 'asking'],
4658
+ [/^(bash|shell|sh|zsh|powershell|pwsh|exec|run_command|terminal)$/i, 'stShell'],
4659
+ [/^skill$/i, 'stSkill'],
4660
+ [/web|fetch|browse|http/i, 'stWeb'],
4661
+ [/^(write|edit|multi_?edit|create_file|str_replace\w*|apply_patch|patch)$/i, 'stWriteAny'],
4662
+ [/read|grep|glob|list|^ls$|find|search|view/i, 'stRead'],
4663
+ ]
4664
+ const clock = (ms) => {
4665
+ const s = Math.max(0, Math.floor(ms / 1000))
4666
+ const p2 = (n) => String(n).padStart(2, '0')
4667
+ return s >= 3600 ? `${Math.floor(s / 3600)}:${p2(Math.floor(s / 60) % 60)}:${p2(s % 60)}` : `${Math.floor(s / 60)}:${p2(s % 60)}`
4668
+ }
4669
+
4670
+ /** 顶上的状态条:在做什么 · 第几屏 · 用了多久(自己每秒走一下,不带着整个右侧栏重画) */
4671
+ function BusyBar({ t, phase, since, side }) {
4672
+ const [, tick] = useState(0)
4673
+ useEffect(() => {
4674
+ const id = setInterval(() => tick((n) => n + 1), 1000)
4675
+ return () => clearInterval(id)
4676
+ }, [])
4677
+ const tm = clock(Date.now() - since)
4678
+ return h('div', { className: 'ddp-busy' + (phase.asking ? ' ask' : '') + (side ? ' side' : '') },
4679
+ phase.asking ? h('i', { className: 'q', 'aria-hidden': true }) : h('span', { className: 'dots', 'aria-hidden': true }, h('i'), h('i'), h('i')),
4680
+ h('span', { className: 'tx', role: 'status' }, phase.text),
4681
+ phase.sub ? h('span', { className: 'sub' }, phase.sub) : null,
4682
+ h('span', { className: 'tm', title: t('stTime', { time: tm }) }, tm),
4683
+ phase.asking ? null : h('i', { className: 'bar', 'aria-hidden': true }))
4684
+ }
4685
+
4686
+ // 生成中的占位:三只带名字的鼠标轮流框出版面(按类型给个大概的样子),框里闪骨架,一遍遍画。位置都是百分比,放哪都行
4687
+ const GD_PLAN = {
4688
+ app: [[0, 0, 100, 11], [5, 14, 90, 24], [5, 42, 44, 4], [5, 49, 90, 11], [5, 63, 90, 11], [5, 77, 90, 9], [0, 90, 100, 10]],
4689
+ web: [[0, 0, 100, 9], [6, 15, 52, 8], [6, 26, 38, 4], [6, 35, 88, 29], [6, 70, 27, 23], [36.5, 70, 27, 23], [67, 70, 27, 23]],
4690
+ poster: [[8, 6, 84, 50], [8, 61, 70, 8], [8, 72, 50, 4], [8, 84, 36, 7]],
4691
+ slides: [[6, 10, 60, 13], [6, 32, 40, 52], [52, 32, 42, 52]],
4692
+ email: [[10, 4, 80, 7], [10, 14, 80, 30], [10, 48, 60, 5], [10, 56, 80, 14], [10, 75, 30, 6]],
4693
+ }
4694
+ const GD_C = ['#4d6bfe', '#e8590c', '#0ca678']
4695
+ /** 占位里的鼠标什么时候动:换了在做的事、或者又写了 15 行就加一;不在生成就不动 */
4696
+ function pulseStep(pr, busy, key, ln) {
4697
+ if (!busy) {
4698
+ pr.key = ''
4699
+ pr.lines = 0
4700
+ return pr.n
4701
+ }
4702
+ if (ln < pr.lines) pr.lines = ln
4703
+ if (key !== pr.key) {
4704
+ pr.key = key
4705
+ pr.lines = ln
4706
+ pr.n++
4707
+ } else if (ln - pr.lines >= 15) {
4708
+ pr.lines = ln
4709
+ pr.n++
4710
+ }
4711
+ return pr.n
4712
+ }
4713
+ const planOf = (kind) => GD_PLAN[kind === 'miniprogram' ? 'app' : kind === 'tablet' || kind === 'desktop' ? 'web' : kind] || GD_PLAN.web
4714
+ const reduced = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches } catch { return false } }
4715
+
4716
+ /**
4717
+ * 生成中的占位。三只带名字的鼠标先把版面框一遍,框完就停住——不循环;之后模型每真有一点动静
4718
+ * (换了在做的事、又写了一截),才有一只去重新框一块。模型停着,它们也停着。
4719
+ */
4720
+ function GhostDraw({ t, kind, pulse = 0 }) {
4721
+ const ref = useRef(null)
4722
+ const plan = planOf(kind)
4723
+ const S = useRef(null)
4724
+ const EASE_K = 'cubic-bezier(.3,.7,.2,1)'
4725
+ useEffect(() => {
4726
+ const el = ref.current
4727
+ if (!el) return
4728
+ const boxes = [...el.querySelectorAll('.b')]
4729
+ const curs = [...el.querySelectorAll('.c')]
4730
+ const s = { boxes, curs, pos: curs.map((_, i) => ({ x: 30 + i * 20, y: 112 })), cur: [], box: [], last: -1, seen: pulse, till: 0, still: typeof el.animate !== 'function' || reduced() }
4731
+ S.current = s
4732
+ if (s.still) return
4733
+ const STEP = 520
4734
+ const MOVE = 320
4735
+ const DRAG = 420
4736
+ const paths = curs.map((_, i) => [{ ms: 0, x: s.pos[i].x, y: s.pos[i].y, o: 0 }])
4737
+ plan.forEach(([x, y, w, hh], i) => {
4738
+ const s0 = 250 + i * STEP
4739
+ const path = paths[i % curs.length]
4740
+ const last = path[path.length - 1]
4741
+ path.push({ ms: s0, x: last.x, y: last.y, o: 1 }, { ms: s0 + MOVE, x, y, o: 1 }, { ms: s0 + MOVE + DRAG, x: Math.min(97, x + w), y: Math.min(97, y + hh), o: 1 })
4742
+ const box = boxes[i]
4743
+ if (box) {
4744
+ s.box[i] = box.animate([
4745
+ { opacity: 0, clipPath: 'inset(0 100% 100% 0)' },
4746
+ { offset: 0.001, opacity: 1, clipPath: 'inset(0 100% 100% 0)', easing: EASE_K },
4747
+ { opacity: 1, clipPath: 'inset(0 0 0 0)' },
4748
+ ], { duration: DRAG, delay: s0 + MOVE, fill: 'both' })
4749
+ }
4750
+ })
4751
+ let end = 0
4752
+ curs.forEach((c, i) => {
4753
+ const path = paths[i]
4754
+ const D = path[path.length - 1].ms || 1
4755
+ end = Math.max(end, D)
4756
+ const kf = []
4757
+ for (const p of path) {
4758
+ const o = p.ms / D
4759
+ if (kf.length && o <= kf[kf.length - 1].offset) continue
4760
+ kf.push({ offset: o, left: p.x + '%', top: p.y + '%', opacity: p.o, easing: EASE_K })
4761
+ }
4762
+ if (kf[kf.length - 1].offset < 1) kf.push({ ...kf[kf.length - 1], offset: 1 })
4763
+ const last = path[path.length - 1]
4764
+ s.pos[i] = { x: last.x, y: last.y }
4765
+ c.classList.toggle('flip', last.x > 78)
4766
+ s.cur[i] = c.animate(kf, { duration: D, fill: 'forwards' })
4767
+ })
4768
+ s.till = performance.now() + end
4769
+ return () => { s.cur.forEach((a) => a && a.cancel()); s.box.forEach((a) => a && a.cancel()) }
4770
+ }, [plan])
4771
+ // 模型真有动静:轮到的那只去重新框一块(框过的那块先收起再跟着鼠标拉开)
4772
+ useEffect(() => {
4773
+ const s = S.current
4774
+ if (!s || s.still || pulse === s.seen) return
4775
+ s.seen = pulse
4776
+ const now = performance.now()
4777
+ if (now < s.till) return // 头一遍还没框完 / 上一下还没做完:这一下不插队
4778
+ const k = pulse % s.curs.length
4779
+ let j = Math.floor(Math.random() * plan.length)
4780
+ if (j === s.last) j = (j + 1) % plan.length
4781
+ s.last = j
4782
+ const [x, y, w, hh] = plan[j]
4783
+ const MOVE = 420
4784
+ const DRAG = 480
4785
+ const from = s.pos[k]
4786
+ const to = { x: Math.min(97, x + w), y: Math.min(97, y + hh) }
4787
+ const c = s.curs[k]
4788
+ const prev = s.cur[k]
4789
+ s.cur[k] = c.animate([
4790
+ { left: from.x + '%', top: from.y + '%', opacity: 1, easing: EASE_K },
4791
+ { offset: MOVE / (MOVE + DRAG), left: x + '%', top: y + '%', opacity: 1, easing: EASE_K },
4792
+ { left: to.x + '%', top: to.y + '%', opacity: 1 },
4793
+ ], { duration: MOVE + DRAG, fill: 'forwards' })
4794
+ if (prev) prev.cancel()
4795
+ s.pos[k] = to
4796
+ c.classList.toggle('flip', to.x > 78)
4797
+ const box = s.boxes[j]
4798
+ if (box) {
4799
+ const pb = s.box[j]
4800
+ s.box[j] = box.animate([
4801
+ { opacity: 1, clipPath: 'inset(0 100% 100% 0)', easing: EASE_K },
4802
+ { opacity: 1, clipPath: 'inset(0 0 0 0)' },
4803
+ ], { duration: DRAG, delay: MOVE, fill: 'both' })
4804
+ if (pb) pb.cancel()
4805
+ }
4806
+ s.till = now + MOVE + DRAG
4807
+ }, [pulse])
4808
+ const names = [t('curLayout'), t('curColor'), t('curCopy')]
4809
+ return h('div', { className: 'ddp-gd', ref, 'aria-hidden': true },
4810
+ ...plan.map(([x, y, w, hh], i) => h('i', { key: 'b' + i, className: 'b', style: { left: x + '%', top: y + '%', width: w + '%', height: hh + '%', '--c': GD_C[i % 3] } })),
4811
+ ...GD_C.map((c, i) => h('span', { key: 'c' + i, className: 'c', style: { '--c': c, left: (30 + i * 20) + '%', top: '112%' } },
4812
+ h('svg', { viewBox: '0 0 17 21' }, h('path', { d: 'M1.2 1.2v15.6l4.3-4 3 6.6 2.9-1.3-3-6.5h6z' })),
4813
+ h('em', null, names[i]))))
4814
+ }
4815
+
4816
+ /**
4817
+ * 边写边看:预览服务器上正在写的那一页(/某屏.html?__ddp_draft=…),浏览器边收边画。
4818
+ * 页面里的小脚本报到后才显出来(之前看到的是底下的占位),画布缩放告诉它,鼠标和字看起来一样大。
4819
+ */
4820
+ function DraftFrame({ url, z = 1, lang, onEnd }) {
4821
+ const ref = useRef(null)
4822
+ const [live, setLive] = useState(false)
4823
+ const zr = useRef(z)
4824
+ zr.current = z
4825
+ const endR = useRef(onEnd)
4826
+ endR.current = onEnd
4827
+ const src = url + (url.includes('?') ? '&' : '?') + 'lang=' + (lang === 'en' ? 'en' : 'zh')
4828
+ let origin = '*'
4829
+ try { origin = new URL(url).origin } catch { /* 地址不对就不发 */ }
4830
+ const tell = (zz) => {
4831
+ const f = ref.current
4832
+ try { if (f && f.contentWindow) f.contentWindow.postMessage({ dshDraft: 'zoom', z: zz }, origin) } catch { /* 页面已经换了 */ }
4833
+ }
4834
+ useEffect(() => {
4835
+ setLive(false)
4836
+ const end = () => { if (endR.current) endR.current(url) }
4837
+ // 一直没报到(草稿已经收走、给的是真文件):别干等,算放完了
4838
+ let quiet = setTimeout(end, 6000)
4839
+ const on = (e) => {
4840
+ const f = ref.current
4841
+ if (!f || e.source !== f.contentWindow || !e.data) return
4842
+ if (e.data.dshDraft === 'end') return end() // 写完、鼠标也退场了:可以换真文件了
4843
+ if (e.data.dshDraft !== 'hello') return
4844
+ clearTimeout(quiet)
4845
+ quiet = null
4846
+ setLive(true)
4847
+ tell(zr.current)
4848
+ }
4849
+ addEventListener('message', on)
4850
+ return () => { clearTimeout(quiet); removeEventListener('message', on) }
4851
+ }, [src])
4852
+ useEffect(() => { if (live) tell(z) }, [z, live])
4853
+ return h('iframe', { ref, key: src, src, className: 'ddp-draft' + (live ? ' on' : ''), title: 'draft', sandbox: 'allow-scripts allow-same-origin', tabIndex: -1, 'aria-hidden': true })
4854
+ }
4855
+
4856
+ /** 没有外框的地方(第一屏的等待卡片):按设备大小排好,再整体缩进容器里 */
4857
+ function FitDraft({ url, kind, lang, onEnd }) {
4858
+ const ref = useRef(null)
4859
+ const [sc, setSc] = useState(0)
4860
+ const [w, hh] = sizeOf('', kind)
4861
+ useEffect(() => {
4862
+ const el = ref.current
4863
+ if (!el) return
4864
+ const m = () => setSc(Math.min(el.clientWidth / w, el.clientHeight / hh) || 0)
4865
+ m()
4866
+ const ro = new ResizeObserver(m)
4867
+ ro.observe(el)
4868
+ return () => ro.disconnect()
4869
+ }, [w, hh])
4870
+ return h('div', { ref, className: 'ddp-fitd' },
4871
+ sc ? h('div', { style: { width: w, height: hh, transform: `translate(-50%, -50%) scale(${sc})` } }, h(DraftFrame, { url, z: sc, lang, onEnd })) : null)
4872
+ }
4873
+
4006
4874
  function Skeleton({ gone }) {
4007
4875
  return h('div', { className: 'ddp-skel' + (gone ? ' gone' : '') },
4008
4876
  h('b', { style: { height: 22, width: '38%' } }),
@@ -4057,23 +4925,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4057
4925
  err ? h('div', { className: 'ddp-err', style: className ? { width: '100%' } : { position: 'absolute', top: 40, left: 12 } }, err) : null)
4058
4926
  }
4059
4927
 
4060
- /** 空状态的小插画:页面线框,光标移过去,框出一块 */
4061
- function EmptyArt() {
4062
- return h('svg', { className: 'ddp-art', viewBox: '0 0 176 120', 'aria-hidden': true },
4063
- h('rect', { className: 'pg', x: 8, y: 6, width: 160, height: 108, rx: 11 }),
4064
- h('path', { className: 'ln', d: 'M8 23h160' }),
4065
- h('circle', { className: 'dt', cx: 19, cy: 14.5, r: 2.4 }),
4066
- h('circle', { className: 'dt', cx: 27, cy: 14.5, r: 2.4 }),
4067
- h('circle', { className: 'dt', cx: 35, cy: 14.5, r: 2.4 }),
4068
- h('rect', { className: 'sk', x: 22, y: 35, width: 78, height: 9, rx: 4.5 }),
4069
- h('rect', { className: 'sk', x: 22, y: 50, width: 58, height: 6, rx: 3 }),
4070
- h('rect', { className: 'bt', x: 22, y: 66, width: 44, height: 15, rx: 7.5 }),
4071
- h('rect', { className: 'sk', x: 106, y: 35, width: 48, height: 48, rx: 9 }),
4072
- h('rect', { className: 'sk', x: 22, y: 92, width: 132, height: 10, rx: 5 }),
4073
- h('rect', { className: 'sel', x: 16, y: 60, width: 56, height: 27, rx: 10 }),
4074
- h('path', { className: 'cur', d: 'M0 0l6.5 16 2.4-6.6 6.6-2.4z' }))
4075
- }
4076
-
4077
4928
  /**
4078
4929
  * 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
4079
4930
  * fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
@@ -4252,9 +5103,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4252
5103
  setCur(id)
4253
5104
  const p = P.current
4254
5105
  const build = first ? !!p.born : !!(changes && changes.whole && p.busy)
4255
- postTo(id, { type: 'reveal', build })
5106
+ postTo(id, { type: 'zoom', z: p.zoom || 1 }) // 先定缩放:生成动画里的鼠标、名字按它定大小
5107
+ postTo(id, { type: 'reveal', build, lang: p.t && p.t('lang') === 'en' ? 'en' : 'zh' })
4256
5108
  postTo(id, { type: 'busy', on: !!p.busy })
4257
- postTo(id, { type: 'zoom', z: p.zoom || 1 })
4258
5109
  if (p.picking) postTo(id, pickMsg())
4259
5110
  if (p.demo) postTo(id, { type: 'demo', on: true })
4260
5111
  if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
@@ -4506,27 +5357,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4506
5357
  }
4507
5358
 
4508
5359
  /** 类型菜单:网页 / App / 平板 / 海报 / 幻灯片 / 邮件 */
4509
- function KindMenu({ t, kind, open, setOpen, onPick, disabled }) {
4510
- const ref = useRef(null)
4511
- useEffect(() => {
4512
- if (!open) return undefined
4513
- const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
4514
- const esc = (e) => { if (e.key === 'Escape') setOpen(false) }
4515
- document.addEventListener('pointerdown', off, true)
4516
- window.addEventListener('keydown', esc)
4517
- return () => {
4518
- document.removeEventListener('pointerdown', off, true)
4519
- window.removeEventListener('keydown', esc)
4520
- }
4521
- }, [open])
4522
- return h('div', { className: 'ddp-kind', ref },
4523
- h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('kind'), disabled: !!disabled, onClick: () => setOpen(!open) },
4524
- h(I[kind]), h('span', null, t(KIND_KEY[kind])), h(I.caret)),
4525
- open ? h('div', { className: 'ddp-menu', role: 'menu' },
4526
- h('div', { className: 'hd' }, t('kind')),
4527
- ...KIND_LIST.map((k) => h('button', { key: k, role: 'menuitemradio', 'aria-checked': k === kind, className: k === kind ? 'sel' : '', onClick: () => onPick(k) },
4528
- h(I[k]), h('span', null, t(KIND_KEY[k])), h('em', null, KIND_SIZE[k].join('×'))))) : null)
4529
- }
4530
5360
 
4531
5361
  // ── 画笔标注:一层透明画布盖在预览上,画的东西按台面上的坐标(CSS 像素)一笔一笔记着,能撤销、能擦 ──
4532
5362
  // 一笔:pen / hl { p: [[x, y]…] } · arrow / rect / oval { a: [x, y], b: [x, y] } · text { x, y, f, v };都带颜色 c,线宽 w
@@ -4793,9 +5623,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4793
5623
  const [pal, setPal] = useState(false) // 颜色板开着
4794
5624
  const [hist, setHist] = useState({ list: [[]], i: 0 }) // 每画一笔存一份整张的,撤销 / 重做就是前后挪
4795
5625
  const [txt, setTxt] = useState(null) // 正在写的字:{ x, y, v }
4796
- // 头几次打开时页面中间教一下怎么用(画了第一笔就收起)
5626
+ // 第一次打开时在底栏上面教一下怎么用(画了第一笔就收起;以后底栏那句够用了)
4797
5627
  const [coach] = useState(() => {
4798
- try { const n = Number(localStorage.getItem('ddp-mk-coach')) || 0; if (n >= 3) return false; localStorage.setItem('ddp-mk-coach', String(n + 1)) } catch { /* 存不了就每次都教 */ }
5628
+ try { const n = Number(localStorage.getItem('ddp-mk-coach')) || 0; if (n >= 1) return false; localStorage.setItem('ddp-mk-coach', String(n + 1)) } catch { /* 存不了就每次都教 */ }
4799
5629
  return true
4800
5630
  })
4801
5631
  const shapes = hist.list[hist.i]
@@ -5062,39 +5892,76 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5062
5892
  h('button', { type: 'button', className: 'go' + (shapes.length || (txt && txt.v.trim()) ? '' : ' idle'), disabled: sending, onClick: send }, t('mkSend')))))
5063
5893
  }
5064
5894
 
5065
- /** 设计规范面板:按组列出变量,点一个放进输入框 */
5066
- function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onSystems, onSaveStyle, onLibrary, onClose }) {
5895
+ /** 设计变量的小预览(字号、圆角、间距、阴影、字体) */
5896
+ function tokenPreview(kind, v) {
5897
+ const px = Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
5898
+ switch (kind) {
5899
+ case 'font': return h('span', { style: { fontFamily: v, fontSize: 15, fontWeight: 600 } }, 'Aa')
5900
+ case 'size': return h('span', { style: { fontSize: Math.min(22, px), fontWeight: 600, lineHeight: 1 } }, '字')
5901
+ case 'space': return h('i', { className: 'bar', style: { width: Math.max(2, Math.min(24, px / 2)) } })
5902
+ case 'radius': return h('i', { className: 'rd', style: { borderTopLeftRadius: Math.min(14, px) } })
5903
+ case 'shadow': return h('i', { className: 'sh', style: { boxShadow: v } })
5904
+ default: return null
5905
+ }
5906
+ }
5907
+
5908
+ /**
5909
+ * 外观面板:整套(配色直接改、别的变量交给模型、调节滑块)|换风格(风格卡、参考图、我的风格)|品牌(设计系统)。
5910
+ * 整套里改的颜色和拖的滑块先在页面上看效果,底下「直接保存」写进文件,或「交给模型」。
5911
+ */
5912
+ function LookPanel({ t, tab, setTab, tokens, tv, onTv, onTokAsk, onTokMake, tw, live, onTw, onTwMake, write, onReset, onAsk, onSave, onPickStyle, onRef, onSaveStyle, onInspire, brand, onClose }) {
5067
5913
  const groups = {}
5068
5914
  for (const tok of tokens) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
5069
- const px = (v) => Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
5070
- const preview = (kind, v) => {
5071
- switch (kind) {
5072
- case 'color': return h('i', { className: 'sw', style: { background: v } })
5073
- case 'font': return h('span', { style: { fontFamily: v, fontSize: 15, fontWeight: 600 } }, 'Aa')
5074
- case 'size': return h('span', { style: { fontSize: Math.min(22, px(v)), fontWeight: 600, lineHeight: 1 } }, '字')
5075
- case 'space': return h('i', { className: 'bar', style: { width: Math.max(2, Math.min(24, px(v) / 2)) } })
5076
- case 'radius': return h('i', { className: 'rd', style: { borderTopLeftRadius: Math.min(14, px(v)) } })
5077
- case 'shadow': return h('i', { className: 'sh', style: { boxShadow: v } })
5078
- default: return null
5079
- }
5080
- }
5081
- return h('div', { className: 'ddp-tokens', onPointerDown: (e) => e.stopPropagation() },
5082
- h('header', null, h(I.palette), h('b', null, t('tokens')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
5083
- tokens.length ? h('p', { className: 'tip' }, t('tokensTip')) : null,
5084
- h('div', { className: 'bd' },
5085
- tokens.length
5086
- ? TOKEN_GROUPS.filter((g) => groups[g]).map((g) => h('div', { key: g },
5915
+ const names = tokenLabels(t, tokens)
5916
+ const list = (tw && tw.list) || []
5917
+ const errs = (tw && tw.errors) || []
5918
+ const n = Object.keys(tv).length + list.filter((k) => hasOwn(live, k.id)).length
5919
+ const tabs = [['set', t('lookSet')], ['style', t('lookStyle')], ['brand', t('lookBrand')]]
5920
+ let body
5921
+ if (tab === 'style') {
5922
+ body = [
5923
+ h('p', { className: 'tip', key: 'tip' }, t('stylesSwap')),
5924
+ h('div', { className: 'bd', key: 'bd' }, h(StyleCards, { t, onPick: onPickStyle }),
5925
+ h('div', { className: 'ddp-lk-more' },
5926
+ onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('lookRef'))) : null,
5927
+ onSaveStyle && tokens.length ? h('button', { title: t('inSaveStyleTip'), onClick: onSaveStyle }, h(I.plus), h('span', null, t('tkSaveStyle'))) : null,
5928
+ onInspire ? h('button', { onClick: onInspire }, h(I.box), h('span', null, t('lookInspire'))) : null)),
5929
+ ]
5930
+ } else if (tab === 'brand') {
5931
+ body = [brand]
5932
+ } else {
5933
+ const colors = groups.color || []
5934
+ body = [
5935
+ h('div', { className: 'bd', key: 'bd' },
5936
+ tokens.length ? null : h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onTokMake }, t('tokensAsk')))),
5937
+ colors.length ? h('h4', null, t('gColor'), h('span', null, t('lookColorTip'))) : null,
5938
+ colors.length ? h('div', { className: 'ddp-lkc' }, ...colors.map((tok) => {
5939
+ const v = hasOwn(tv, tok.name) ? tv[tok.name] : tok.value
5940
+ return h('label', { key: tok.name, className: 'it' + (hasOwn(tv, tok.name) ? ' on' : ''), title: `${tok.name}: ${v}` },
5941
+ h('span', { className: 'sw', style: { background: v } },
5942
+ h('input', { type: 'color', value: colorHex(v, tokens), 'aria-label': names.get(tok.name), onChange: (e) => onTv(tok.name, e.target.value) })),
5943
+ h('span', { className: 'tx' }, h('span', { className: 'nm' }, names.get(tok.name)), h('span', { className: 'vl' }, v)))
5944
+ })) : null,
5945
+ ...TOKEN_GROUPS.filter((g) => g !== 'color' && groups[g]).map((g) => h('div', { key: g },
5087
5946
  h('h4', null, t(TOKEN_KEY[g])),
5088
- ...groups[g].map((tok) => h('button', { key: tok.name, className: 'ddp-tok', title: `${tok.name}: ${tok.value}`, onClick: () => onPick(tok) },
5089
- h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, preview(g, tok.value)),
5090
- h('span', { className: 'tx' }, h('span', { className: 'nm' }, tok.name), h('span', { className: 'vl' }, tok.value))))))
5091
- : h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onAsk }, t('tokensAsk'))))),
5092
- onRef || onStyles || onLibrary ? h('footer', null,
5093
- onSaveStyle && tokens.length ? h('button', { title: t('inSaveStyleTip'), onClick: onSaveStyle }, h(I.plus), h('span', null, t('tkSaveStyle'))) : null,
5094
- onStyles ? h('button', { onClick: onStyles }, h(I.sparkle), h('span', null, t('stylesOpen'))) : null,
5095
- onSystems ? h('button', { onClick: onSystems }, h(I.library), h('span', null, t('sysOpen'))) : null,
5096
- onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('refPick'))) : null,
5097
- onLibrary ? h('button', { onClick: onLibrary }, h(I.box), h('span', null, t('library'))) : null) : null)
5947
+ ...groups[g].map((tok) => h('button', { key: tok.name, className: 'ddp-tok', title: `${tok.name}: ${tok.value}\n${t('lookTokTip')}`, onClick: () => onTokAsk(tok) },
5948
+ h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, tokenPreview(g, tok.value)),
5949
+ h('span', { className: 'tx' }, h('span', { className: 'nm' }, names.get(tok.name)), h('span', { className: 'vl' }, tok.value)))))),
5950
+ h('h4', null, t('lookTw')),
5951
+ list.length
5952
+ ? h('div', { className: 'ddp-style ddp-lktw' }, ...list.map((k) => tweakRow(t, k, live, onTw)), errs.length ? h('div', { className: 'note', key: 'err' }, errs.join(';')) : null)
5953
+ : h('p', { className: 'ddp-lk-none' }, errs.length ? errs.join(';') : t('lookTwNone'), ' ', h('button', { className: 'lk', onClick: () => onTwMake(errs) }, t(errs.length ? 'twFix' : 'twMake')))),
5954
+ n ? h('footer', { className: 'row', key: 'ft' },
5955
+ h('span', { className: 'cnt' }, t('lookN', { n })),
5956
+ h('button', { onClick: onReset }, h('span', null, t('styleReset'))),
5957
+ h('button', { onClick: onAsk, title: t('twAskTip') }, h('span', null, t('styleAsk'))),
5958
+ h('button', { className: 'go', onClick: onSave, disabled: !write.ok, title: write.ok ? t('lookSaveTip') : write.tip }, h('span', null, t('lookSave')))) : null,
5959
+ ]
5960
+ }
5961
+ return h('div', { className: 'ddp-tokens ddp-look', onPointerDown: (e) => e.stopPropagation() },
5962
+ h('header', null, h(I.palette), h('b', null, t('look')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
5963
+ h('div', { className: 'ddp-lktabs', role: 'tablist' }, ...tabs.map(([k, label]) => h('button', { key: k, role: 'tab', 'aria-selected': tab === k, className: tab === k ? 'on' : '', onClick: () => setTab(k) }, label))),
5964
+ ...body)
5098
5965
  }
5099
5966
 
5100
5967
  /** 设计体检的结果:按屏列出问题(和页面上的编号一样),点一条在页面上滚过去、那个框闪一下 */
@@ -5118,7 +5985,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5118
5985
  n ? h('p', { className: 'tip' }, t('checkNote')) : null,
5119
5986
  h('footer', { className: 'row' },
5120
5987
  h('button', { onClick: onAgain, disabled: ck.running }, h(I.refresh), h('span', null, t('checkAgain'))),
5121
- h('button', { className: 'go', onClick: onFix, disabled: ck.running || !n }, h(I.sparkle), h('span', null, t('checkFix')))))
5988
+ n ? h('button', { className: 'go', onClick: onFix, disabled: ck.running, title: t('checkFixTip') }, h(I.sparkle), h('span', null, t('checkFix'))) : null))
5122
5989
  }
5123
5990
 
5124
5991
  /**
@@ -5136,13 +6003,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5136
6003
  // 现在用的是哪个变量:改过的看改成了什么,没改的看样式表里怎么写的
5137
6004
  const varOf = (k) => { const m = VAR_ONLY.exec(String(cur(k) || '').trim()); return m ? m[1] : k in sty.live ? '' : (sty.vars || {})[k] || '' }
5138
6005
  const set = (props, v) => onSet(Object.fromEntries(props.map((p) => [p, v])))
6006
+ const names = tokenLabels(t, tokens)
5139
6007
  const chips = (kind, props) => (groups[kind] || []).slice(0, 10).map((tok) => h('button', {
5140
6008
  key: tok.name,
5141
6009
  className: 'chip' + (kind === 'color' ? ' c' : '') + (varOf(props[0]) === tok.name ? ' on' : ''),
5142
6010
  title: `${tok.name}: ${tok.value}`,
5143
6011
  onClick: () => set(props, `var(${tok.name})`),
5144
- }, kind === 'color' ? h('i', { style: { background: tok.value } }) : tok.name.replace(/^--/, '')))
5145
- const shown = (k, text) => (varOf(k) ? h('em', { className: 'v', title: t('styleVar', { name: varOf(k) }) }, varOf(k)) : h('em', null, text))
6012
+ }, kind === 'color' ? h('i', { style: { background: tok.value } }) : names.get(tok.name)))
6013
+ const nice = (v) => names.get(v) || v.replace(/^--/, '')
6014
+ const shown = (k, text) => (varOf(k) ? h('em', { className: 'v', title: t('styleVar', { name: varOf(k) }) }, nice(varOf(k))) : h('em', null, text))
5146
6015
  const slider = (key, props, max, kind) => {
5147
6016
  const v = Math.round(pxOf(cur(props[0]), tokens) ?? 0)
5148
6017
  const c = kind ? chips(kind, props) : []
@@ -5160,7 +6029,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5160
6029
  const w = Math.min(700, Math.max(400, Math.round(parseInt(cur('font-weight'), 10) / 100) * 100 || 400))
5161
6030
  const n = Object.keys(sty.live).length
5162
6031
  return box(
5163
- h('p', { className: 'who', title: sty.selector }, h('code', null, sty.name), sty.text ? ` · ${sty.text}` : ''),
6032
+ h('p', { className: 'who', title: `${sty.name} · ${sty.selector}` }, ...(() => { const e = elemLabel(t, sty.name, sty.text); return [h('b', null, e.role), e.text ? h('span', null, `「${e.text}」`) : null] })()),
5164
6033
  h('div', { className: 'bd' },
5165
6034
  slider('styleFont', ['font-size'], 72, 'size'),
5166
6035
  h('div', { className: 'row', key: 'w' },
@@ -5173,10 +6042,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5173
6042
  slider('stylePadX', ['padding-left', 'padding-right'], 48, 'space'),
5174
6043
  sty.layout ? slider('styleGap', ['gap'], 48, 'space') : null),
5175
6044
  n ? h('div', { className: 'cnt' }, t('styleN', { n: styleChanges(t, sty).length })) : null,
5176
- h('footer', { className: 'row' },
5177
- h('button', { onClick: onReset, disabled: !n }, h('span', null, t('styleReset'))),
5178
- h('button', { onClick: onAsk, disabled: !n, title: t('styleAskTip') }, h('span', null, t('styleAsk'))),
5179
- h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
6045
+ n ? h('footer', { className: 'row' },
6046
+ h('button', { onClick: onReset }, h('span', null, t('styleReset'))),
6047
+ h('button', { onClick: onAsk, title: t('styleAskTip') }, h('span', null, t('styleAsk'))),
6048
+ h('button', { className: 'go', onClick: onSave, disabled: !write.ok, title: write.ok ? t('lookSaveTip') : write.tip }, h('span', null, t('styleSave')))) : null)
5180
6049
  }
5181
6050
 
5182
6051
  /** 风格样板:用这个风格画的一小屏(顶栏、标题、按钮、商品卡、数据卡);卡片上缩小着放,点开是原大 */
@@ -5207,12 +6076,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5207
6076
  }
5208
6077
 
5209
6078
  /** 一排风格卡:每张是缩小的样板;点一张看原大样板(左右翻),「用这个风格」才交给 onPick */
5210
- function StyleCards({ t, onPick }) {
6079
+ function StyleCards({ t, onPick, limit }) {
5211
6080
  const en = t('lang') === 'en'
5212
6081
  // 内置的 20 张 + 设计库 › 灵感里存的「我的风格」
5213
6082
  const mine = useInspire().filter((x) => x.type === 'style' && x.style && x.style.c).map(myStyleOf)
5214
6083
  const LIST = mine.length ? [...STYLES, ...mine] : STYLES
5215
6084
  const [on, setOn] = useState(-1) // 正在看样板的那个;-1 是看全部
6085
+ const [all, setAll] = useState(false) // 空白页只放前 limit 张,点「更多风格」全展开
5216
6086
  const was = useRef(-1)
5217
6087
  const box = useRef(null)
5218
6088
  useEffect(() => {
@@ -5224,6 +6094,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5224
6094
  // 列表和样板高矮不一样,外面滚动的位置会变:把样板 / 刚才那张卡滚进来
5225
6095
  if (was.current >= 0 || on >= 0) (on >= 0 ? el : f || el).scrollIntoView({ block: 'nearest' })
5226
6096
  if (on >= 0) was.current = on
6097
+ if (on >= 0 && limit && on >= limit) setAll(true) // 样板里翻到了后面的卡:回全部时要看得到它
5227
6098
  }, [on])
5228
6099
  const n = LIST.length
5229
6100
  if (on >= 0) {
@@ -5245,17 +6116,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5245
6116
  h('button', { className: 'ddp-btn nx', title: t('styleNext'), onClick: () => go(1) }, h(I.back)),
5246
6117
  h('button', { className: 'ddp-go', onClick: () => onPick(x) }, t('styleUse'))))
5247
6118
  }
5248
- return h('div', { className: 'ddp-styles', ref: box }, ...LIST.map((x, i) => h('button', { key: x.id, className: 'ddp-sc' + (x.mine ? ' mine' : ''), title: en ? x.den : x.dzh, onClick: () => setOn(i) },
6119
+ const cut = limit && !all && n > limit
6120
+ const grid = h('div', { className: 'ddp-styles' + (cut ? ' one' : ''), ref: box }, ...(cut ? LIST.slice(0, limit) : LIST).map((x, i) => h('button', { key: x.id, className: 'ddp-sc' + (x.mine ? ' mine' : ''), title: en ? x.den : x.dzh, onClick: () => setOn(i) },
5249
6121
  h('span', { className: 'pv', 'aria-hidden': 'true' }, h(StyleSample, { t, x })),
5250
6122
  h('span', { className: 'nm' }, en ? x.en : x.zh))))
5251
- }
5252
-
5253
- /** 换风格面板(设计已经打开时):点一张卡让模型整套换成那个风格 */
5254
- function StylesPanel({ t, onPick, onClose }) {
5255
- return h('div', { className: 'ddp-tokens', onPointerDown: (e) => e.stopPropagation() },
5256
- h('header', null, h(I.palette), h('b', null, t('stylesTitle')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
5257
- h('p', { className: 'tip' }, t('stylesSwap')),
5258
- h('div', { className: 'bd' }, h(StyleCards, { t, onPick })))
6123
+ if (!limit || n <= limit) return grid
6124
+ return h('div', { className: 'ddp-stylesw' }, grid, h('button', { className: 'lk ddp-more-st', onClick: () => setAll(!all) }, all ? t('stylesLess') : t('stylesMore', { n: n - limit })))
5259
6125
  }
5260
6126
 
5261
6127
  // ── 调节滑块、品牌设计系统、转成代码 ─────────────────────────────────────
@@ -5283,41 +6149,32 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5283
6149
  return t('twAskHead') + '\n' + rows.join('\n') + '\n' + t('twAskTail') + '\n'
5284
6150
  }
5285
6151
 
5286
- /** 调节滑块面板:模型给这套设计做的几个维度(tweaks.json),拖的时候所有屏一起变,定下来写回文件或交给模型 */
5287
- function TweaksPanel({ t, tw, live, write, onSet, onReset, onAsk, onSave, onMake, onClose }) {
5288
- const head = h('header', null, h(I.dial), h('b', null, t('tweaks')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
5289
- const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style ddp-tw', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
5290
- const list = (tw && tw.list) || []
5291
- const errs = (tw && tw.errors) || []
5292
- if (!list.length) {
5293
- return box(h('div', { className: 'bd' }, h('div', { className: 'none' },
5294
- errs.length ? errs.map((e, i) => h('div', { key: i }, e)) : t('twEmpty'),
5295
- h('div', null, h('button', { className: 'ddp-go', onClick: () => onMake(errs) }, t(errs.length ? 'twFix' : 'twMake'))))))
5296
- }
5297
- const cur = (k) => (hasOwn(live, k.id) ? live[k.id] : k.value)
5298
- const row = (k) => {
5299
- const v = cur(k)
5300
- const lab = h('label', null, h('span', { title: k.var || k.attr }, k.label), h('em', { className: hasOwn(live, k.id) ? 'v' : '' }, tweakShown(k, v)))
5301
- const note = k.note ? h('div', { className: 'note' }, k.note) : null
5302
- if (k.kind === 'range') {
5303
- return h('div', { className: 'row', key: k.id }, lab,
5304
- h('input', { type: 'range', min: k.min, max: k.max, step: k.step, value: v, 'aria-label': k.label, onChange: (e) => onSet(k, Number(e.target.value)) }), note)
5305
- }
5306
- if (k.kind === 'color') {
5307
- return h('div', { className: 'row', key: k.id }, lab,
5308
- h('div', { className: 'chips' }, h('input', { type: 'color', value: /^#[0-9a-f]{6}$/i.test(v) ? v : '#000000', 'aria-label': k.label, onChange: (e) => onSet(k, e.target.value) })), note)
5309
- }
6152
+ /** 调节滑块的一行:模型给这套设计做的维度(tweaks.json),拖的时候所有屏一起变 */
6153
+ function tweakRow(t, k, live, onSet) {
6154
+ const v = hasOwn(live, k.id) ? live[k.id] : k.value
6155
+ const lab = h('label', null, h('span', { title: k.var || k.attr }, k.label), h('em', { className: hasOwn(live, k.id) ? 'v' : '' }, tweakShown(k, v)))
6156
+ const note = k.note ? h('div', { className: 'note' }, k.note) : null
6157
+ if (k.kind === 'range') {
5310
6158
  return h('div', { className: 'row', key: k.id }, lab,
5311
- h('div', { className: 'seg' }, ...(k.options || []).map((o) => h('button', { key: o.value, className: String(v) === o.value ? 'on' : '', onClick: () => onSet(k, o.value) }, o.label))), note)
6159
+ h('input', { type: 'range', min: k.min, max: k.max, step: k.step, value: v, 'aria-label': k.label, onChange: (e) => onSet(k, Number(e.target.value)) }), note)
5312
6160
  }
5313
- const n = list.filter((k) => hasOwn(live, k.id)).length
5314
- return box(
5315
- h('p', { className: 'tip' }, t('twTip')),
5316
- h('div', { className: 'bd' }, ...list.map(row), errs.length ? h('div', { className: 'note', key: 'err' }, errs.join(';')) : null),
5317
- h('footer', { className: 'row' },
5318
- h('button', { onClick: onReset, disabled: !n }, h('span', null, t('styleReset'))),
5319
- h('button', { onClick: onAsk, disabled: !n, title: t('twAskTip') }, h('span', null, t('styleAsk'))),
5320
- h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
6161
+ if (k.kind === 'color') {
6162
+ return h('div', { className: 'row', key: k.id }, lab,
6163
+ h('div', { className: 'chips' }, h('input', { type: 'color', value: /^#[0-9a-f]{6}$/i.test(v) ? v : '#000000', 'aria-label': k.label, onChange: (e) => onSet(k, e.target.value) })), note)
6164
+ }
6165
+ return h('div', { className: 'row', key: k.id }, lab,
6166
+ h('div', { className: 'seg' }, ...(k.options || []).map((o) => h('button', { key: o.value, className: String(v) === o.value ? 'on' : '', onClick: () => onSet(k, o.value) }, o.label))), note)
6167
+ }
6168
+
6169
+ /** 外观面板里改的:滑块 + 直接改的设计变量 → 交给模型的一段字 */
6170
+ function formatLook(t, list, live, tokens, tv) {
6171
+ let out = Object.keys(live).length ? formatTweaks(t, list, live) : ''
6172
+ const rows = Object.entries(tv).map(([name, v]) => {
6173
+ const tok = tokens.find((x) => x.name === name)
6174
+ return `- ${name}(${tok ? tokenLabel(t, tok) : name}):${tok ? tok.value : '?'} → ${v}`
6175
+ })
6176
+ if (rows.length) out += t('lookAskHead') + '\n' + rows.join('\n') + '\n' + t('lookAskTail') + '\n'
6177
+ return out
5321
6178
  }
5322
6179
 
5323
6180
  async function getJson(path) {
@@ -5348,7 +6205,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5348
6205
  return h('button', { type: 'button', className: 'ddp-dirbtn', title: t('pickDir'), 'aria-label': t('pickDir'), disabled: busy, onClick: pick }, h(I.folder))
5349
6206
  }
5350
6207
 
5351
- function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose }) {
6208
+ function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose, bare }) {
5352
6209
  const [data, setData] = useState(null)
5353
6210
  const [err, setErr] = useState('')
5354
6211
  const [src, setSrc] = useState('')
@@ -5366,9 +6223,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5366
6223
  onExtract(v)
5367
6224
  setSrc('')
5368
6225
  }
5369
- return h('div', { className: 'ddp-tokens ddp-sys', onPointerDown: (e) => e.stopPropagation() },
5370
- h('header', null, h(I.library), h('b', null, t('systems')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
6226
+ return h('div', { className: bare ? 'ddp-sys bare' : 'ddp-tokens ddp-sys', onPointerDown: (e) => e.stopPropagation() },
6227
+ bare ? null : h('header', null, h(I.library), h('b', null, t('systems')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
5371
6228
  h('p', { className: 'tip' }, t('sysTip')),
6229
+ h('form', { onSubmit: go },
6230
+ h('input', { value: src, placeholder: t('sysPh'), 'aria-label': t('sysPh'), onChange: (e) => setSrc(e.target.value) }),
6231
+ h(DirButton, { t, onPicked: setSrc, onSay }),
6232
+ h('button', { type: 'submit', disabled: !src.trim() }, t('sysExtract'))),
5372
6233
  h('div', { className: 'bd' },
5373
6234
  err ? h('div', { className: 'none' }, err) : null,
5374
6235
  data && !list.length ? h('div', { className: 'none' }, t('sysEmpty')) : null,
@@ -5379,12 +6240,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5379
6240
  x.sources && x.sources.length ? h('small', { title: x.sources.join('\n') }, x.sources[0]) : null,
5380
6241
  h('div', { className: 'acts' },
5381
6242
  h('button', { onClick: () => setDef(x, !x.default) }, x.default ? t('sysUnset') : t('sysSet')),
5382
- hasDesign ? h('button', { onClick: () => onApply(x) }, t('sysApply')) : null)))),
5383
- h('footer', null,
5384
- h('form', { onSubmit: go },
5385
- h('input', { value: src, placeholder: t('sysPh'), 'aria-label': t('sysPh'), onChange: (e) => setSrc(e.target.value) }),
5386
- h(DirButton, { t, onPicked: setSrc, onSay }),
5387
- h('button', { type: 'submit', disabled: !src.trim() }, t('sysExtract')))))
6243
+ hasDesign ? h('button', { onClick: () => onApply(x) }, t('sysApply')) : null)))))
5388
6244
  }
5389
6245
 
5390
6246
  const FW_LIST = ['react', 'vue', 'next', 'html']
@@ -5392,6 +6248,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5392
6248
  // 小程序的设计:先列 uni-app、原生小程序
5393
6249
  const FW_MINI = ['uniapp', 'weapp']
5394
6250
 
6251
+ /** 「uni-app(HBuilderX,Vue 3)」→「uni-app · HBuilderX · Vue 3」:放进「跟现有项目一样:…」里别括号套括号 */
6252
+ const dirParts = (x) => String(x || '').split(/[\\/]+/).filter(Boolean)
6253
+ const dirName = (x) => dirParts(x).pop() || String(x || '')
6254
+ const dirParent = (x) => { const q = dirParts(x); return q.length > 1 ? q[q.length - 2] : '' }
6255
+ const flatLabel = (x) => String(x || '').replace(/\s*[((]\s*/g, ' · ').replace(/\s*[,,、]\s*/g, ' · ').replace(/\s*[))]\s*/g, ' ').replace(/\s+/g, ' ').trim()
6256
+
5395
6257
  /** 转成代码:用什么做、放哪、转几屏;点了把一段话放进输入框,模型调 handoff_design 照交接包做,每屏做完 compare_design */
5396
6258
  function HandoffPanel({ t, sid, root, fileTarget, count, cur, onGo, onSay, onClose }) {
5397
6259
  const [info, setInfo] = useState(null)
@@ -5400,6 +6262,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5400
6262
  const [one, setOne] = useState(false)
5401
6263
  const [moved, setMoved] = useState(null) // 选的文件夹不能直接放:{ dest, why }('nested' 在里面新建、'beside' 放设计目录旁边)
5402
6264
  const [checking, setChecking] = useState('') // 'busy' 正在问服务器这个文件夹;别的字是没问成的原因
6265
+ const [typing, setTyping] = useState(false) // 路径平时只显示文件夹名,点了才变输入框
5403
6266
  // 现在是哪套设计、第几次选:选文件夹的窗口开着时换了设计、或者又选了一次 / 手改了路径,旧的结果作废
5404
6267
  // hand:用户自己点过框架(换设计前一直听用户的,不再按选的文件夹自动换)
5405
6268
  const live = useRef({ root, seq: 0, hand: false })
@@ -5450,7 +6313,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5450
6313
  const p = info.project || {}
5451
6314
  const same = !!p.framework
5452
6315
  // 「跟现有项目一样」认得出 HBuilderX 的 uni-app、微信原生小程序(没有 package.json 也行)
5453
- const sameLabel = p.framework === 'weapp' ? t('hoWeapp') : p.label
6316
+ const sameLabel = flatLabel(p.framework === 'weapp' ? t('hoWeapp') : p.label)
5454
6317
  const list = info.kind === 'miniprogram' ? [...FW_MINI, ...FW_LIST] : FW_LIST
5455
6318
  const name = (k) => (k === 'html' ? t('hoHtml') : k === 'uniapp' ? t('hoUni') : k === 'weapp' ? t('hoWeapp') : FW_LABEL[k])
5456
6319
  const opts = [...(same ? [['same', t('hoSame', { label: sameLabel })]] : []), ...list.map((k) => [k, name(k)])]
@@ -5462,7 +6325,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5462
6325
  ...opts.map(([k, l]) => h('button', { key: k, className: 'opt' + (fw === k ? ' on' : ''), onClick: () => { live.current.hand = true; setFw(k) } }, h('i'), h('span', null, l)))),
5463
6326
  h('div', { className: 'row' }, h('label', null, t('hoDest')),
5464
6327
  h('div', { className: 'pathrow' },
5465
- h('input', { type: 'text', value: dest, spellCheck: false, 'aria-label': t('hoDest'), onChange: (e) => typeDest(e.target.value) }),
6328
+ typing || !dest.trim()
6329
+ ? h('input', { type: 'text', value: dest, spellCheck: false, autoFocus: typing, 'aria-label': t('hoDest'), onChange: (e) => typeDest(e.target.value), onBlur: () => setTyping(false), onKeyDown: (e) => { if (e.key === 'Enter' || e.key === 'Escape') e.currentTarget.blur() } })
6330
+ : h('button', { type: 'button', className: 'pathv', title: dest + '\n' + t('hoEdit'), onClick: () => setTyping(true) }, h(I.folder), h('b', null, dirName(dest)), dirParent(dest) ? h('em', null, t('hoIn', { dir: dirParent(dest) })) : null),
5466
6331
  h(DirButton, { t, onPicked: pickDest, onSay })),
5467
6332
  checking ? h('p', { className: 'moved' + (checking === 'busy' ? '' : ' bad') }, checking === 'busy' ? t('hoChecking') : checking)
5468
6333
  : moved && moved.dest === dest ? h('p', { className: 'moved' }, t(moved.why === 'beside' ? 'hoBeside' : 'hoMoved', { name: moved.dest.split(/[\\/]/).pop() })) : null),
@@ -5487,40 +6352,155 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5487
6352
  window.removeEventListener('keydown', esc)
5488
6353
  }
5489
6354
  }, [open])
6355
+ // 菜单比剩下的地方高(屏多、侧栏矮):限高、整个菜单能滚,底下的入口也点得到
6356
+ react.useLayoutEffect(() => {
6357
+ const wrap = ref.current
6358
+ const m = open && wrap ? wrap.querySelector(':scope > .ddp-menu') : null
6359
+ const box = wrap ? wrap.closest('.ddp-stage, .ddp') : null
6360
+ if (!m || !box) return
6361
+ const b = box.getBoundingClientRect()
6362
+ const r = m.getBoundingClientRect()
6363
+ const up = r.bottom <= wrap.getBoundingClientRect().top + 1
6364
+ const room = Math.floor(up ? r.bottom - b.top - 8 : b.bottom - r.top - 8)
6365
+ if (r.height > room) { m.style.maxHeight = Math.max(120, room) + 'px'; m.style.overflowY = 'auto' }
6366
+ }, [open])
5490
6367
  }
5491
6368
 
5492
6369
  /** 导出菜单:这一屏(带外框 / 纯画面 / 整页长图)、整套展示图 */
5493
- function ExportMenu({ t, kind, count, open, setOpen, onPick, disabled, zip }) {
5494
- const ref = useRef(null)
5495
- useDismiss(open, setOpen, ref)
6370
+ /** 导出的几种:这一屏(带外框 / 纯画面 / 整页长图)、整套展示图、PDF、PPT */
6371
+ function exportItems(t, kind, count, name) {
5496
6372
  const items = []
5497
- if (FRAMED[kind]) items.push(['frame', I.single, t('exFrame')])
5498
- items.push(['raw', I.image, t('exRaw')])
5499
- if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong')]) // 海报、幻灯片画幅是定的,没有「整页」
6373
+ const n = { name: name || t('exThisName') } // 写出是哪一屏,别只说「这一屏」
6374
+ if (FRAMED[kind]) items.push(['frame', I.single, t('exFrame', n)])
6375
+ items.push(['raw', I.image, t('exRaw', n)])
6376
+ if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong', n)]) // 海报、幻灯片画幅是定的,没有「整页」
5500
6377
  if (count > 1) items.push(['show', I.canvas, t('exShow')])
5501
6378
  items.push(['pdf', I.pdf, t('exPdf')], ['pptx', I.deck, t('exPptx')])
5502
- if (zip) items.push(['zip', I.box, t('exZip')], ['share', I.share, t('exShare')]) // 只有本地的设计才能打包、分享
5503
- return h('div', { className: 'ddp-kind', ref },
5504
- h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('exportT'), disabled, onClick: () => setOpen(!open) }, h(I.download)),
5505
- open ? h('div', { className: 'ddp-menu', role: 'menu' },
5506
- h('div', { className: 'hd' }, t('exportT')),
5507
- ...items.map(([mode, Icon, label]) => h('button', { key: mode, role: 'menuitem', onClick: () => onPick(mode) }, h(Icon), h('span', null, label))),
5508
- h('div', { className: 'ft' }, t('exWhere'))) : null)
6379
+ return items
5509
6380
  }
5510
6381
 
5511
6382
  /**
5512
- * 「更多」:右侧栏窄的时候,工具栏上放不下的几个按钮收进这里(哪几个收起来由容器宽度的 CSS 决定)。
5513
- * items: [显示档 'o1' | 'o2', 图标, 名字, 选中, 不可用, 点了做什么(字符串就是在浏览器里打开的地址)]
6383
+ * 工具栏上的下拉菜单(视图、交付、更多都用它)。
6384
+ * items: [类名, 图标, 名字, 选中, 不可用, 点了做什么(字符串就是在浏览器里打开的地址), 右边小字]
6385
+ * 字符串 'hd:名字' 是小标题,'sep' 是分隔线
5514
6386
  */
5515
- function MoreMenu({ t, open, setOpen, has, items }) {
6387
+ // 浮块按钮的悬停说明:系统 title 要等一秒多、只有一行灰字;这里停一下就出,上面名字、下面一句用途
6388
+ // 用法:按钮带上 tipA(名字, 说明);HoverTip 放进浮块所在的那层,自己盯着里面带 data-tip 的东西
6389
+ const tipA = (name, desc, key) => ({ 'data-tip': name, 'data-tip-d': desc || undefined, 'data-tip-k': key || undefined, 'aria-label': desc ? name + ' — ' + desc : name, 'aria-keyshortcuts': key && !/\s/.test(key) ? key : undefined })
6390
+ function HoverTip() {
6391
+ const ref = useRef(null)
6392
+ const [tip, setTip] = useState(null) // { n, d, k, cx, y, up }
6393
+ useEffect(() => {
6394
+ const el = ref.current
6395
+ const box = el && el.parentElement
6396
+ if (!box) return
6397
+ let cur = null // 鼠标停在哪个按钮上
6398
+ let timer = 0
6399
+ let showing = false
6400
+ let warmTill = 0 // 刚看过一个:挪到旁边的按钮立刻出,不再等
6401
+ const show = (b) => {
6402
+ if (!b.isConnected || b.getAttribute('aria-expanded') === 'true') return
6403
+ const r = b.getBoundingClientRect()
6404
+ const s = box.getBoundingClientRect()
6405
+ const up = r.top + r.height / 2 > s.top + s.height / 2 // 下半截的按钮往上弹
6406
+ showing = true
6407
+ setTip({ n: b.dataset.tip, d: b.dataset.tipD || '', k: b.dataset.tipK || '', cx: r.left + r.width / 2 - s.left, y: up ? r.top - s.top - 8 : r.bottom - s.top + 8, up })
6408
+ }
6409
+ const hide = () => {
6410
+ clearTimeout(timer)
6411
+ if (showing) warmTill = Date.now() + 600
6412
+ showing = false
6413
+ setTip(null)
6414
+ }
6415
+ const enter = (b) => {
6416
+ if (b === cur) return
6417
+ cur = b
6418
+ clearTimeout(timer)
6419
+ if (!b) return hide()
6420
+ if (showing || Date.now() < warmTill) show(b)
6421
+ else timer = setTimeout(() => show(b), 350)
6422
+ }
6423
+ const over = (e) => { const b = e.target.closest ? e.target.closest('[data-tip]') : null; enter(b && box.contains(b) ? b : null) }
6424
+ const leave = () => enter(null)
6425
+ const down = () => { clearTimeout(timer); showing = false; warmTill = 0; setTip(null) } // 点了就收,等鼠标挪开再说
6426
+ const focus = (e) => { const b = e.target.closest ? e.target.closest('[data-tip]') : null; if (b && b.matches(':focus-visible')) { cur = b; clearTimeout(timer); show(b) } }
6427
+ const blur = () => { cur = null; hide() }
6428
+ const key = (e) => { if (e.key === 'Escape') down() }
6429
+ box.addEventListener('pointerover', over)
6430
+ box.addEventListener('pointerleave', leave)
6431
+ box.addEventListener('pointerdown', down, true)
6432
+ box.addEventListener('focusin', focus)
6433
+ box.addEventListener('focusout', blur)
6434
+ box.addEventListener('keydown', key, true)
6435
+ return () => {
6436
+ clearTimeout(timer)
6437
+ box.removeEventListener('pointerover', over)
6438
+ box.removeEventListener('pointerleave', leave)
6439
+ box.removeEventListener('pointerdown', down, true)
6440
+ box.removeEventListener('focusin', focus)
6441
+ box.removeEventListener('focusout', blur)
6442
+ box.removeEventListener('keydown', key, true)
6443
+ }
6444
+ }, [])
6445
+ // 量出自己多宽再摆(画出来之前):横着别出边,上弹的贴着按钮上沿
6446
+ react.useLayoutEffect(() => {
6447
+ const el = ref.current
6448
+ if (!el || !tip) return
6449
+ const W = el.parentElement ? el.parentElement.clientWidth : 0
6450
+ const w = el.offsetWidth
6451
+ el.style.left = Math.max(8, Math.min(W - w - 8, tip.cx - w / 2)) + 'px'
6452
+ el.style.top = (tip.up ? tip.y - el.offsetHeight : tip.y) + 'px'
6453
+ }, [tip])
6454
+ // 说明已经在按钮的 aria-label 里,读屏不用再念一遍
6455
+ return h('div', { ref, className: 'ddp-tipbox' + (tip ? ' on' : ''), 'aria-hidden': 'true' },
6456
+ tip ? h('div', { className: 'nm' }, h('b', null, tip.n), tip.k ? h('kbd', null, tip.k) : null) : null,
6457
+ tip && tip.d ? h('span', null, tip.d) : null)
6458
+ }
6459
+
6460
+ function DropMenu({ t, cls, btnCls, title, tip, icon, label, caret, has, open, setOpen, disabled, items, foot }) {
5516
6461
  const ref = useRef(null)
5517
6462
  useDismiss(open, setOpen, ref)
5518
- return h('div', { className: 'ddp-kind ddp-more', ref },
5519
- h('button', { className: 'ddp-btn' + (open ? ' sel' : '') + (has ? ' has' : ''), title: t('more'), onClick: () => setOpen(!open) }, h(I.more)),
6463
+ return h('div', { className: 'ddp-kind' + (cls ? ' ' + cls : ''), ref },
6464
+ h('button', { className: 'ddp-btn' + (btnCls ? ' ' + btnCls : '') + (open ? ' sel' : '') + (has ? ' has' : ''), ...(tip ? tipA(tip[0], tip[1], tip[2]) : { title }), 'aria-haspopup': 'menu', 'aria-expanded': open, disabled: !!disabled, onClick: () => setOpen(!open) },
6465
+ icon ? h(icon) : null, label ? h('span', { className: 'lb' }, label) : null, caret ? h(I.caret) : null),
5520
6466
  open ? h('div', { className: 'ddp-menu', role: 'menu' },
5521
- ...items.filter(Boolean).map(([tier, Icon, label, on, off, run]) => (typeof run === 'string'
5522
- ? h('a', { key: label, className: tier, role: 'menuitem', href: run, target: '_blank', rel: 'noopener noreferrer', onClick: () => setOpen(false) }, h(Icon), h('span', null, label))
5523
- : h('button', { key: label, className: tier + (on ? ' sel' : ''), role: 'menuitem', disabled: off, onClick: () => { setOpen(false); run() } }, h(Icon), h('span', null, label))))) : null)
6467
+ ...items.filter(Boolean).map((x, i) => {
6468
+ if (typeof x === 'string') {
6469
+ if (x.startsWith('hd:')) return h('div', { key: 'h' + i, className: 'hd' }, x.slice(3))
6470
+ return h('div', { key: 's' + i, className: 'sp ' + x.slice(4), role: 'separator' })
6471
+ }
6472
+ const [tier, Icon, name, on, off, run, em] = x
6473
+ const kids = [h(Icon), h('span', null, name), em != null && em !== '' ? h('em', null, em) : null]
6474
+ return typeof run === 'string'
6475
+ ? h('a', { key: name, className: tier, role: 'menuitem', href: run, target: '_blank', rel: 'noopener noreferrer', onClick: () => setOpen(false) }, ...kids)
6476
+ : h('button', { key: name, className: tier + (on ? ' sel' : ''), role: 'menuitem', disabled: off, onClick: () => { setOpen(false); run() } }, ...kids)
6477
+ }),
6478
+ foot ? h('div', { className: 'ft' }, foot) : null) : null)
6479
+ }
6480
+
6481
+ // 左上屏名 ▾:换屏(画布 / 某一屏)、多尺寸、类型、打开别的页面——「在看什么、怎么看」都在这一个菜单里
6482
+ function ScreenMenu({ t, label, title, tip, src, busy, list, cur, canvas, open, setOpen, onPick, onAll, onOther, sizes, kind, onKind }) {
6483
+ const ref = useRef(null)
6484
+ useDismiss(open, setOpen, ref)
6485
+ const n = list.length
6486
+ const close = (run) => () => { setOpen(false); run() }
6487
+ return h('div', { className: 'ddp-kind ddp-where', ref },
6488
+ h('button', { className: 'ddp-addr' + (open ? ' sel' : ''), ...(tip ? tipA(tip[0], tip[1]) : { title: title || t('screensT') }), 'data-src': src || undefined, 'aria-haspopup': 'menu', 'aria-expanded': open, onClick: () => setOpen(!open) },
6489
+ h('i', { className: 'ddp-dot' + (busy ? ' wait' : '') }),
6490
+ (() => { const m = /^(.+?)( · [^·]+)$/.exec(label || ''); return m ? h('span', { className: 'lb2' }, h('b', null, m[1]), h('em', null, m[2])) : h('span', null, label) })(),
6491
+ h(I.caret)),
6492
+ open ? h('div', { className: 'ddp-menu l', role: 'menu' },
6493
+ h('div', { className: 'hd' }, n > 1 ? t('screensHd') : t('screensT')),
6494
+ h('button', { className: canvas && !(sizes && sizes.on) ? 'sel' : '', role: 'menuitemradio', 'aria-checked': canvas, onClick: close(onAll) }, h(I.canvas), h('span', null, t('canvasShort')), n > 1 ? h('em', null, t('screensAll', { n })) : null),
6495
+ h('div', { className: 'ls' }, ...list.map((x) => h('button', { key: x.key, className: !canvas && !(sizes && sizes.on) && x.key === cur ? 'sel' : '', role: 'menuitemradio', 'aria-checked': !canvas && x.key === cur, title: x.key, onClick: close(() => onPick(x.key)) },
6496
+ h(I.single), h('span', null, x.label)))),
6497
+ sizes ? h('div', { className: 'sp', role: 'separator' }) : null,
6498
+ sizes ? h('button', { className: sizes.on ? 'sel' : '', role: 'menuitemcheckbox', 'aria-checked': !!sizes.on, disabled: !!sizes.off, title: sizes.tip, onClick: close(sizes.run) }, h(I.sizes), h('span', null, t('sizesShort')), sizes.note ? h('em', null, sizes.note) : null) : null,
6499
+ onKind ? h('div', { className: 'hd' }, t('kind')) : null,
6500
+ onKind ? h('div', { className: 'ddp-kinds', role: 'group', 'aria-label': t('kind') }, ...KIND_LIST.map((k) => h('button', { key: k, role: 'menuitemradio', 'aria-checked': k === kind, className: k === kind ? 'sel' : '', title: KIND_SIZE[k].join('×'), onClick: close(() => onKind(k)) },
6501
+ h(I[k]), h('span', null, t(KIND_KEY[k]))))) : null,
6502
+ h('div', { className: 'sp', role: 'separator' }),
6503
+ h('button', { role: 'menuitem', onClick: close(onOther) }, h(I.folder), h('span', null, t('openOther')))) : null)
5524
6504
  }
5525
6505
 
5526
6506
  /** 二维码矩阵(一行一串 0 / 1)→ SVG(四周留 4 格白边) */
@@ -5553,10 +6533,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5553
6533
  seen ? h('span', { key: 'st', className: 'st ok' }, h(I.check), t('phoneSeen')) : h('span', { key: 'st', className: 'st' }, h('i'), t('phoneWait')),
5554
6534
  h('p', { key: 'fl', className: 'fine' }, t('phoneFollow')),
5555
6535
  h('div', { key: 'row', className: 'row' },
5556
- h('button', { className: 'ddp-go', onClick: () => onCopy(res.url) }, t('phoneCopy')),
5557
- h('button', { className: 'ddp-go', onClick: onStop }, t('phoneStop'))),
5558
- h('p', { key: 'auto', className: 'fine' }, t('phoneAuto')),
5559
- seen ? null : h('p', { key: 'help', className: 'fine' }, t('phoneHelp')),
6536
+ h('button', { className: 'ddp-go sec', onClick: onStop }, t('phoneStop')),
6537
+ h('button', { className: 'ddp-go', onClick: () => onCopy(res.url) }, t('phoneCopy'))),
6538
+ h('details', { key: 'more', className: 'fine', open: false },
6539
+ h('summary', null, t('phoneTrouble')),
6540
+ h('p', null, t('phoneHelp')),
6541
+ h('p', null, t('phoneAuto'))),
5560
6542
  ]
5561
6543
  }
5562
6544
  return h('div', { className: 'ddp-tokens ddp-phone', onPointerDown: (e) => e.stopPropagation() },
@@ -5652,9 +6634,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5652
6634
  w.exists
5653
6635
  ? [
5654
6636
  h('button', { key: 'o', disabled: cur, onClick: () => onOpen(w) }, t('wkOpen')),
5655
- h('button', { key: 'r', title: t('wkReveal'), onClick: () => op(w, 'reveal') }, h(I.folder)),
5656
- h('button', { key: 'z', title: t('wkZip'), onClick: () => onPack(w.dir) }, h(I.download)),
5657
- h('button', { key: 's', title: t('wkShare'), onClick: () => onShare(w.dir) }, h(I.share)),
6637
+ h('button', { key: 'r', title: t('wkReveal'), onClick: () => op(w, 'reveal') }, h(I.folder), h('span', null, t('wkRevealShort'))),
6638
+ h('button', { key: 'z', title: t('wkZip'), onClick: () => onPack(w.dir) }, h(I.download), h('span', null, t('wkZipShort'))),
6639
+ h('button', { key: 's', title: t('wkShare'), onClick: () => onShare(w.dir) }, h(I.share), h('span', null, t('wkShareShort'))),
5658
6640
  h('button', { key: 'p', title: t(w.pinned ? 'wkUnpin' : 'wkPin'), className: w.pinned ? 'on' : '', onClick: () => op(w, w.pinned ? 'unpin' : 'pin') }, '★'),
5659
6641
  ]
5660
6642
  : dirPicker() ? h('button', { key: 'l', onClick: () => relocate(w) }, t('wkRelocate')) : null,
@@ -5797,28 +6779,108 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5797
6779
  }
5798
6780
  const mkRevoked = (v, id, rev) => !!v && (v.revoked || []).some((r) => r.id === id && (!r.rev || !rev || r.rev === rev))
5799
6781
 
6782
+ // 首页顶上一条:左边名字,右边「开始 / 市场 / 我的作品 / 灵感」——设计库并进首页的页签
6783
+ const HOME_TABS = [['', 'homeTab'], ['market', 'libMarket'], ['works', 'libWorks'], ['inspire', 'libInspire']]
6784
+ function HomeBar({ t, tab, onTab, lead }) {
6785
+ // 左右方向键在几栏之间挪焦点,回车 / 空格再打开(按到哪就开哪会一路弹设计库)
6786
+ const onKey = (e) => {
6787
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return
6788
+ const bs = [...e.currentTarget.querySelectorAll('[role=tab]')]
6789
+ const i = Math.max(0, bs.indexOf(document.activeElement))
6790
+ const n = e.key === 'Home' ? 0 : e.key === 'End' ? bs.length - 1 : (i + (e.key === 'ArrowRight' ? 1 : -1) + bs.length) % bs.length
6791
+ e.preventDefault()
6792
+ if (bs[n]) bs[n].focus()
6793
+ }
6794
+ return h('header', { className: 'ddp-homebar' + (lead ? ' led' : '') },
6795
+ lead || null,
6796
+ h('div', { className: 'nm' }, h('i', { className: 'mk', 'aria-hidden': true }), h('b', null, t('title'))),
6797
+ h('div', { className: 'seg', role: 'tablist', onKeyDown: onKey }, ...HOME_TABS.map(([k, key]) => h('button', { key: k || 'home', role: 'tab', 'aria-selected': tab === k, tabIndex: tab === k ? 0 : -1, className: tab === k ? 'on' : '', onClick: () => onTab(k) }, t(key)))))
6798
+ }
6799
+
6800
+ // 首页大输入框:说一句 + 类型 + 风格 → onSend(发给模型)。没写字时 Tab 用占位里的例子
6801
+ const ASK_KINDS = ['app', 'miniprogram', 'web', 'poster', 'slides', 'tablet', 'desktop', 'email']
6802
+ const ASK_EX = { web: 'exWeb', app: 'exApp', miniprogram: 'exMini', tablet: 'exTablet', desktop: 'exDesktop', poster: 'exPoster', slides: 'exSlides', email: 'exEmail' }
6803
+ function HomeAsk({ t, onSend, onRef, plus, draft, note, autoFocus }) {
6804
+ const d0 = (draft && draft.current) || {}
6805
+ const [text, setText] = useState(d0.text || '')
6806
+ const [kind, setKind] = useState(d0.kind || '')
6807
+ const [sty, setSty] = useState(d0.sty || null)
6808
+ useEffect(() => { if (draft) draft.current = { text, kind, sty } }, [text, kind, sty])
6809
+ useEffect(() => { if (autoFocus && ta.current) ta.current.focus({ preventScroll: true }) }, [])
6810
+ const [pop, setPop] = useState('') // 'plus' | 'style'
6811
+ const ta = useRef(null)
6812
+ const card = useRef(null)
6813
+ useDismiss(pop === 'style', () => setPop(''), card)
6814
+ const en = t('lang') === 'en'
6815
+ const ex = t(ASK_EX[kind] || 'exAuto')
6816
+ const back = () => { if (ta.current) ta.current.focus() }
6817
+ const go = () => {
6818
+ const s = text.trim()
6819
+ if (!s) return back()
6820
+ if (onSend({ text: s, kind, style: sty }) !== false) setText('')
6821
+ }
6822
+ const onKey = (e) => {
6823
+ if (e.nativeEvent && e.nativeEvent.isComposing) return
6824
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); go() } else if (e.key === 'Tab' && !e.shiftKey && !text) { e.preventDefault(); setText(ex) }
6825
+ }
6826
+ return h('div', { className: 'ddp-askwrap' },
6827
+ h('div', { className: 'ddp-ask', ref: card },
6828
+ h('textarea', { ref: ta, className: 'ddp-askta', rows: 2, value: text, placeholder: t('askPh', { ex }), 'aria-label': t('emptyHead'), onChange: (e) => setText(e.target.value), onKeyDown: onKey }),
6829
+ h('div', { className: 'row' },
6830
+ h(DropMenu, { t, cls: 'ddp-askplus', tip: [t('askMore'), t('askMoreD')], icon: I.plus, open: pop === 'plus', setOpen: (v) => setPop(v ? 'plus' : ''), items: plus }),
6831
+ h('button', { className: 'chip ref', ...tipA(t('askRef'), t('askRefD')), onClick: onRef }, h(I.image), h('span', null, t('askRef'))),
6832
+ h('button', { className: 'chip' + (sty ? ' on' : ''), ...tipA(sty ? t('askStyle', { name: en ? sty.en : sty.zh }) : t('askStyleAuto'), t('askStyleD')), 'aria-haspopup': 'dialog', 'aria-expanded': pop === 'style', onClick: () => setPop(pop === 'style' ? '' : 'style') },
6833
+ sty ? h('i', { className: 'sw', style: { background: `linear-gradient(135deg,${sty.c.primary} 0 50%,${sty.c.accent} 50%)` } }) : h(I.palette),
6834
+ h('span', null, sty ? t('askStyle', { name: en ? sty.en : sty.zh }) : t('askStyleAuto')), h(I.caret)),
6835
+ !text ? h('button', { className: 'tabhint', tabIndex: -1, onClick: () => { setText(ex); back() } }, h('kbd', null, 'Tab'), t('askTab')) : null,
6836
+ h('button', { className: 'send', disabled: !text.trim(), ...tipA(t('askSend'), '', '↵'), onClick: go }, h(I.send))),
6837
+ pop === 'style' ? h('div', { className: 'ddp-askpop', role: 'dialog', 'aria-label': t('askStylePick') },
6838
+ h('div', { className: 'hd' }, h('b', null, t('askStylePick')), h('button', { className: 'lk', onClick: () => { setSty(null); setPop(''); back() } }, t('askStyleAutoBtn'))),
6839
+ h(StyleCards, { t, onPick: (x) => { setSty(x); setPop(''); back() } })) : null),
6840
+ note ? h('p', { className: 'ddp-asknote' }, h(I.info), h('span', null, note)) : null,
6841
+ h('div', { className: 'ddp-askkinds', role: 'radiogroup', 'aria-label': t('kind') }, ...ASK_KINDS.map((k) => h('button', { key: k, role: 'radio', 'aria-checked': kind === k, className: kind === k ? 'on' : '', onClick: () => setKind(kind === k ? '' : k) },
6842
+ h(I[k]), h('span', null, t(KIND_KEY[k]))))))
6843
+ }
6844
+
6845
+ // 首页「接着做」:最近的三套(找不到文件夹的不放),点了直接打开
6846
+ function HomeWorks({ t, onOpen, onAll, cur, onBack }) {
6847
+ const norm = (x) => String(x || '').replace(/[\\/]+$/, '')
6848
+ const [list, setList] = useState(null)
6849
+ useEffect(() => {
6850
+ let dead = false
6851
+ getJson('works').then((d) => { if (!dead) setList(d.works || []) }, () => { if (!dead) setList([]) })
6852
+ return () => { dead = true }
6853
+ }, [])
6854
+ const ws = (list || []).filter((w) => w.exists).slice(0, 3)
6855
+ if (!ws.length) return null
6856
+ return h('section', { className: 'ddp-homesec' },
6857
+ h('div', { className: 'hd' }, h('b', null, t('homeRecent')), h('button', { className: 'lk', onClick: onAll }, t('homeRecentAll'))),
6858
+ h('div', { className: 'ddp-recent' }, ...ws.map((w) => { const here = !!cur && norm(w.dir) === norm(cur); return h('button', { key: w.id, className: 'wk' + (here ? ' here' : ''), title: w.dir, 'aria-current': here ? 'true' : undefined, onClick: () => (here && onBack ? onBack() : onOpen(w)) },
6859
+ here ? h('em', { className: 'now' }, t('wkHere')) : null,
6860
+ h('span', { className: 'im' }, w.thumb ? h('img', { src: `${API}workthumb?id=${w.id}&v=${w.thumb}`, alt: '', loading: 'lazy', onError: (e) => { e.currentTarget.style.visibility = 'hidden' } }) : null, h(I[w.kind] || I.web)),
6861
+ h('span', { className: 'tx' }, h('b', null, w.title), h('span', null, [t(KIND_KEY[w.kind] || 'kindWeb'), t('wkScreens', { n: w.screens }), agoText(t, w.updated)].join(' · ')))) })))
6862
+ }
6863
+
5800
6864
  function TplStart({ t, onOpen, onMore }) {
5801
6865
  const v = useMarketView()
5802
- const list = v && v.designs ? [...v.designs].sort((a, b) => (b.featured - a.featured) || (Date.parse(b.updated) || 0) - (Date.parse(a.updated) || 0)).slice(0, 6) : []
6866
+ const list = v && v.designs ? [...v.designs].sort((a, b) => (b.featured - a.featured) || String(a.id).localeCompare(String(b.id))).slice(0, 6) : [] // 按名字排:离线目录换成线上目录时不跳
5803
6867
  if (!list.length) return null
5804
- return h('div', { className: 'ddp-tplstart' },
6868
+ return h('section', { className: 'ddp-homesec ddp-tplstart' },
5805
6869
  h('div', { className: 'hd' }, h('b', null, t('tplStartHead')), h('button', { className: 'lk', onClick: onMore }, t('tplStartMore'))),
5806
6870
  h('div', { className: 'row' }, ...list.map((e) => h('button', { key: e.id, className: 'tp', title: mkPick(t, e.description) || mkPick(t, e.title), onClick: () => onOpen(e.id) },
5807
6871
  h('span', { className: 'im' }, e.thumb ? h('img', { src: `${API}mthumb?id=${encodeURIComponent(e.id)}&r=${encodeURIComponent(e.rev || '')}`, alt: '', loading: 'lazy' }) : h(I[e.kind] || I.web)),
5808
- h('span', { className: 'nm' }, mkPick(t, e.title))))))
6872
+ h('span', { className: 'tx' }, h('b', null, mkPick(t, e.title)), h('span', null, [t(KIND_KEY[e.kind] || 'kindWeb'), e.screens && e.screens.length ? t('wkScreens', { n: e.screens.length }) : ''].filter(Boolean).join(' · ')))))))
5809
6873
  }
5810
6874
 
5811
- function MarketTab({ t, focus, onUseTpl, onUseStyle, onSay }) {
6875
+ function MarketTab({ t, cur, setCur, onUseTpl, onUseStyle, onSay }) {
5812
6876
  const [d, setD] = useState(null)
5813
6877
  const [err, setErr] = useState('')
5814
6878
  const [typePick, setType] = useState('template')
5815
6879
  const [kind, setKind] = useState('')
5816
6880
  const [q, setQ] = useState('')
5817
6881
  const [sort, setSort] = useState('featured')
5818
- const [cur, setCur] = useState(focus || '')
5819
6882
  const [busy, setBusy] = useState(false)
5820
6883
  const mine = useInspire() // 收藏过的风格打勾(按市场 id 认)
5821
- useEffect(() => { if (focus) setCur(focus) }, [focus])
5822
6884
  const load = (refresh) => {
5823
6885
  if (refresh) setBusy(true)
5824
6886
  return loadMarket(refresh).then((x) => { setD(x); setErr('') }, (e) => setErr(e.message)).finally(() => setBusy(false))
@@ -5858,27 +6920,25 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5858
6920
  .sort((a, b) => (sort === 'featured' ? (b.featured - a.featured) || time(b.updated) - time(a.updated) : sort === 'new' ? time(b.added) - time(a.added) : time(b.updated) - time(a.updated)))
5859
6921
  const kinds = MK_KINDS.filter((k) => d.designs.some((e) => e.kind === k))
5860
6922
  const card = (e) => {
5861
- const by = e.author && (e.author.name || e.author.login)
5862
- const meta = [e.type === 'template' ? t(KIND_KEY[e.kind] || 'kindWeb') : '', e.screens && e.screens.length ? t('wkScreens', { n: e.screens.length }) : '', by ? '@' + by : ''].filter(Boolean).join(' · ')
6923
+ const by = e.author && !/^orderg-x$/i.test(e.author.login || '') ? e.author.name || e.author.login : '' // 官方做的不写作者(每张都一样)
6924
+ const meta = [e.type === 'template' ? t(KIND_KEY[e.kind] || 'kindWeb') : '', e.screens && e.screens.length ? t('wkScreens', { n: e.screens.length }) : '', t(e.license === 'CC-BY-4.0' ? 'mkUseBY' : 'mkUse0'), by ? '@' + by : ''].filter(Boolean).join(' · ')
5863
6925
  return h('div', { key: e.id, className: 'ddp-work ddp-mk' },
5864
6926
  h('button', { className: 'th', title: mkPick(t, e.description) || mkPick(t, e.title), onClick: () => setCur(e.id) },
5865
6927
  e.thumb ? h('img', { src: `${API}mthumb?id=${encodeURIComponent(e.id)}&r=${encodeURIComponent(e.rev || '')}`, alt: '', loading: 'lazy', onError: (ev) => { ev.currentTarget.style.visibility = 'hidden' } }) : null,
5866
6928
  h('span', { className: 'ic' }, e.palette && Object.keys(e.palette).length ? h('span', { className: 'pal' }, ...Object.values(e.palette).slice(0, 4).map((c, i) => h('i', { key: i, style: { background: c } }))) : h(I[e.kind] || I.web)),
5867
- e.featured ? h('em', null, t('mkSortFeatured')) : null),
5868
- h('div', { className: 'nm' }, h('b', { title: mkPick(t, e.title) }, mkPick(t, e.title)),
5869
- e.ai && e.ai.generated ? h('span', { className: 'bdg' }, t('mkAi')) : null,
5870
- h('span', { className: 'bdg' }, e.license === 'CC-BY-4.0' ? 'CC BY' : 'CC0')),
5871
- h('div', { className: 'mt' }, meta))
6929
+ e.featured && sort !== 'featured' ? h('em', null, t('mkSortFeatured')) : null), // 按精选排时精选的本来就在前面,不再每张标
6930
+ h('div', { className: 'nm' }, h('b', { title: mkPick(t, e.title) }, mkPick(t, e.title))),
6931
+ h('div', { className: 'mt', title: meta }, meta))
5872
6932
  }
5873
6933
  return h('div', { className: 'ddp-market' },
5874
6934
  h('div', { className: 'ddp-mkbar' },
5875
- types.length > 1 ? h('div', { className: 'seg' }, ...types.map(([k, label, n]) => h('button', { key: k, className: type === k ? 'on' : '', onClick: () => { setType(k); setKind('') } }, `${label} ${n}`))) : null,
6935
+ types.length > 1 ? h('div', { className: 'tabs', role: 'tablist' }, ...types.map(([k, label, n]) => h('button', { key: k, role: 'tab', 'aria-selected': type === k, className: type === k ? 'on' : '', onClick: () => { setType(k); setKind('') } }, label, h('em', null, n)))) : null,
5876
6936
  h('input', { className: 'q', type: 'search', value: q, placeholder: t('mkSearch'), 'aria-label': t('mkSearch'), onChange: (e) => setQ(e.target.value) }),
5877
- h('select', { value: sort, 'aria-label': t('mkSortFeatured'), onChange: (e) => setSort(e.target.value) },
5878
- h('option', { value: 'featured' }, t('mkSortFeatured')), h('option', { value: 'new' }, t('mkSortNew')), h('option', { value: 'updated' }, t('mkSortUpdated')))),
5879
- type === 'template' && kinds.length > 1 ? h('div', { className: 'ddp-mkkinds' },
5880
- h('button', { className: kind === '' ? 'on' : '', onClick: () => setKind('') }, t('mkAllKinds')),
5881
- ...kinds.map((k) => h('button', { key: k, className: kind === k ? 'on' : '', onClick: () => setKind(k) }, t(KIND_KEY[k])))) : null,
6937
+ type !== 'style' ? h('div', { className: 'sort', role: 'group', 'aria-label': t('mkSortT') }, ...[['featured', 'mkSortFeatured'], ['new', 'mkSortNew'], ['updated', 'mkSortUpdated']].map(([k, label]) => h('button', { key: k, className: sort === k ? 'on' : '', 'aria-pressed': sort === k, onClick: () => setSort(k) }, t(label)))) : null),
6938
+ // 类型一排横着滑;设计系统、风格不分类型:灰着,不收起来(下面的卡片不跳)
6939
+ kinds.length > 1 ? h('div', { className: 'ddp-mkkinds' },
6940
+ kinds.length > 1 ? [h('button', { key: '', className: kind === '' && type === 'template' ? 'on' : '', disabled: type !== 'template', onClick: () => setKind('') }, t('mkAllKinds')),
6941
+ ...kinds.map((k) => h('button', { key: k, className: kind === k && type === 'template' ? 'on' : '', disabled: type !== 'template', onClick: () => setKind(k) }, t(KIND_KEY[k])))] : null) : null,
5882
6942
  d.off || d.error || d.needUpdate ? h('p', { className: 'ddp-libtip warn' }, d.needUpdate ? t('mkNeedUpdate') : d.off ? t('mkOff') : t('mkOffline')) : null,
5883
6943
  !list.length ? h('div', { className: 'ddp-libnone' }, t('mkNoResult'))
5884
6944
  : type === 'style'
@@ -5947,7 +7007,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5947
7007
  : shots && shots.length ? h('img', { key: shots[i], src: `${API}mshot?id=${encodeURIComponent(e.id)}&f=${encodeURIComponent(shots[i])}&r=${encodeURIComponent(e.rev || '')}`, alt: (screens[i] && mkPick(t, screens[i].title)) || '' })
5948
7008
  : e.thumb ? h('img', { src: `${API}mthumb?id=${encodeURIComponent(e.id)}&r=${encodeURIComponent(e.rev || '')}`, alt: '' }) : null
5949
7009
  return h('div', { className: 'ddp-mkd' },
5950
- h('button', { className: 'lk back', onClick: onBack }, '← ', t('mkBack')),
5951
7010
  h('h3', null, mkPick(t, e.title)),
5952
7011
  h('p', { className: 'mt' }, [isTpl ? t(KIND_KEY[e.kind] || 'kindWeb') : t('mkTypeSys'), screens.length ? t('wkScreens', { n: screens.length }) : '', by ? t('mkBy', { who: by }) : '', e.ai && e.ai.generated ? t('mkAi') : ''].filter(Boolean).join(' · ')),
5953
7012
  mkPick(t, e.description) ? h('p', null, mkPick(t, e.description)) : null,
@@ -5967,18 +7026,26 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5967
7026
  e.derivedFrom ? h('li', null, t('mkDerived', { x: String(e.derivedFrom) })) : null),
5968
7027
  h('p', { className: 'ddp-libfoot' },
5969
7028
  h('a', { href: mkUrl(e), target: '_blank', rel: 'noopener noreferrer' }, t('mkSource')), ' · ',
5970
- h('a', { href: report, target: '_blank', rel: 'noopener noreferrer' }, t('mkReport')), ' · ', h('code', null, e.id)))
7029
+ h('a', { href: report, target: '_blank', rel: 'noopener noreferrer', title: e.id }, t('mkReport'))))
5971
7030
  }
5972
7031
 
5973
- function LibraryView({ t, tab, setTab, focus, hasDesign, curRoot, onClose, onOpen, onPack, onShare, onSaveStyle, onUseStyle, onUseTpl, onRef, onExtract, onMarket, onSay }) {
7032
+ function LibraryView({ t, tab, setTab, focus, hasDesign, homey, curRoot, onClose, onOpen, onPack, onShare, onSaveStyle, onUseStyle, onUseTpl, onRef, onExtract, onMarket, onSay }) {
5974
7033
  const tabs = [['market', t('libMarket')], ['works', t('libWorks')], ['inspire', t('libInspire')]]
5975
- return h('div', { className: 'ddp-lib', onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose() } } },
5976
- h('header', null,
5977
- h('button', { className: 'ddp-btn', title: t(hasDesign ? 'libBack' : 'libClose'), 'aria-label': t(hasDesign ? 'libBack' : 'libClose'), onClick: onClose }, h(I.back)),
5978
- h('b', null, t('library')),
5979
- h('div', { className: 'seg', role: 'tablist' }, ...tabs.map(([k, label]) => h('button', { key: k, role: 'tab', 'aria-selected': tab === k, className: tab === k ? 'on' : '', onClick: () => setTab(k) }, label)))),
7034
+ const [mkCur, setMkCur] = useState(focus || '') // 市场里点开的那一套(详情页)
7035
+ useEffect(() => { if (focus) setMkCur(focus) }, [focus])
7036
+ const inDetail = tab === 'market' && !!mkCur
7037
+ // 只有一个返回:在详情页回列表,不然回画布 / 空白页
7038
+ const back = () => (inDetail ? setMkCur('') : onClose())
7039
+ const backTip = inDetail ? t('mkBack') : t(hasDesign ? 'libBack' : 'libClose')
7040
+ return h('div', { className: 'ddp-lib', onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => { if (e.key === 'Escape') { e.stopPropagation(); back() } } },
7041
+ homey
7042
+ ? h(HomeBar, { t, tab, onTab: (k) => { if (!k) onClose(); else { setTab(k); if (k === 'market' && tab === 'market') setMkCur('') } }, lead: inDetail ? h('button', { className: 'ddp-btn', title: backTip, 'aria-label': backTip, onClick: back }, h(I.back)) : null })
7043
+ : h('header', null,
7044
+ h('button', { className: 'ddp-btn', title: backTip, 'aria-label': backTip, onClick: back }, h(I.back)),
7045
+ h('b', null, t('library')),
7046
+ h('div', { className: 'seg', role: 'tablist' }, ...tabs.map(([k, label]) => h('button', { key: k, role: 'tab', 'aria-selected': tab === k, className: tab === k ? 'on' : '', onClick: () => { setTab(k); if (k === 'market' && tab === 'market') setMkCur('') } }, label)))),
5980
7047
  h('div', { className: 'bd' },
5981
- tab === 'market' ? h(MarketTab, { t, focus, onUseTpl, onUseStyle, onSay })
7048
+ tab === 'market' ? h(MarketTab, { t, cur: mkCur, setCur: setMkCur, onUseTpl, onUseStyle, onSay })
5982
7049
  : tab === 'works' ? h(WorksTab, { t, curRoot, onOpen, onPack, onShare, onMarket, onSay })
5983
7050
  : h(InspireTab, { t, hasDesign, onSaveStyle, onUseStyle, onRef, onExtract, onMarket, onSay })))
5984
7051
  }
@@ -6031,7 +7098,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6031
7098
  const SHARE_AGREE = [['own', 'shAgreeOwn'], ['brand', 'shAgreeBrand'], ['private', 'shAgreePrivate'], ['rules', 'shAgreeRules']]
6032
7099
 
6033
7100
  /** 分享到设计市场:填名字、介绍、授权,勾四个确认 → 按分享的规矩查(个人信息硬拦)→ 打 zip → 打开预填好的 GitHub 投稿页 */
6034
- function SharePanel({ t, sid, root, onFix, onSay, onClose }) {
7101
+ function SharePanel({ t, sid, root, brand, onFix, onSay, onClose }) {
6035
7102
  const L = t('lang') === 'en' ? 'en' : 'zh'
6036
7103
  const [info, setInfo] = useState(null)
6037
7104
  const [title, setTitle] = useState('')
@@ -6048,7 +7115,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6048
7115
  post('pack', { session: sid, dir: root, info: true, lang: L }).then((d) => {
6049
7116
  if (!on) return
6050
7117
  setInfo(d)
6051
- setTitle(d.title || '')
7118
+ setTitle(d.title && d.title !== baseName(root) ? d.title : brand || d.title || '')
6052
7119
  setDesc(d.description || '')
6053
7120
  setLic(d.license === 'CC-BY-4.0' ? 'CC-BY-4.0' : 'CC0-1.0')
6054
7121
  }, (e) => { if (on) setInfo({ error: e.message }) })
@@ -6069,6 +7136,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6069
7136
  const done = res && res.ok && res.share
6070
7137
  return h('div', { className: 'ddp-tokens ddp-pack ddp-share', onPointerDown: (e) => e.stopPropagation() },
6071
7138
  h('header', null, h(I.share), h('b', null, t('shTitle')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
7139
+ h('p', { className: 'tip gh' }, h(I.info), h('span', null, t('shGh'))),
6072
7140
  h('p', { className: 'tip' }, t('shTip')),
6073
7141
  h('div', { className: 'bd' },
6074
7142
  root ? h('p', { className: 'which', title: root }, h(I.folder), h('span', null, baseName(root))) : h('div', { className: 'none' }, t('pkNoDesign')),
@@ -6125,6 +7193,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6125
7193
  const [cam, setCam] = useState(null)
6126
7194
  const [anim, setAnim] = useState(false)
6127
7195
  const [drag, setDrag] = useState(false)
7196
+ const [zoomMenu, setZoomMenu] = useState(false)
6128
7197
  const auto = useRef(true)
6129
7198
  const camRef = useRef(cam)
6130
7199
  camRef.current = cam
@@ -6135,8 +7204,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6135
7204
  // 多尺寸:每块自己的类型和大小(items[i] = { kind, size })
6136
7205
  const items = props.items || null
6137
7206
  const boxes = items ? items.map((it) => deviceBox(it.kind, it.size)) : null
6138
- const L = box.w > 0 && box.h > 0 ? (boxes ? layoutRow(boxes, box.w, box.h) : layoutGrid(n, dw, dh, box.w, box.h)) : null
6139
- const layoutKey = L ? [n, L.cols, dw, dh, box.w, box.h, boxes ? boxes.join(';') : ''].join('|') : ''
7207
+ // 上下各有一排浮块:排的时候只用中间那块高度,镜头整体往下挪 rt
7208
+ const rt = props.top || 0
7209
+ const bh = Math.max(0, box.h - rt - (props.bottom || 0))
7210
+ const L = box.w > 0 && bh > 0 ? (boxes ? layoutRow(boxes, box.w, bh) : layoutGrid(n, dw, dh, box.w, bh)) : null
7211
+ // 右边开着面板(inset 像素被挡着):排法不变,镜头只用左边看得见的那块(太窄就不让,照旧整块)
7212
+ const ins = props.inset && box.w - props.inset >= 240 ? props.inset : 0
7213
+ const vw = box.w - ins
7214
+ const layoutKey = L ? [n, L.cols, dw, dh, box.w, box.h, rt, bh, ins, boxes ? boxes.join(';') : ''].join('|') : ''
7215
+ const fitAt = () => { const c = fitCam(L, vw, bh); return { ...c, y: c.y + rt } }
6140
7216
 
6141
7217
  useEffect(() => {
6142
7218
  const el = ref.current
@@ -6154,16 +7230,16 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6154
7230
  if (!L) return
6155
7231
  if (auto.current || !camRef.current) {
6156
7232
  setAnim(!!camRef.current)
6157
- setCam(fitCam(L, box.w, box.h))
6158
- }
7233
+ setCam(fitAt())
7234
+ } else if (ins && props.sel) showKey(props.sel) // 自己挪过镜头:只把选中的那屏挪出来,别被面板挡着
6159
7235
  }, [layoutKey])
6160
7236
 
6161
7237
  // 让某一屏露出来(体检里点了一条):整屏不在看得见的地方就把镜头挪过去;右边 inset 像素被面板挡着
6162
7238
  const rv = props.reveal
6163
- useEffect(() => {
7239
+ function showKey(key) {
6164
7240
  const c = camRef.current
6165
- if (!rv || !L || !c) return
6166
- const i = screens.findIndex((x) => x.key === rv.key)
7241
+ if (!key || !L || !c) return
7242
+ const i = screens.findIndex((x) => x.key === key)
6167
7243
  if (i < 0) return
6168
7244
  const [cw, ch] = boxes ? boxes[i] : [dw, dh]
6169
7245
  const vw = Math.max(120, box.w - (props.inset || 0))
@@ -6174,12 +7250,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6174
7250
  let x = c.x
6175
7251
  let y = c.y
6176
7252
  if (x0 < 8 || x0 + w > vw - 8) x += (w > vw - 16 ? 8 : (vw - w) / 2) - x0
6177
- if (y0 < LAB || y0 + hh > box.h - 8) y += (hh > box.h - LAB - 8 ? LAB : (box.h - hh) / 2) - y0
7253
+ if (y0 < rt + LAB || y0 + hh > rt + bh - 8) y += (hh > bh - LAB - 8 ? rt + LAB : rt + (bh - hh) / 2) - y0
6178
7254
  if (x === c.x && y === c.y) return
6179
7255
  auto.current = false
6180
7256
  setAnim(true)
6181
7257
  setCam({ ...c, x, y })
6182
- }, [rv && rv.n])
7258
+ }
7259
+ useEffect(() => { if (rv) showKey(rv.key) }, [rv && rv.n])
6183
7260
 
6184
7261
  const zoomAt = (factor, px, py, smooth) => {
6185
7262
  const c = camRef.current
@@ -6199,7 +7276,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6199
7276
  if (!c) return
6200
7277
  if (e.ctrlKey || e.metaKey) {
6201
7278
  const r = el.getBoundingClientRect()
6202
- zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top, false)
7279
+ // 触控板捏合每次只给几个像素;鼠标滚轮(Windows 上 Ctrl + 滚轮)一格就是 100 上下,不收住一格放大近 3 倍
7280
+ const dy = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY
7281
+ zoomAt(Math.exp(-Math.max(-25, Math.min(25, dy)) * 0.01), e.clientX - r.left, e.clientY - r.top, false)
6203
7282
  } else {
6204
7283
  auto.current = false
6205
7284
  setAnim(false)
@@ -6247,7 +7326,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6247
7326
  if (!L) return
6248
7327
  auto.current = true
6249
7328
  setAnim(true)
6250
- setCam(fitCam(L, box.w, box.h))
7329
+ setCam(fitAt())
6251
7330
  }
6252
7331
 
6253
7332
  const z = cam ? cam.z : 1
@@ -6266,7 +7345,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6266
7345
  className: 'ddp-cell' + (on ? ' sel' : ''),
6267
7346
  style: { left: L.pos[i].x, top: L.pos[i].y, width: cw, height: ch },
6268
7347
  },
6269
- lab(sc.key, [h('span', { key: 's' }, labelOf(sc)), !picking && props.badge ? props.badge(sc) : null, on && !picking && props.actions ? h('span', { key: 'a', className: 'ddp-act' }, ...props.actions(sc)) : null], cw),
7348
+ lab(sc.key, [props.tag ? props.tag(sc) : null, h('span', { key: 's' }, labelOf(sc)), !picking && props.badge ? props.badge(sc) : null, !picking && props.actions ? h('span', { key: 'a', className: 'ddp-act' }, ...props.actions(sc)) : null], cw),
6270
7349
  props.render(sc, z, i),
6271
7350
  picking ? null : h('div', { className: 'ddp-shield', 'data-key': sc.key, title: items ? '' : t('openScreen'), style: { borderRadius: fr.r } })))
6272
7351
  })
@@ -6274,7 +7353,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6274
7353
  const p = L.pos[n - 1]
6275
7354
  cells.push(h('div', { key: '__ghost', className: 'ddp-cell ghost', style: { left: p.x, top: p.y, width: dw, height: dh } },
6276
7355
  lab('', [h('i', { key: 'i', className: 'sp' }), h('span', { key: 's' }, t('generating'))]),
6277
- h(Device, { kind, w: size[0], h: size[1], label: '' }, h(Skeleton, {}))))
7356
+ h(Device, { kind, w: size[0], h: size[1], label: '' },
7357
+ h(GhostDraw, { key: 'gd', t, kind, pulse: props.pulse || 0 }),
7358
+ props.draft ? h(DraftFrame, { key: 'df', url: props.draft, z, lang: t('lang'), onEnd: props.onDraftEnd }) : null)))
6278
7359
  }
6279
7360
  }
6280
7361
  let flowSvg = null
@@ -6296,11 +7377,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6296
7377
  className: 'ddp-world' + (anim ? ' anim' : ''),
6297
7378
  style: { transform: `translate(${cam.x}px, ${cam.y}px) scale(${z})`, '--z': z },
6298
7379
  }, ...cells, flowSvg) : null,
7380
+ // 右下缩放:「43% ▾」,点开是放大 / 缩小 / 适合画布 / 实际大小
6299
7381
  h('div', { className: 'ddp-zoom' },
6300
- h('button', { className: 'ddp-btn', title: t('zoomOut'), onClick: () => center(1 / 1.25) }, h(I.minus)),
6301
- h('b', null, Math.round(z * 100) + '%'),
6302
- h('button', { className: 'ddp-btn', title: t('zoomIn'), onClick: () => center(1.25) }, h(I.plus)),
6303
- h('button', { className: 'ddp-btn', title: t('fit'), onClick: fit }, h(I.fit))))
7382
+ h(DropMenu, { t, tip: [t('zoomT'), t('dZoom', { mod: MOD.replace(/\+$/, '') })], label: Math.round(z * 100) + '%', caret: true, open: zoomMenu, setOpen: setZoomMenu, items: [
7383
+ ['', I.plus, t('zoomIn'), false, z >= ZMAX, () => center(1.25)],
7384
+ ['', I.minus, t('zoomOut'), false, z <= ZMIN, () => center(1 / 1.25)],
7385
+ 'sep',
7386
+ ['', I.fit, t('fit'), false, false, fit],
7387
+ ['', I.single, t('zoomReal'), Math.abs(z - 1) < 0.005, false, () => center(1 / z)],
7388
+ ] })))
6304
7389
  }
6305
7390
 
6306
7391
  // ── 右侧栏标签页 ─────────────────────────────────────────────────────────
@@ -6317,7 +7402,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6317
7402
  const [styleEdit, setStyleEdit] = useState(false) // 调样式:点元素、拖滑块,写回它的 style 属性
6318
7403
  const [sty, setSty] = useState(null) // 调样式选中的元素:{ key, url, name, selector, text, loc, base, vars, gradient, layout, live }
6319
7404
  const [editing, setEditing] = useState(false)
6320
- const [menu, setMenu] = useState(false)
6321
7405
  const [toast, setToast] = useState('')
6322
7406
  const [flash, setFlash] = useState('')
6323
7407
  const [reloadSeq, setReloadSeq] = useState(0)
@@ -6326,14 +7410,19 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6326
7410
  const setTop = (k, dk) => setTops((cur) => (cur[k] === dk ? cur : { ...cur, [k]: dk }))
6327
7411
  const [sel, setSel] = useState('')
6328
7412
  const [panel, setPanel] = useState(false)
6329
- const [styles, setStyles] = useState(false) // 换风格面板
7413
+ const [lookTab, setLookTab] = useState('set') // 外观面板开在哪一页:set 整套 / style 换风格 / brand 品牌
7414
+ const [tv, setTv] = useState({}) // 外观面板里直接改的设计变量:名字 → 值(还没写进文件)
6330
7415
  const [dropping, setDropping] = useState('') // 正拖着文件进来:dragKind 给的种类
6331
7416
  const [dropSoft, setDropSoft] = useState(false) // 拖动在预览页面里:蒙层只是提示、不挡事件(松手由页面转告)
6332
7417
  const softTimer = useRef(0)
6333
7418
  const refInput = useRef(null)
6334
7419
  const pageInput = useRef(null)
7420
+ const homeRef = useRef(null)
7421
+ const homeKind = useRef('') // 首页发出去时选的类型:第一屏出来前的占位按它画
6335
7422
  const [exMenu, setExMenu] = useState(false)
6336
- const [more, setMore] = useState(false) // 窄的时候收起来的几个按钮
7423
+ const [more, setMore] = useState(false) // ⋯ 菜单(窄的时候收起来的几个按钮也在这里)
7424
+ const [scrMenu, setScrMenu] = useState(false)
7425
+ const brandMem = useRef({}) // 目录 → 产品名(画布上认出来的),单页时去掉标题里的它
6337
7426
  const [hist, setHist] = useState(false) // 版本面板
6338
7427
  const [versions, setVersions] = useState([])
6339
7428
  const [ver, setVer] = useState(null) // 正在看的旧版本
@@ -6349,6 +7438,27 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6349
7438
  const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
6350
7439
  const [pane, setPane] = useState('') // 右边的面板:tweaks 调节滑块、systems 品牌设计系统、handoff 转成代码、pack 打成 zip、zip 设计包没过检查
6351
7440
  const [lib, setLib] = useState('') // 设计库:'' 关着;开着时是哪一栏(market / works / inspire)
7441
+ // 开着设计时叠上来的首页:'' 没开 / 'on' 开着 / 'off' 正在收起(放完收起动画再拿掉)
7442
+ const [homeAt, setHomeAt] = useState('')
7443
+ const homeDraft = useRef({ text: '', kind: '', sty: null }) // 首页输入框里写了一半的:收起再打开还在
7444
+ const homeOpen = () => { setLib(''); setLibFocus(''); setHomeAt('on') }
7445
+ const homeClose = () => {
7446
+ setHomeAt((c) => (c === 'on' ? 'off' : c))
7447
+ setTimeout(() => setHomeAt((c) => (c === 'off' ? '' : c)), reduced() ? 0 : 190)
7448
+ }
7449
+ const homeAway = () => { if (homeAt === 'on') homeClose() } // 从首页打开了设计、进了打包面板:首页收起,别挡着
7450
+ // 首页 / 设计库叠在上面时,Esc 只归它们:底下的调样式、改字、点选、试用别跟着退(没存的改动会丢)
7451
+ const overlayUp = () => { try { return !!document.querySelector('.ddp-home.over, .ddp-libwrap') } catch { return false } }
7452
+ useEffect(() => {
7453
+ if (homeAt !== 'on') return undefined
7454
+ const esc = (e) => {
7455
+ if (e.key !== 'Escape' || e.defaultPrevented) return
7456
+ if (document.querySelector('.ddp-home.over [aria-expanded="true"], .ddp-libwrap')) return // 先收菜单、设计库
7457
+ homeClose()
7458
+ }
7459
+ window.addEventListener('keydown', esc)
7460
+ return () => window.removeEventListener('keydown', esc)
7461
+ }, [homeAt])
6352
7462
  const [libFocus, setLibFocus] = useState('') // 从空白页「从模板开始」点进来的那一套
6353
7463
  const libDefault = () => (marketStore.has ? 'market' : 'works')
6354
7464
  // 市场还没读过(刚开页面):等读完(本地缓存,很快)再定开哪一页
@@ -6387,6 +7497,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6387
7497
  // 这一轮开始时已有的屏记下来:这轮里出了新的一屏,就在画布末尾留个「生成中…」等下一屏;
6388
7498
  // 一阵子(GHOST_MS)没再出新屏多半是在改了,占位撤掉;正在写一个还没有的 HTML 时一定留着
6389
7499
  const marks = useRef({ site: '', born: new Map(), built: new Set(), turnKeys: null, lastNew: 0 })
7500
+ const draftedR = useRef(new Map()) // 边写边看过的屏 → 什么时候:真文件出来时不再播一遍生成动画
7501
+ const pulseR = useRef({ key: '', lines: 0, n: 0 }) // 模型真有动静就加一(占位里的鼠标按它动)
7502
+ const singleDraftR = useRef('') // 单屏里正在看着写的那屏:写完换过去看它
7503
+ const playR = useRef({ url: '', key: '', hold: 0, end: false, meta: '', lump: false }) // 在放的草稿(见下面 dr)
6390
7504
  const M = marks.current
6391
7505
  if (M.site !== site) {
6392
7506
  M.site = site
@@ -6402,13 +7516,27 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6402
7516
  M.born.set(sc.key, busy)
6403
7517
  if (busy) M.lastNew = Date.now()
6404
7518
  }
6405
- const isBorn = (key) => !!M.born.get(key) && !M.built.has(key)
7519
+ const isBorn = (key) => !!M.born.get(key) && !M.built.has(key) && !(Date.now() - (draftedR.current.get(key) || 0) < 180000)
7520
+ // 状态条从哪一刻开始计时:服务端说这一轮开始多久了(右侧栏中途才打开也对得上)
7521
+ const turnT = useRef({ on: false, at: 0 })
7522
+ if (busy && !turnT.current.on) turnT.current = { on: true, at: Date.now() - Math.max(0, Number(st && st.turnFor) || 0) }
7523
+ else if (!busy && turnT.current.on) turnT.current = { on: false, at: 0 }
7524
+ // 开始/结束生成时单屏的设备要让位(给状态条腾一条):这一下缓过去,别一跳
7525
+ const [ease, setEase] = useState(false)
7526
+ const busySeen = useRef(busy)
7527
+ useEffect(() => {
7528
+ if (busySeen.current === busy) return
7529
+ busySeen.current = busy
7530
+ setEase(true)
7531
+ const id = setTimeout(() => setEase(false), 520)
7532
+ return () => clearTimeout(id)
7533
+ }, [busy])
6406
7534
 
6407
7535
  const infoOf = (key) => info[site + '|' + key] || null
6408
7536
  const ik = (sc) => (sc.v ? 'v' + sc.v + '|' : '') + sc.key // 旧版本的屏单独记标题
6409
7537
  const focus = (st && st.focus) || ''
6410
7538
  const main = infoOf(focus) || (screens[0] && infoOf(screens[0].key)) || {}
6411
- const meta = parseMeta(main.meta || '')
7539
+ const meta = parseMeta(main.meta || (st && st.draft && st.draft.meta) || (playR.current.end ? '' : playR.current.meta) || '') // 第一屏还在放草稿(真的没摆出来):按草稿开头写的类型
6412
7540
  const tokens = main.tokens || []
6413
7541
  const kind = (st && st.kind) || meta.kind || 'web'
6414
7542
  const size = sizeOf((st && st.size) || meta.size, kind)
@@ -6416,14 +7544,68 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6416
7544
  const canvas = view === 'canvas'
6417
7545
  const writingNew = !!(st && st.writing) && /\.html?$/i.test(st.writing) && !screens.some((s) => s.key === st.writing)
6418
7546
  const fresh = !!M.turnKeys && screens.some((s) => !M.turnKeys.has(s.key)) && Date.now() - M.lastNew < GHOST_MS
6419
- const ghost = canvas && busy && !ver && (writingNew || fresh)
7547
+ // 边写边看:模型正在写一屏新的,预览服务器上已经有它的草稿(远程看的时候本机地址打不开,还是放占位)。
7548
+ // 正文先到的草稿 key 是空的(还不知道是哪屏),名字到了才填上;写完落地后草稿还留一会儿,靠它认出新屏
7549
+ const sd = st && st.draft && st.draft.url && (busy || st.draft.live) && !away(st.draft.url) ? st.draft : null // 说完了还报的:刚写完的那份
7550
+ // 看着放的那份草稿:整页一下子到的(lump,预览服务器几秒里一截截放出来),真文件落地了也先放完再换。
7551
+ // 一路流着写的早看过了,落地就换真文件(不等放完:页面引的外部字体卡着的话 load 要等很久)。
7552
+ // 右侧栏头一回看到它时它得是还在写 / 刚写完的(live),中途才打开右侧栏的不补放旧的
7553
+ const PL = playR.current
7554
+ if (sd && sd.url !== PL.url && sd.live) playR.current = { url: sd.url, key: sd.key || '', hold: 0, end: false, meta: sd.meta || '', lump: !!sd.lump }
7555
+ else if (sd && sd.url === PL.url) {
7556
+ if (sd.key) PL.key = sd.key
7557
+ if (sd.meta) PL.meta = sd.meta
7558
+ if (sd.lump) PL.lump = true
7559
+ }
7560
+ const play = playR.current
7561
+ const landed = (key) => !!key && screens.some((x) => x.key === key)
7562
+ const playing = !!play.url && play.lump && !play.end && !(sd && sd.url !== play.url) && (!play.hold || Date.now() - play.hold < HOLD_MAX)
7563
+ const dr = sd && !landed(sd.key) ? sd : playing ? { key: play.key, url: play.url } : null
7564
+ if (dr && dr.url === play.url && !play.hold && (!sd || landed(dr.key))) play.hold = Date.now() // 从这时起是「放完再换」:最多等 HOLD_MAX
7565
+ const hideKey = dr && landed(dr.key) ? dr.key : '' // 放着的这屏已经落地:画布上先不摆真的那块(草稿就在占位那格里放)
7566
+ if (sd && sd.key) draftedR.current.set(sd.key, Date.now())
7567
+ const ghost = canvas && !ver && ((busy && (writingNew || fresh)) || !!dr)
6420
7568
  const [, setTick] = useState(0)
7569
+ const draftEnd = (u) => {
7570
+ const p = playR.current
7571
+ if (p.url !== u || p.end) return
7572
+ p.end = true
7573
+ setTick((n) => n + 1)
7574
+ }
7575
+ useEffect(() => {
7576
+ if (!play.hold || play.end) return
7577
+ const id = setTimeout(() => setTick((n) => n + 1), Math.max(0, play.hold + HOLD_MAX - Date.now()) + 50)
7578
+ return () => clearTimeout(id)
7579
+ }, [play.url, play.hold, play.end])
6421
7580
  useEffect(() => {
6422
7581
  if (!fresh || writingNew) return
6423
7582
  const id = setTimeout(() => setTick((n) => n + 1), Math.max(0, M.lastNew + GHOST_MS - Date.now()) + 50)
6424
7583
  return () => clearTimeout(id)
6425
7584
  }, [fresh, writingNew, M.lastNew])
6426
7585
  const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
7586
+ // 状态条上那句:在等你回答 / 正在做第几屏 / 正在改哪屏 / 按调的工具说 / 没调工具就是在想
7587
+ const busyPhase = () => {
7588
+ if (!st) return { text: t('working') }
7589
+ if (st.asking) return { text: t('asking'), asking: true }
7590
+ const newN = screens.length + 1 // 按画布上排第几屏说(已有 3 屏、再出一屏 = 第 4 屏),和新屏出来的位置对得上
7591
+ const w = st.writing || ''
7592
+ const ln = Number(st.lines) || 0
7593
+ const more = ln >= 10 ? t('stLines', { n: ln }) : '' // 写了多少行:写大文件的几分钟里看得到在动
7594
+ if (w) {
7595
+ if (!/\.html?$/i.test(w)) return { text: t('writing', { file: w }), sub: more }
7596
+ const sc = screens.find((x) => x.key === w)
7597
+ return sc ? { text: t('stEdit', { name: labelOf(sc) }), sub: more } : { text: t('stNew', { n: newN }), sub: more ? `${w} · ${more}` : w }
7598
+ }
7599
+ if (st.draft && !st.draft.key && /^(write|create_file)$/i.test(String(st.tool || ''))) return { text: t('stNew', { n: newN }), sub: more } // 正文先到:看得出是新的一屏,名字还没到
7600
+ const tool = String(st.tool || '')
7601
+ if (!tool) return { text: t('stThink') }
7602
+ const m = TOOL_PHASE.find(([re]) => re.test(tool))
7603
+ if (m && m[1] === 'stWriteAny' && !url) return { text: t('stNew', { n: 1 }), sub: more } // 第一屏还没出来:写的就是它
7604
+ return { text: m ? t(m[1]) : t('working'), sub: m && m[1] === 'stWriteAny' ? more : '' }
7605
+ }
7606
+ const busyBar = (side) => h(BusyBar, { key: 'busy', t, phase: busyPhase(), since: turnT.current.at || Date.now(), side })
7607
+ // 占位里的鼠标什么时候动:换了在做的事、或者又写了 15 行——模型停着它们就停着
7608
+ const pulse = pulseStep(pulseR.current, busy, busy ? busyPhase().text + '|' + ((st && st.tool) || '') : '', Number(st && st.lines) || 0)
6427
7609
  // 画布上的标题去掉几屏都带的产品名(「首页 · 梅林练」→「首页」);产品名留给导出的展示图做大标题
6428
7610
  const shortOf = (sc) => {
6429
7611
  const sp = splitTitles(shown.map(labelOf))
@@ -6448,8 +7630,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6448
7630
  return Object.keys(out).length === Object.keys(c).length ? c : out
6449
7631
  })
6450
7632
  }, [tsig])
6451
- useEffect(() => { setTw({}) }, [site])
6452
- const tweak = Object.keys(tw).length ? tweakMsg(tlist, tw) : null
7633
+ const twGen = useRef(0) // 换一次设计加一:晚到的保存回包认得出是上一套的
7634
+ useEffect(() => { twGen.current++; setTw({}); setTv({}) }, [site])
7635
+ // 拖的滑块 + 外观面板里直接改的颜色:一起发给页面先看效果
7636
+ const tweak = Object.keys(tw).length || Object.keys(tv).length ? (() => { const m = tweakMsg(tlist, tw); return { vars: { ...m.vars, ...tv }, attrs: m.attrs } })() : null
6453
7637
 
6454
7638
  // 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
6455
7639
  const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
@@ -6490,6 +7674,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6490
7674
  try {
6491
7675
  await openTarget(sid, p)
6492
7676
  setEditing(false)
7677
+ homeAway() // 从叠着的首页 / 设计库里打开的:收起来,看新打开的
7678
+ setLib('')
6493
7679
  } catch (e) {
6494
7680
  say(t('openFailed', { msg: e.message }))
6495
7681
  }
@@ -6561,6 +7747,16 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6561
7747
  if (!patch.focus && poll && poll.state) poll.set({ ...poll.state, ...patch })
6562
7748
  post('view', { session: sid, ...patch }).then((s) => poll && poll.set(s), (e) => say(t('openFailed', { msg: e.message })))
6563
7749
  }
7750
+ // 单屏里看着写完的那屏:真文件出来了就换过去看它(不然写完一闪又回到原来那屏)
7751
+ useEffect(() => {
7752
+ let k = singleDraftR.current
7753
+ if (k === '*' && sd && sd.key) k = singleDraftR.current = sd.key
7754
+ if (!k || dr) return
7755
+ if (screens.some((x) => x.key === k)) {
7756
+ singleDraftR.current = ''
7757
+ if (!canvas && focus !== k) change({ view: 'single', focus: k })
7758
+ } else if (!busy) singleDraftR.current = ''
7759
+ })
6564
7760
 
6565
7761
  // 量舞台大小,单页时按比例缩放
6566
7762
  useEffect(() => {
@@ -6674,7 +7870,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6674
7870
  setPicking(false)
6675
7871
  setTextEdit(false)
6676
7872
  setPanel(false)
6677
- setStyles(false)
6678
7873
  setHist(false)
6679
7874
  setPhone(false)
6680
7875
  closeCheck()
@@ -6718,7 +7913,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6718
7913
  }
6719
7914
  useEffect(() => {
6720
7915
  if (!styleEdit) return undefined
6721
- const onKey = (e) => { if (e.key === 'Escape') setStyleEdit(false) }
7916
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setStyleEdit(false) }
6722
7917
  window.addEventListener('keydown', onKey)
6723
7918
  return () => window.removeEventListener('keydown', onKey)
6724
7919
  }, [styleEdit])
@@ -6734,14 +7929,14 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6734
7929
 
6735
7930
  useEffect(() => {
6736
7931
  if (!textEdit) return undefined
6737
- const onKey = (e) => { if (e.key === 'Escape') setTextEdit(false) }
7932
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setTextEdit(false) }
6738
7933
  window.addEventListener('keydown', onKey)
6739
7934
  return () => window.removeEventListener('keydown', onKey)
6740
7935
  }, [textEdit])
6741
7936
 
6742
7937
  useEffect(() => {
6743
7938
  if (!picking) return undefined
6744
- const onKey = (e) => { if (e.key === 'Escape') setPicking(false) }
7939
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setPicking(false) }
6745
7940
  window.addEventListener('keydown', onKey)
6746
7941
  return () => window.removeEventListener('keydown', onKey)
6747
7942
  }, [picking])
@@ -6798,7 +7993,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6798
7993
  }
6799
7994
  useEffect(() => {
6800
7995
  if (!demo) return undefined
6801
- const onKey = (e) => { if (e.key === 'Escape') setDemo(null) }
7996
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setDemo(null) }
6802
7997
  window.addEventListener('keydown', onKey)
6803
7998
  return () => window.removeEventListener('keydown', onKey)
6804
7999
  }, [!!demo])
@@ -6964,15 +8159,21 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6964
8159
  insert(g.letter ? t('pickText', { name: labelOf(sc), where, others, base: g.stem }) : t('pickKeep', { name: labelOf(sc), where, others }), 'insertedSend')
6965
8160
  }
6966
8161
 
6967
- // 几个方向并排时每屏标题旁都挂着「就要这个」,不用先点中那一屏才看得到
8162
+ // 同一屏的几个版本:标 A / B / C(原版或 -a 是 A)
8163
+ const takeTag = (sc) => {
8164
+ const g = !sc.v && takesOf(shown.map((x) => x.key), sc.key)
8165
+ return g ? (g.letter || 'a').toUpperCase() : ''
8166
+ }
8167
+ const tagOf = (sc) => { const L = takeTag(sc); return L ? h('b', { key: 'g', className: 'ddp-vt', title: t('takeN', { l: L }) }, L) : null }
8168
+ // 几个方向并排时每屏标题旁都挂着「用这版」,不用先点中那一屏才看得到
6968
8169
  const badge = (sc) => !sc.v && takesOf(shown.map((x) => x.key), sc.key)
6969
8170
  ? h('button', { key: 'p', className: 'ddp-take', title: t('pickThis'), onClick: () => pickDirection(sc) }, h(I.check), h('span', null, t('pickShort')))
6970
8171
  : null
6971
8172
 
6972
8173
  const actions = (sc) => [
6973
- h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download)),
6974
- sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle)),
6975
- h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single)),
8174
+ h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download), h('span', { className: 'lb' }, t('actX'))),
8175
+ sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle), h('span', { className: 'lb' }, t('actV'))),
8176
+ h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single), h('span', { className: 'lb' }, t('actO'))),
6976
8177
  ]
6977
8178
 
6978
8179
  // 对比:拖中间的把手
@@ -7097,14 +8298,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7097
8298
  setTextEdit(false)
7098
8299
  setStyleEdit(false)
7099
8300
  setPanel(false)
7100
- setStyles(false)
7101
8301
  setHist(false)
7102
8302
  setPhone(false)
7103
8303
  closeCheck()
7104
8304
  setPane('')
7105
8305
  // 之前点选 / 调样式留下的「已选」框别夹在标注里
7106
8306
  for (const a of apis.current.values()) if (a && a.unpin) a.unpin()
7107
- setMarkSize(box.w && box.h ? { w: box.w, h: box.h } : null)
8307
+ setMarkSize(box.w && box.h > TOP_R + BOT_R ? { w: box.w, h: box.h - TOP_R - BOT_R } : null)
7108
8308
  setMark(true)
7109
8309
  }
7110
8310
  // 换了设计、看旧版本、演示:画的东西对不上了,收掉
@@ -7294,13 +8494,14 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7294
8494
  useEffect(() => { if (checkOff && ck) closeCheck() }, [checkOff])
7295
8495
 
7296
8496
  const toggleDemo = () => (demo ? setDemo(null) : startDemo())
7297
- const toggleHist = () => { setHist(!hist); setPanel(false); setStyles(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8497
+ const openLook = (tab) => { if (tab) setLookTab(tab); setPanel(true); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8498
+ const toggleHist = () => { setHist(!hist); setPanel(false); setPhone(false); closeCheck(); setStyleEdit(false) }
7298
8499
  // 右边同一时间只开一个面板:别的开了,这几个关掉;开这几个时别的关掉
7299
- const oldOpen = !!(panel || styles || hist || phone || ck || styleEdit)
8500
+ const oldOpen = !!(panel || hist || phone || ck || styleEdit)
7300
8501
  useEffect(() => { if (oldOpen) setPane('') }, [oldOpen])
7301
8502
  const openPane = (p) => {
7302
8503
  const next = pane === p ? '' : p
7303
- if (next) { setPanel(false); setStyles(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8504
+ if (next) { setPanel(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
7304
8505
  setPane(next)
7305
8506
  }
7306
8507
  const setTweak = (k, v) => setTw((c) => {
@@ -7308,12 +8509,24 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7308
8509
  return { ...c, [k.id]: v }
7309
8510
  })
7310
8511
  const saveTweaks = async () => {
8512
+ if (ver) return say(t('twVer')) // 旧版本的屏不收调节:存了改的是当前设计
7311
8513
  try {
7312
8514
  // 带上拖的是哪套设计:请求路上会话换了设计,服务端就不写
7313
- const r = await post('tweak', { session: sid, root: st && st.target ? st.target.root : '', values: tw })
8515
+ const sentTw = tw
8516
+ const sentTv = tv
8517
+ const gen = twGen.current
8518
+ const r = await post('tweak', { session: sid, root: st && st.target ? st.target.root : '', values: sentTw, vars: sentTv })
7314
8519
  if (!r.ok) return say(r.reason === 'busy' ? t('styleBusyTip') : r.reason === 'moved' ? t('twMoved') : t('twNoWrite'))
7315
- if (r.missed && r.missed.length) say(t('twMissed', { names: r.missed.map((x) => x.label).join(t('sep')) }))
8520
+ if (r.missed && r.missed.length) say(t('twMissed', { names: r.missed.map((x) => { const tok = String(x.id).startsWith('--') && tokens.find((k) => k.name === x.id); return tok ? tokenLabel(t, tok) : x.label }).join(t('sep')) }))
7316
8521
  else showFlash(t('twSaved', { n: r.changed.length }))
8522
+ // 写进去的(设计变量、滑块):放掉,页面重新读文件就是新值了(滑块和配色管同一个颜色时以配色为准,滑块的旧值也得放掉);
8523
+ // 没写进去的留着,还能交给模型;请求路上又改过的也留着
8524
+ if (r.changed && r.changed.length && gen === twGen.current) {
8525
+ const ids = new Set(r.changed.map((x) => x.id))
8526
+ const drop = (c, sent) => { let out = c; for (const id of Object.keys(c)) if (ids.has(id) && c[id] === sent[id]) { if (out === c) out = { ...c }; delete out[id] } return out }
8527
+ setTv((c) => drop(c, sentTv))
8528
+ setTw((c) => drop(c, sentTw))
8529
+ }
7317
8530
  } catch (e) {
7318
8531
  say(t('failed', { msg: e.message }))
7319
8532
  }
@@ -7330,58 +8543,88 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7330
8543
  }), 'insertedSend')
7331
8544
  setPane('')
7332
8545
  }
7333
- const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
8546
+ const togglePhone = () => { setPhone(!phone); setPanel(false); setHist(false); closeCheck(); setStyleEdit(false) }
7334
8547
  const reload = () => setReloadSeq((x) => x + 1)
7335
8548
 
7336
- const bar = h('div', { className: 'ddp-bar' + (editing ? ' editing' : '') },
7337
- editing || !url
7338
- ? h(AddressForm, { t, sid, initial: st && st.target && st.target.kind === 'file' ? st.target.file : st && st.label ? st.label : '', autoFocus: true, onDone: () => setEditing(false), onBrowse: browse })
7339
- : h('div', { className: 'ddp-addr', title: t('editAddr'), onClick: () => setEditing(true) },
7340
- h('i', { className: 'ddp-dot' + (busy ? ' wait' : '') }),
7341
- h('span', null, canvas && screens.length > 1 ? `${st.label} · ${screens.length}` : st.label)),
7342
- h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo || mark, onClick: togglePick },
7343
- h(I.pick), h('span', null, picking ? t('picking') : t('pick'))),
7344
- h('button', { className: 'ddp-btn' + (textEdit ? ' on' : ''), title: t('textEdit'), disabled: !url || !!ver || !!demo || busy || mark, onClick: toggleText }, h(I.text)),
7345
- h('button', { className: 'ddp-btn' + (styleEdit ? ' on' : ''), title: t('styleEdit'), disabled: styleOff || mark, onClick: toggleStyle }, h(I.sliders)),
7346
- h('button', { className: 'ddp-btn' + (mark ? ' on' : ''), title: t('mark'), 'aria-pressed': mark, disabled: markOff, onClick: toggleMark }, h(I.pen)),
7347
- h('span', { className: 'ddp-sep' }),
7348
- h('div', { className: 'ddp-seg' },
7349
- h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), disabled: mark, onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
7350
- h('button', { className: 'ddp-btn' + (!canvas && !inMulti ? ' sel' : ''), title: t('single'), disabled: mark, onClick: () => { setMulti(false); openSingle() } }, h(I.single)),
7351
- h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver || mark, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
7352
- h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver || mark, onClick: toggleDemo }, h(I.play)),
7353
- h(KindMenu, { t, kind, open: menu, setOpen: setMenu, disabled: mark, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
7354
- h('button', { className: 'ddp-btn' + (panel ? ' sel' : '') + (tokens.length && !panel ? ' has' : ''), title: t('tokens'), onClick: () => { setPanel(!panel); setStyles(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) } }, h(I.palette)),
7355
- h('button', { className: 'ddp-btn o2' + (pane === 'tweaks' ? ' sel' : '') + (tlist.length && pane !== 'tweaks' ? ' has' : ''), title: t('tweaks'), disabled: !fileTarget || !!ver, onClick: () => openPane('tweaks') }, h(I.dial)),
7356
- h('button', { className: 'ddp-btn o2' + (ck ? ' sel' : ''), title: ckCount && !ck ? t('checkCount', { n: ckCount }) : t('checkTip'), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
7357
- h(I.stetho), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
7358
- h('button', { className: 'ddp-btn o2' + (hist || ver ? ' sel' : ''), title: t('history'), disabled: !fileTarget, onClick: toggleHist }, h(I.history)),
7359
- h('button', { className: 'ddp-btn o2' + (pane === 'handoff' ? ' sel' : ''), title: t('handoff'), disabled: !url || !!ver, onClick: () => openPane('handoff') }, h(I.code)),
7360
- h(ExportMenu, { t, kind, count: shown.length, open: exMenu, setOpen: setExMenu, onPick: (m) => doExport(m), disabled: !url, zip: fileTarget }),
7361
- anyDark || dark ? h('button', { className: 'ddp-btn o1' + (dark ? ' sel' : ''), title: t('dark'), onClick: () => setDark(!dark) }, h(I.moon)) : null,
7362
- h('button', { className: 'ddp-btn o1' + (phone ? ' sel' : '') + (phoneOn && !phone ? ' has' : ''), title: t('phone'), disabled: !url, onClick: togglePhone }, h(I.scan)),
7363
- h('button', { className: 'ddp-btn o1', title: t('refresh'), onClick: reload }, h(I.refresh)),
7364
- h('a', { className: 'ddp-btn o1', title: t('external'), href: url, target: '_blank', rel: 'noopener noreferrer' }, h(I.external)),
7365
- h(MoreMenu, { t, open: more, setOpen: setMore, has: (phoneOn && !phone) || (!!ckCount && !ck && !checkOff), items: [
7366
- ['o2', I.play, t('demoShort'), !!demo, !url || !!ver || mark, toggleDemo],
7367
- ['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
7368
- ['o2', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
7369
- ['o2', I.dial, t('tweaks'), pane === 'tweaks', !fileTarget || !!ver, () => openPane('tweaks')],
7370
- ['o2', I.code, t('handoff'), pane === 'handoff', !url || !!ver, () => openPane('handoff')],
7371
- ['', I.box, t('library'), !!lib, false, () => { setLibFocus(''); if (lib) setLib(''); else libDefaultThen((k) => setLib((c) => c || k)) }],
7372
- ['', I.library, t('systems'), pane === 'systems', false, () => openPane('systems')],
7373
- anyDark || dark ? ['o1', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
7374
- ['o1', I.scan, t('phone'), phone, !url, togglePhone],
7375
- ['o1', I.refresh, t('refresh'), false, false, reload],
7376
- ['o1', I.external, t('external'), false, !url, url],
7377
- ] }))
8549
+ // 浮在画布上的几块,各管一件事,按钮都带字:左上 设计库 | 屏名 ▾(换屏、多尺寸、类型都在这个菜单里);
8550
+ // 右上 外观 体检 | 导出 ▾ ⋯;底部中间 选择 改字 样式 画笔 | 试用;右下缩放(画布自己带)
8551
+ // 鼠标停一下出说明卡(HoverTip):名字 + 一句用途
8552
+ // 窄了(样式表里):先截屏名,再收字
8553
+ const curKey = canvas ? '' : (focus || (screens[0] && screens[0].key) || '')
8554
+ const curSc = screens.find((x) => x.key === curKey)
8555
+ // 产品名:画布上几屏标题都带它时认得出来;单页只拿得到一屏的标题,用记下的那个把它去掉(「订单 · 巷口面馆」→「订单」)
8556
+ const brand0 = splitTitles(shown.map(labelOf)).brand
8557
+ if (brand0) brandMem.current[site] = brand0
8558
+ const brand = brand0 || brandMem.current[site] || ''
8559
+ const dropBrand = (x) => {
8560
+ if (!brand || x === brand) return x
8561
+ const p = x.split(/\s*[·||—]\s*|\s+[-–]\s+/).map((y) => y.trim()).filter(Boolean)
8562
+ return p.length >= 2 && (p[0] === brand || p[p.length - 1] === brand) ? p.filter((y) => y !== brand).join(' · ') : x
8563
+ }
8564
+ const stLabel = (st && st.label) || ''
8565
+ const scrLabel = canvas && screens.length > 1
8566
+ ? t('screensN', { name: brand || stLabel, n: screens.length })
8567
+ : curSc ? dropBrand(shortOf(curSc)) + (takeTag(curSc) ? ' · ' + takeTag(curSc) : '') : stLabel
8568
+ // 菜单里点某一屏:单看这一屏(「全部 n 屏」回画布)
8569
+ const pickScreen = (key) => {
8570
+ setMulti(false)
8571
+ if (demo) setDemo(null)
8572
+ setSel(key)
8573
+ if (canvas || key !== focus) change({ view: 'single', focus: key })
8574
+ }
8575
+ // 导出菜单写出是哪一屏(和 doExport 取同一屏)
8576
+ const exSc = shown.find((x) => x.key === (canvas ? sel || focus : focus)) || shown[0]
8577
+ const exName = exSc ? dropBrand(shortOf(exSc)) + (takeTag(exSc) ? ' · ' + takeTag(exSc) : '') : ''
8578
+ const libOpen = () => { setLibFocus(''); if (lib) setLib(''); else libDefaultThen((k) => setLib((c) => c || k)) }
8579
+ const dockTight = box.w > 0 && box.w <= 400 // 侧栏很窄:底部工具坞只放得下四个,「试用」进「更多」
8580
+ const tool = (cls, on, desc, off, run, Icon, label) => h('button', { className: 'ddp-tool' + (cls ? ' ' + cls : '') + (on ? ' on' : ''), ...tipA(label, desc, on ? 'Esc' : ''), 'aria-pressed': !!on, disabled: !!off, onClick: run }, h(Icon), h('span', null, label))
8581
+ const islands = editing
8582
+ ? [h('div', { key: 'top', className: 'ddp-top' },
8583
+ h('div', { className: 'ddp-isl tl wide' },
8584
+ h(AddressForm, { t, sid, initial: st && st.target && st.target.kind === 'file' ? st.target.file : st && st.label ? st.label : '', autoFocus: true, onDone: () => setEditing(false), onBrowse: browse })))]
8585
+ : [
8586
+ h('div', { key: 'top', className: 'ddp-top' },
8587
+ h('div', { className: 'ddp-isl tl' },
8588
+ h('button', { className: 'ddp-btn lab' + (homeAt === 'on' ? ' sel' : ''), ...tipA(t('homeBtn'), t('dHome'), homeAt === 'on' ? 'Esc' : ''), 'aria-pressed': homeAt === 'on', onClick: () => (homeAt === 'on' ? homeClose() : homeOpen()) }, h(I.home), h('span', { className: 'lb' }, t('homeBtn'))),
8589
+ h('span', { className: 'ddp-sep' }),
8590
+ h(ScreenMenu, { t, label: scrLabel, tip: [scrLabel || stLabel, t('dScreen')], src: stLabel, busy, list: screens.map((x) => { const L = takeTag(x); return { key: x.key, label: dropBrand(shortOf(x)) + (L ? ' · ' + L : '') } }), cur: curKey, canvas: canvas && !inMulti && !demo, open: scrMenu, setOpen: setScrMenu,
8591
+ onPick: pickScreen, onAll: () => { setMulti(false); if (demo) setDemo(null); change({ view: 'canvas' }) }, onOther: () => setEditing(true),
8592
+ sizes: mark ? null : { on: inMulti, off: !sizeSet || !!ver, tip: sizeSet ? t('sizes') : t('sizesNo'), note: sizeSet ? '' : t('sizesNoShort'), run: () => setMulti(!inMulti) },
8593
+ kind, onKind: mark ? null : (k) => change({ kind: k }) })),
8594
+ h('div', { className: 'ddp-isl tr' },
8595
+ h('button', { className: 'ddp-btn lab' + (panel ? ' sel' : ''), ...tipA(t('look'), t('dLook')), onClick: () => (panel ? setPanel(false) : openLook()) }, h(I.palette), h('span', { className: 'lb' }, t('look'))),
8596
+ h('button', { className: 'ddp-btn lab' + (ck ? ' sel' : ''), ...tipA(t('checkShort'), ckCount && !ck ? t('dCheckN', { n: ckCount }) : t('dCheck')), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
8597
+ h(I.stetho), h('span', { className: 'lb' }, t('checkShort')), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
8598
+ h('span', { className: 'ddp-sep' }),
8599
+ h(DropMenu, { t, tip: [t('deliver'), t('dExport')], btnCls: 'pri', icon: I.download, label: t('deliver'), caret: true, open: exMenu, setOpen: setExMenu, disabled: !url, items: [
8600
+ 'hd:' + t('deliverCode'),
8601
+ ['', I.code, t('handoff'), pane === 'handoff', !!ver, () => openPane('handoff')],
8602
+ 'hd:' + t('exFiles'),
8603
+ ...exportItems(t, kind, shown.length, exName).map(([mode, Icon, label]) => ['', Icon, label, false, false, () => doExport(mode)]),
8604
+ fileTarget ? 'hd:' + t('deliverShare') : null, // 只有本地的设计才能打包、分享
8605
+ fileTarget ? ['', I.box, t('exZip'), false, false, () => doExport('zip')] : null,
8606
+ fileTarget ? ['', I.share, t('exShare'), false, false, () => doExport('share')] : null,
8607
+ ], foot: t('exWhere') }),
8608
+ h(DropMenu, { t, cls: 'ddp-more', tip: [t('more'), phoneOn && !phone ? t('phoneOnTip') + ' · ' + t('dMore') : t('dMore')], icon: I.more, has: phoneOn && !phone, open: more, setOpen: setMore, items: [
8609
+ dockTight ? ['', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo] : null, // 侧栏太窄,底部工具坞放不下「试用」:挪到这里
8610
+ dockTight ? 'sep' : null,
8611
+ ['', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
8612
+ ['', I.scan, t('phone'), phone, !url, togglePhone, phoneOn ? t('phoneOnShort') : ''],
8613
+ anyDark || dark ? ['', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
8614
+ 'sep',
8615
+ ['', I.refresh, t('refresh'), false, false, reload],
8616
+ ['', I.external, t('external'), false, !url, url],
8617
+ ] }))),
8618
+ h('div', { key: 'dock', className: 'ddp-isl ddp-dock', role: 'toolbar', 'aria-label': t('toolsT') },
8619
+ tool('pick', picking, t('dPick'), !url || !!ver || !!demo || mark, togglePick, I.pick, t('pick')),
8620
+ tool('', textEdit, t('dText'), !url || !!ver || !!demo || busy || mark, toggleText, I.text, t('textShort')),
8621
+ tool('', styleEdit, t('dStyle'), styleOff || mark, toggleStyle, I.sliders, t('styleShort')),
8622
+ tool('', mark, t('dMark'), markOff, toggleMark, I.pen, t('markShort')),
8623
+ dockTight ? null : h('span', { className: 'ddp-sep dm' }),
8624
+ dockTight ? null : tool('dm', !!demo, t('dPlay'), !url || !!ver, toggleDemo, I.play, t('demoShort'))),
8625
+ ]
7378
8626
 
7379
8627
  // 模型用提问卡问你、在等回答:不转圈,换成一个橙点
7380
- const busyChip = (cls) => st && st.asking
7381
- ? h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'q' }), h('span', null, t('asking')))
7382
- : h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'sp' }),
7383
- h('span', { className: 'tx' }, st && st.writing ? t('writing', { file: st.writing }) : t('working')))
7384
-
7385
8628
  // ── 参考图:拖进来 / 粘贴 / 从设计规范面板选;存进设计目录的 refs/,尺寸和主要颜色放进输入框 ──
7386
8629
  const addRef = async (file) => {
7387
8630
  if (!file || !/^image\//.test(file.type)) return say(t('refNotImage'))
@@ -7477,9 +8720,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7477
8720
  setTab: (k) => { setLibFocus(''); setLib(k) },
7478
8721
  focus: libFocus,
7479
8722
  hasDesign: !!url,
8723
+ homey: !url || homeAt === 'on',
7480
8724
  curRoot: fileTarget ? st.target.root : '',
7481
8725
  onClose: () => { setLib(''); setLibFocus('') },
7482
- onOpen: (w) => openTarget(sid, w.dir).then(() => { setLib(''); setEditing(false) }, (e) => say(t('wkOpenFail', { msg: e.message }))),
8726
+ onOpen: (w) => openTarget(sid, w.dir).then(() => { setLib(''); setEditing(false); homeAway() }, (e) => say(t('wkOpenFail', { msg: e.message }))),
7483
8727
  onPack: openPack,
7484
8728
  onShare: openShare,
7485
8729
  onSaveStyle: saveMyStyle,
@@ -7491,17 +8735,20 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7491
8735
  onSay: say,
7492
8736
  }),
7493
8737
  toast ? h('div', { className: 'ddp-toast low' }, h('span', null, toast.text)) : null) : null
7494
- const root = (...kids) => h('div', rootProps, ...kids, libLayer, dropLayer, fileInput, pageFileInput)
8738
+ // 开着设计时点「首页」:首页叠在画布上(设计库再叠在它上面)
8739
+ const root = (...kids) => h('div', rootProps, ...kids, url && homeAt ? homeView(true) : null, libLayer, dropLayer, fileInput, pageFileInput)
7495
8740
 
7496
8741
  // ── 设计库:打成 zip、拖进来的设计包、存成我的风格 ──
7497
8742
  function openPack(dir) {
7498
8743
  setPackDir(dir || '')
7499
8744
  setLib('')
8745
+ homeAway()
7500
8746
  if (pane !== 'pack') openPane('pack')
7501
8747
  }
7502
8748
  function openShare(dir) {
7503
8749
  setPackDir(dir || '')
7504
8750
  setLib('')
8751
+ homeAway()
7505
8752
  if (pane !== 'share') openPane('share')
7506
8753
  }
7507
8754
  const sameDir = (a, b) => String(a || '').replace(/[\\/]+$/, '') === String(b || '').replace(/[\\/]+$/, '')
@@ -7532,6 +8779,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7532
8779
  if (d.error === 'check') {
7533
8780
  setZipIssues({ errors: d.errors || [], more: d.more || 0 })
7534
8781
  setLib('')
8782
+ homeAway()
7535
8783
  if (pane !== 'zip') openPane('zip')
7536
8784
  return undefined
7537
8785
  }
@@ -7542,6 +8790,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7542
8790
  }
7543
8791
  setLib('')
7544
8792
  setEditing(false)
8793
+ homeAway()
7545
8794
  if (pane === 'zip' || pane === 'pack' || pane === 'share') { setPane(''); setZipIssues(null) }
7546
8795
  if (poll && d.snap) poll.set(d.snap)
7547
8796
  insert(formatTpl(t, d))
@@ -7560,6 +8809,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7560
8809
  }
7561
8810
  setLib('')
7562
8811
  setEditing(false)
8812
+ homeAway()
7563
8813
  if (pane === 'zip' || pane === 'pack' || pane === 'share') { setPane(''); setZipIssues(null) }
7564
8814
  if (poll && d.snap) poll.set(d.snap)
7565
8815
  insert(formatTpl(t, d))
@@ -7585,20 +8835,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7585
8835
 
7586
8836
  // 调节滑块 / 品牌设计系统 / 转成代码(右边的面板)
7587
8837
  const sidePane = () => {
7588
- if (pane === 'tweaks') {
7589
- return h(TweaksPanel, {
7590
- t,
7591
- tw: tweaks,
7592
- live: tw,
7593
- write: busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('twSaveTip') },
7594
- onSet: setTweak,
7595
- onReset: () => setTw({}),
7596
- onAsk: () => insert(formatTweaks(t, tlist, tw), 'insertedSend'),
7597
- onSave: saveTweaks,
7598
- onMake: (errs) => insert(errs && errs.length ? t('twFixText', { errs: errs.join(';') }) + '\n' : t('twMakeText') + '\n', 'insertedSend'),
7599
- onClose: () => setPane(''),
7600
- })
7601
- }
7602
8838
  if (pane === 'systems') {
7603
8839
  return h(SystemsPanel, {
7604
8840
  t,
@@ -7614,7 +8850,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7614
8850
  return h(PackPanel, { t, sid, root: packDir || (fileTarget ? st.target.root : ''), onFix: fixPack, onSay: say, onClose: () => setPane('') })
7615
8851
  }
7616
8852
  if (pane === 'share') {
7617
- return h(SharePanel, { t, sid, root: packDir || (fileTarget ? st.target.root : ''), onFix: fixPack, onSay: say, onClose: () => setPane('') })
8853
+ return h(SharePanel, { t, sid, root: packDir || (fileTarget ? st.target.root : ''), brand: !packDir || (fileTarget && packDir === st.target.root) ? brand : '', onFix: fixPack, onSay: say, onClose: () => setPane('') })
7618
8854
  }
7619
8855
  if (pane === 'zip' && zipIssues) {
7620
8856
  return h(ZipIssues, { t, issues: zipIssues, onClose: () => { setPane(''); setZipIssues(null) } })
@@ -7625,46 +8861,112 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7625
8861
  return null
7626
8862
  }
7627
8863
 
8864
+ // 首页大输入框发出去:放进 DSH 的输入框直接发;输入框里本来有字、或旧版 DSH 没有 submit:放进去等用户自己发
8865
+ const homeSend = ({ text, kind, style }) => {
8866
+ const ia = props.inputActions
8867
+ const cel = composerEl()
8868
+ const was = cel ? composerText(cel) : null
8869
+ const had = was === null || !!was.trim() // 读不出来(有引用标签、分屏)当成有字:只放进去,不替用户发
8870
+ const msg = text + (kind ? '\n' + t('askKindLine', { k: t(KIND_KEY[kind]) }) : '') + (url ? '\n' + t('askNewLine') : '')
8871
+ homeKind.current = kind || ''
8872
+ if (!insert(msg, 'inserted', false)) {
8873
+ insert([msg, style ? formatStyle(t, style, false) : ''].filter(Boolean).join('\n\n')) // 插不进:整段复制
8874
+ return false
8875
+ }
8876
+ if (style) insert(formatStyle(t, style, false), 'inserted', false)
8877
+ if (url) homeClose() // 开着设计时从首页发的:收起首页,看着新的一套长出来
8878
+ if (had || !ia || typeof ia.submit !== 'function') { say(t('insertedSend')); return true }
8879
+ setTimeout(() => {
8880
+ try { ia.submit(); say(t('askSent')) } catch (e) { console.warn('[dsh-design-studio] submit 失败', e); say(t('insertedSend')) }
8881
+ }, 80)
8882
+ return true
8883
+ }
8884
+ // 「+」里的「打开已有的 HTML」:桌面版直接选文件;网页版展开下面的地址框
8885
+ const openHtml = () => {
8886
+ if (browse) return browse()
8887
+ const d = homeRef.current && homeRef.current.querySelector('.ddp-ways')
8888
+ if (!d) { if (url) { homeClose(); setEditing(true) } return } // 叠着的首页没有地址框:用左上角的
8889
+ d.open = true
8890
+ d.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
8891
+ const i = d.querySelector('input')
8892
+ if (i) i.focus({ preventScroll: true })
8893
+ }
8894
+ const toLib = (k, id) => { setLibFocus(id || ''); setLib(k) }
8895
+ const homeView = (over) => h('div', { key: 'home', className: 'ddp-home' + (over ? ' over' + (homeAt === 'off' ? ' out' : '') : ''), ref: homeRef, 'aria-label': over ? t('homeBtn') : undefined, role: over ? 'dialog' : undefined },
8896
+ h(HomeBar, {
8897
+ t,
8898
+ tab: '',
8899
+ onTab: (k) => { if (k) toLib(k) },
8900
+ lead: over ? h('button', { className: 'ddp-homeback', ...tipA(t('homeBack', { name: scrLabel || stLabel }), '', 'Esc'), onClick: homeClose }, h(I.back), h('span', null, t('homeBack', { name: scrLabel || stLabel }))) : null,
8901
+ }),
8902
+ h('div', { className: 'ddp-homebd' },
8903
+ h('div', { className: 'ddp-start' },
8904
+ h('div', { className: 'ddp-hero' }, h('h1', null, t('emptyHead')), h('p', null, t('emptySub'))),
8905
+ !over && st && st.hint ? h('button', {
8906
+ className: 'ddp-go',
8907
+ onClick: () => post('open', { session: sid, target: st.hint }).then((x) => { if (poll) poll.set(x) }, (e) => say(t('openFailed', { msg: e.message }))),
8908
+ }, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
8909
+ h(HomeAsk, {
8910
+ t,
8911
+ draft: homeDraft,
8912
+ autoFocus: over,
8913
+ note: over ? t('askNewNote') : '',
8914
+ onSend: homeSend,
8915
+ onRef: pickRef,
8916
+ plus: [
8917
+ ['', I.page, t('askOpenHtml'), false, false, openHtml],
8918
+ ['', I.palette, t('askBrand'), false, false, () => { if (over) homeClose(); openPane('systems') }],
8919
+ ['', I.library, t('askLib'), false, false, () => { setLibFocus(''); libDefaultThen((k) => setLib((c) => c || k)) }],
8920
+ ],
8921
+ }),
8922
+ h(HomeWorks, {
8923
+ t,
8924
+ cur: over && fileTarget ? st.target.root : '',
8925
+ onBack: homeClose,
8926
+ onOpen: (w) => openTarget(sid, w.dir).then(() => { setEditing(false); if (over) homeClose() }, (e) => say(t('wkOpenFail', { msg: e.message }))),
8927
+ onAll: () => toLib('works'),
8928
+ }),
8929
+ h(TplStart, { t, onOpen: (id) => toLib('market', id), onMore: () => toLib('market') }),
8930
+ over ? null : h('details', { className: 'ddp-ways' },
8931
+ h('summary', null, h(I.caret), t('waysHtml')),
8932
+ h('div', { className: 'in' },
8933
+ h('p', null, t(browse ? 'emptyHintPick' : 'emptyHint')),
8934
+ h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {}, onBrowse: browse }))))),
8935
+ h(HoverTip, { key: 'tip' }))
8936
+
7628
8937
  if (!url) {
7629
8938
  const toastEl = toast ? h('div', { className: 'ddp-toast' }, h('span', null, toast.text)) : null
7630
8939
  if (st ? st.loading : !waited) return root(h('div', { className: 'ddp-stage', ref: stageRef }), toastEl)
7631
8940
  if (busy) {
7632
- // 模型在做第一屏:先放一个生成中的占位
8941
+ // 模型在做第一屏:先放一个生成中的占位(按选的类型画个大概的样子)
8942
+ const waitKind = homeKind.current || (st && st.kind) || 'web'
7633
8943
  return root(
7634
- h('div', { className: 'ddp-stage', ref: stageRef },
7635
- h('div', { className: 'ddp-wait' }, h('div', { className: 'card' }, h(Skeleton, {})), h('p', null, t('waiting'))),
8944
+ h('div', { className: 'ddp-stage bz', ref: stageRef },
8945
+ h('div', { className: 'ddp-wait' },
8946
+ h('div', { className: 'card k-' + waitKind },
8947
+ h(GhostDraw, { key: 'gd', t, kind: waitKind, pulse }),
8948
+ dr ? h(FitDraft, { key: 'df', url: dr.url, kind: waitKind, lang: t('lang'), onEnd: draftEnd }) : null),
8949
+ h('p', null, t(dr ? 'waitingLive' : 'waiting'))),
7636
8950
  h('div', { className: 'ddp-glow' }),
7637
8951
  h('div', { className: 'ddp-ring' }),
7638
- busyChip('')),
8952
+ busyBar(false)),
7639
8953
  toastEl)
7640
8954
  }
7641
8955
  return root(
7642
- h('div', { className: 'ddp-empty' },
7643
- h(EmptyArt),
7644
- h('b', null, t('empty')),
7645
- st && st.hint ? h('button', {
7646
- className: 'ddp-go',
7647
- onClick: () => post('open', { session: sid, target: st.hint }).then((s) => { if (poll) poll.set(s) }, (e) => say(t('openFailed', { msg: e.message }))),
7648
- }, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
7649
- h('ol', { className: 'ddp-steps' }, h('li', null, t('step1')), h('li', null, t('step2')), h('li', null, t('step3'))),
7650
- h('div', { className: 'ddp-tries' }, h('span', null, t('tryHead')),
7651
- ['try1', 'try2', 'try3'].map((k) => h('button', { key: k, className: 'ddp-try', onClick: () => insert(t(k), 'insertedSend') }, t(k)))),
7652
- h('p', null, t(browse ? 'emptyHintPick' : 'emptyHint')),
7653
- h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {}, onBrowse: browse }),
7654
- h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose'))),
7655
- h('p', { className: 'ddp-refline' }, t('sysEmptyLine'), ' · ', h('button', { className: 'lk', onClick: () => openPane('systems') }, t('sysOpen'))),
7656
- h('p', { className: 'ddp-refline' }, t('libLine'), ' · ', h('button', { className: 'lk', onClick: () => { setLibFocus(''); libDefaultThen((k) => setLib((c) => c || k)) } }, t('libOpen'))),
7657
- h(TplStart, { t, onOpen: (id) => { setLibFocus(id); setLib('market') }, onMore: () => { setLibFocus(''); setLib('market') } }),
7658
- h('div', { className: 'ddp-stylepick' }, h('b', null, t('stylesHead')), h('p', null, t('stylesTip')), h(StyleCards, { t, onPick: (x) => insert(formatStyle(t, x, false)) }))),
8956
+ homeView(false),
7659
8957
  pane === 'systems' || pane === 'pack' || pane === 'share' || pane === 'zip' ? sidePane() : null,
7660
8958
  toastEl)
7661
8959
  }
7662
8960
 
8961
+ // 上下两排浮块中间那块的高度:单页、演示、对比按它缩放
8962
+ const ah = Math.max(0, box.h - TOP_R - BOT_R)
8963
+ const cvBody = !demo && (inMulti || (canvas && !(ver && cmp)))
7663
8964
  let body
8965
+ let busyGap = false
7664
8966
  if (demo) {
7665
8967
  const fill = kind === 'web'
7666
8968
  const [dw, dh] = deviceBox(kind, size)
7667
- const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - 46) / dh))
8969
+ const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (ah - 16 - 46) / dh))
7668
8970
  const top = demo.layers[demo.layers.length - 1]
7669
8971
  body = h('div', { className: 'ddp-demo' }, ...demo.layers.map((l) => {
7670
8972
  const isTop = l.id === top.id
@@ -7701,6 +9003,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7701
9003
  body = h(Canvas, {
7702
9004
  key: 'multi',
7703
9005
  t,
9006
+ top: TOP_R,
9007
+ bottom: BOT_R,
7704
9008
  screens: fake,
7705
9009
  items,
7706
9010
  kind,
@@ -7738,7 +9042,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7738
9042
  const old = now ? { key: now.key, id: `v${ver.n}|${now.key}`, v: ver.n, url: vbase + encodeURIComponent(now.key), label: now.label } : null
7739
9043
  const fill = kind === 'web'
7740
9044
  const [dw, dh] = deviceBox(kind, size)
7741
- const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
9045
+ const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (ah - 16 - (ver ? 46 : 0)) / dh))
7742
9046
  // 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
7743
9047
  const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
7744
9048
  const screenOf = (sc) => h(Screen, { t, remote, onPageDrag: pageDrag, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
@@ -7759,17 +9063,23 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7759
9063
  body = h(Canvas, {
7760
9064
  key: 'grid',
7761
9065
  t,
7762
- screens: shown,
9066
+ top: TOP_R + (ver ? 46 : 0), // 看旧版本时上面还有一条横幅
9067
+ bottom: BOT_R,
9068
+ screens: hideKey && !ver ? shown.filter((x) => x.key !== hideKey) : shown,
7763
9069
  actions,
7764
9070
  badge,
9071
+ tag: tagOf,
7765
9072
  kind,
7766
9073
  size,
7767
9074
  ghost,
9075
+ draft: ghost && dr ? dr.url : '',
9076
+ onDraftEnd: draftEnd,
9077
+ pulse,
7768
9078
  picking: picking || textEdit || styleEdit,
7769
9079
  sel: sel || focus,
7770
9080
  labelOf: shortOf,
7771
9081
  reveal,
7772
- inset: ck || styleEdit || panel || styles || pane ? 296 : 0, // 右边的面板 276 宽,离边 10
9082
+ inset: ck || styleEdit || panel || pane || hist || phone ? 296 : 0, // 右边的面板 276 宽,离边 10
7773
9083
  render: (sc, z) => renderScreen(sc, z, false),
7774
9084
  flows,
7775
9085
  onSelect: setSel,
@@ -7779,30 +9089,41 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7779
9089
  const fill = kind === 'web'
7780
9090
  const [dw, dh] = deviceBox(kind, size)
7781
9091
  // 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
7782
- const aside = !fill && (ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? 296 : 0
9092
+ const sideOn = !!(ck || styleEdit || panel || pane || hist || phone) && box.w - 296 >= 240
9093
+ const aside = !fill && sideOn ? 296 : 0
9094
+ // 生成中:右侧栏窄(或开着面板)时状态条在上排浮块下面,手机这类给它让一条;宽的时候它在上排中间,不用让。网页不让(一让就重排)
9095
+ busyGap = busy && !fill && (box.w < 860 || sideOn)
7783
9096
  // 画笔标注开着:网页按开标注时的大小定住(侧栏拉窄了就整体缩小,不重排),画的才对得上
7784
9097
  const lock = fill && mark && markSize ? markSize : null
7785
- const z = lock ? Math.max(0.05, Math.min(1, box.w / lock.w, box.h / lock.h)) : fill ? 1 : Math.max(0.05, Math.min(1, (box.w - aside - 32) / dw, (box.h - 32 - (ver || hintTop ? 46 : 0)) / dh))
9098
+ const z = lock ? Math.max(0.05, Math.min(1, box.w / lock.w, ah / lock.h)) : fill ? 1 : Math.max(0.05, Math.min(1, (box.w - aside - 32) / dw, (ah - 16 - (ver || hintTop || busyGap ? 46 : 0)) / dh))
7786
9099
  const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
9100
+ // 单屏时模型在写新的一屏:先看着它写(没在选、改字、调样式、标注的时候),写完换过去
9101
+ const watch = dr && !ver && !demo && !picking && !textEdit && !styleEdit && !mark && !lock
9102
+ if (watch) singleDraftR.current = dr.key || '*' // 还不知道是哪屏:名字到了再说
7787
9103
  body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
7788
9104
  h('div', { className: 'ddp-fit' + (fill && !lock ? ' fill' : ''), style: fill && !lock ? undefined : { width: lock ? lock.w : dw, height: lock ? lock.h : dh, transform: `translate(-50%, -50%) scale(${z})` } },
7789
- renderScreen(one, z, fill)))
9105
+ watch
9106
+ ? h(Device, { key: 'draft', kind, w: size[0], h: size[1], fill, label: '', os: meta.os },
9107
+ h(GhostDraw, { key: 'gd', t, kind, pulse }),
9108
+ h(DraftFrame, { key: 'df', url: dr.url, z: fill ? 1 : z, lang: t('lang'), onEnd: draftEnd }))
9109
+ : renderScreen(one, z, fill)))
7790
9110
  }
7791
9111
 
7792
9112
  let chip = null
7793
- const side = canvas || inMulti ? ' left' : ''
9113
+ const sideOpen = !!(ck || styleEdit || panel || pane || hist || phone) && box.w - 296 >= 240 // 右边开着面板:提示放在左边那块的中间
9114
+ const side = sideOpen ? ' side' : ''
7794
9115
  const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : styleEdit && !sty ? t(canvas ? 'styleAny' : 'styleOne') : ''
7795
9116
  if (flash) chip = h('div', { className: 'ddp-chip ok' + side, key: 'ok' }, h(I.check), h('span', null, flash))
7796
- else if (busy) chip = busyChip(side)
7797
9117
 
7798
9118
  return root(
7799
- bar,
7800
- h('div', { className: 'ddp-stage' + (!canvas && !inMulti && kind === 'web' ? ' full' : ''), ref: stageRef },
7801
- h('div', { className: 'ddp-body' + (ver || demo || hintTop ? ' vb' : '') }, body),
9119
+ h('div', { className: 'ddp-stage isl' + (busy ? ' bz' : ''), ref: stageRef },
9120
+ h('div', { className: 'ddp-body' + (cvBody ? '' : ' pad' + (ver || demo || hintTop || busyGap ? ' vb' : '') + (ease ? ' ease' : '')) }, body),
7802
9121
  busy ? h('div', { className: 'ddp-glow' }) : null,
7803
9122
  busy ? h('div', { className: 'ddp-ring' }) : null,
9123
+ busy ? busyBar(sideOpen) : null,
7804
9124
  // 右边开着面板时提示条居中在面板左边那块,别被面板盖住一半
7805
- hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
9125
+ // 提示只占一个位置:有新消息(toast)时它先让开,消息走了再回来
9126
+ hintText && !toast ? h('div', { className: 'ddp-hint' + (sideOpen ? ' side' : '') }, hintText) : null,
7806
9127
  ck ? h(CheckPanel, { t, ck, onFocus: focusIssue, onFix: fixIssues, onAgain: runCheck, onClose: closeCheck }) : null,
7807
9128
  styleEdit ? h(StylePanel, {
7808
9129
  t,
@@ -7815,11 +9136,40 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7815
9136
  onSave: saveStyle,
7816
9137
  onClose: () => setStyleEdit(false),
7817
9138
  }) : null,
7818
- panel ? h(TokensPanel, { t, tokens, onPick: (tok) => insert(formatToken(tok, stRef.current)), onAsk: () => insert(t('tokensAskText') + '\n', 'insertedSend'), onRef: pickRef,
7819
- onStyles: () => { setPanel(false); setStyles(true) }, onSystems: () => openPane('systems'),
7820
- onSaveStyle: saveMyStyle, onLibrary: () => { setPanel(false); setLib('inspire') }, onClose: () => setPanel(false) }) : null,
9139
+ panel ? h(LookPanel, {
9140
+ t,
9141
+ tab: lookTab,
9142
+ setTab: setLookTab,
9143
+ tokens,
9144
+ tv,
9145
+ onTv: (name, v) => setTv((c) => ({ ...c, [name]: v })),
9146
+ onTokAsk: (tok) => insert(formatToken(tok, stRef.current)),
9147
+ onTokMake: () => insert(t('tokensAskText') + '\n', 'insertedSend'),
9148
+ tw: tweaks,
9149
+ live: tw,
9150
+ onTw: setTweak,
9151
+ onTwMake: (errs) => insert(errs && errs.length ? t('twFixText', { errs: errs.join(';') }) + '\n' : t('twMakeText') + '\n', 'insertedSend'),
9152
+ write: !fileTarget ? { ok: false, tip: t('twNoWrite') } : ver ? { ok: false, tip: t('twVer') } : busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('lookSaveTip') },
9153
+ onReset: () => { setTw({}); setTv({}) },
9154
+ onAsk: () => insert(formatLook(t, tlist, tw, tokens, tv), 'insertedSend'),
9155
+ onSave: saveTweaks,
9156
+ onPickStyle: (x) => { setPanel(false); insert(formatStyle(t, x, true), 'insertedSend') },
9157
+ onRef: pickRef,
9158
+ onSaveStyle: saveMyStyle,
9159
+ onInspire: () => { setPanel(false); setLib('inspire') },
9160
+ brand: lookTab === 'brand' ? h(SystemsPanel, {
9161
+ t,
9162
+ bare: true,
9163
+ rev: (st && st.turns) || 0,
9164
+ hasDesign: !!url,
9165
+ onExtract: (src) => insert(t('sysExtractText', { src }) + '\n', 'insertedSend'),
9166
+ onApply: (x) => insert(t('sysApplyText', { name: x.name, dir: x.dir }) + '\n', 'insertedSend'),
9167
+ onSay: say,
9168
+ onClose: () => setPanel(false),
9169
+ }) : null,
9170
+ onClose: () => setPanel(false),
9171
+ }) : null,
7821
9172
  pane ? sidePane() : null,
7822
- styles ? h(StylesPanel, { t, onPick: (x) => { setStyles(false); insert(formatStyle(t, x, true), 'insertedSend') }, onClose: () => setStyles(false) }) : null,
7823
9173
  phone ? h(PhonePanel, {
7824
9174
  t,
7825
9175
  sid,
@@ -7833,19 +9183,23 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7833
9183
  mark && !markOff ? h(MarkLayer, { t, sending: markBusy, frames: markFrames, onCancel: () => { markRun.current++; setMark(false) }, onSend: sendMark, onCopy: canCopyImage() ? copyMark : null }) : null,
7834
9184
  demo ? h('div', { className: 'ddp-banner' },
7835
9185
  h(I.play),
7836
- h('span', null, t('demoOn') + ' · ' + ((infoOf(demoKey) || {}).title || demoKey.replace(/\.html?$/i, ''))),
7837
- h('button', { className: 'ddp-btn', title: t('demoBack'), disabled: demo.stack.length < 2, onClick: demoBack }, h(I.back)),
7838
- h('button', { className: 'ddp-btn', title: t('demoRestart'), disabled: demo.stack.length < 2, onClick: demoRestart }, h(I.refresh)),
7839
- h('button', { className: 'ddp-btn', title: t('demoExit'), onClick: () => setDemo(null) }, h(I.close))) : null,
9186
+ h('b', null, t('demoOn')),
9187
+ h('span', { className: 'nm' }, dropBrand((infoOf(demoKey) || {}).title || demoKey.replace(/\.html?$/i, ''))),
9188
+ demo.stack.length < 2 ? h('em', null, t('demoHow')) : null,
9189
+ h('button', { className: 'ddp-btn', ...tipA(t('demoBack')), disabled: demo.stack.length < 2, onClick: demoBack }, h(I.back)),
9190
+ h('button', { className: 'ddp-btn', ...tipA(t('demoRestart')), disabled: demo.stack.length < 2, onClick: demoRestart }, h(I.refresh)),
9191
+ h('button', { className: 'ddp-btn', ...tipA(t('demoExit'), '', 'Esc'), onClick: () => setDemo(null) }, h(I.close))) : null,
7840
9192
  ver ? h('div', { className: 'ddp-banner' },
7841
9193
  h(I.history),
7842
9194
  h('span', null, t('viewing', { n: ver.n, time: hhmm(ver.at) })),
7843
- h('button', { className: 'ddp-go' + (cmp ? ' on' : ''), onClick: () => setCmp(!cmp) }, t('compare')),
9195
+ h('button', { className: 'ddp-go sec' + (cmp ? ' on' : ''), onClick: () => setCmp(!cmp) }, t('compare')),
7844
9196
  h('button', { className: 'ddp-go pri', onClick: doRestore }, t('restore')),
7845
9197
  h('button', { className: 'ddp-btn', title: t('backNow'), onClick: () => { setVer(null); setCmp(false) } }, h(I.close))) : null,
7846
9198
  chip,
7847
- toast ? h('div', { className: 'ddp-toast' + (mark ? ' mk' : ver || demo || hintText ? ' low' : '') }, h('span', null, toast.text),
7848
- toast.act ? h('button', { onClick: () => { toast.act.run(); setToast('') } }, toast.act.label) : null) : null))
9199
+ toast ? h('div', { className: 'ddp-toast' + (mark ? ' mk' : ver || demo ? ' low' : '') + (sideOpen && !mark ? ' side' : ''), role: 'status' }, h('span', null, toast.text),
9200
+ toast.act ? h('button', { onClick: () => { toast.act.run(); setToast('') } }, toast.act.label) : null) : null,
9201
+ ...islands,
9202
+ h(HoverTip, { key: 'tip' })))
7849
9203
  }
7850
9204
 
7851
9205
  // ── 标题栏按钮:打开预览,顺便在模型开了新页面时把右侧栏弹出来 ──────────
@@ -7894,7 +9248,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7894
9248
  ctx.effect(() => ctx.sidebarRightTabs.register({
7895
9249
  id: TAB_ID,
7896
9250
  kind: KIND,
7897
- title: () => t('title'),
9251
+ title: () => t('tabTitle'),
7898
9252
  keepMounted: true,
7899
9253
  guide: [{ id: 'open', order: 30, title: () => t('title'), description: () => t('guideDesc'), icon: GuideIcon }],
7900
9254
  }), 'dsh-design-studio: tab type')
@@ -7971,7 +9325,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7971
9325
  // 注意:这里是客户端**服务名**(短的),不是包名
7972
9326
  exports.inject = INJECT
7973
9327
  // 测试用
7974
- exports._test = { MarketDetail, dragKind, droppedOf, pickInfo, pickLabel, pickRanges, pickRule, chipOf, chipRanges, makeT, ZH, EN, formatCheck, formatTweaks, markHit, markPath, STYLES, formatStyle, accentUse, styleLook, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm, formatTpl, formatPrep, styleFromTokens, myStyleOf, isZipFile }
9328
+ exports._test = { pulseStep, MarketDetail, dragKind, droppedOf, pickInfo, pickLabel, pickRanges, pickRule, chipOf, chipRanges, makeT, ZH, EN, formatCheck, formatTweaks, markHit, markPath, STYLES, formatStyle, accentUse, styleLook, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm, formatTpl, formatPrep, styleFromTokens, myStyleOf, isZipFile }
7975
9329
  return module.exports
7976
9330
  },
7977
9331
  })