dsh-design-studio 0.11.0 → 0.13.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: '下一个(→)',
@@ -379,6 +419,7 @@ window.__ModuleLoader__.load({
379
419
  styleFonts: '标题 {head};正文 {body}',
380
420
  styleShape: '圆角 {r}px;阴影 {s}',
381
421
  styleStart: '照这个风格来做(先把这些定进 tokens.css;不用再问想要什么感觉):',
422
+ styleAuto: '这张风格卡是首页按题材自动挑的,不是我选的:直接做全套就照它的配色和字体来(定进 tokens.css);要先出三个方向,就让方向 A 用它、B 和 C 从风格卡里另挑两种不同的感觉。我在话里另说了风格、或者有默认的品牌设计系统,就以那个为准:',
382
423
  styleRestyle: '把现在的设计换成这个风格:先改 tokens.css(配色、字体、圆角、阴影),各屏跟着变;页面里写死的颜色也换成变量;布局和文案先不动。\n',
383
424
  demoNone: '这一屏还没有跳到别的屏的按钮;可以让模型用 <a href="detail.html"> 把几屏串起来',
384
425
  phone: '在手机上看',
@@ -396,29 +437,111 @@ window.__ModuleLoader__.load({
396
437
  phoneAuto: '只有扫了这个码的手机能打开,30 分钟没人看自动关。',
397
438
  phoneNoLan: '电脑没连上 Wi-Fi 或有线网,手机连不过来。',
398
439
  phoneFail: '没打开:{msg}',
399
- phoneHelp: '打不开?确认在同一个 Wi-Fi;手机开着代理 / VPN 的先关掉;电脑问「是否允许传入连接」时点允许。',
440
+ phoneHelp: '确认在同一个 Wi-Fi;手机开着代理 / VPN 的先关掉;电脑问「是否允许传入连接」时点允许。',
441
+ phoneTrouble: '打不开?',
400
442
  zoomIn: '放大',
401
443
  zoomOut: '缩小',
402
444
  fit: '适合画布',
403
445
  openScreen: '双击只看这一屏',
404
- generating: '生成中…',
446
+ generating: '设计中…',
405
447
  waiting: '模型正在工作,页面写出来就显示在这里',
448
+ waitingLive: '页面正在一段段写出来,写完就能点着改',
406
449
  refresh: '刷新',
407
450
  external: '在浏览器里打开',
408
451
  more: '更多',
409
- demoShort: '演示',
452
+ demoShort: '试用',
453
+ // 浮块按钮悬停说明:名字下面这一句
454
+ dPick: '点一个元素或拖出一块区域,发给模型改',
455
+ dText: '点页面里的字直接改,回车保存',
456
+ dStyle: '点一个元素,拖滑块调字号、颜色、圆角和间距',
457
+ dMark: '画圈、画箭头、写字,拼成一张图发给模型',
458
+ dPlay: '像真的 App 一样点,按钮会跳到对应的屏',
459
+ dLib: '存过的设计、风格和模板,也能逛设计市场',
460
+ dLook: '改配色、字体,或整套换个风格',
461
+ dCheck: '找出文字溢出、重叠、看不清、按钮太小',
462
+ dCheckN: '上次找到 {n} 个问题,点开看',
463
+ dExport: '转成代码、存图片 / PDF / PPT、打包分享',
464
+ dMore: '版本、在手机上看、刷新、在浏览器里打开',
465
+ dZoom: '按住 {mod} 滚动也能缩放',
466
+ dScreen: '换一屏、看全部、多尺寸、换类型',
467
+ toolsT: '工具',
410
468
  editAddr: '换一个地址',
469
+ textShort: '改字',
470
+ styleShort: '样式',
471
+ markShort: '画笔',
472
+ viewT: '视图',
473
+ canvasShort: '画布',
474
+ singleShort: '单页',
475
+ sizesShort: '多尺寸',
476
+ look: '外观',
477
+ lookTip: '外观:配色、字体、整套风格',
478
+ deliver: '导出',
479
+ deliverTip: '导出:转成代码、存成图片 / PDF / PPT、打包分享',
480
+ exFiles: '存成文件',
481
+ checkShort: '体检',
482
+ zoomT: '缩放',
483
+ zoomReal: '实际大小',
484
+ actX: '导出',
485
+ actV: '再出两版',
486
+ actO: '单看',
487
+ deliverCode: '代码',
488
+ deliverShare: '分享',
489
+ screensT: '换一屏 / 打开别的页面',
490
+ screensHd: '屏幕',
491
+ screensAll: '全部 {n} 屏',
492
+ screensN: '{name} · {n} 屏',
493
+ openOther: '打开别的页面…',
494
+ phoneOnTip: '手机预览开着',
495
+ lookSet: '整套',
496
+ lookStyle: '换风格',
497
+ lookBrand: '品牌',
498
+ lookColorTip: '点色块直接改,所有屏一起变',
499
+ lookTokTip: '点一下放进输入框,跟模型说怎么改',
500
+ lookTw: '调节滑块',
501
+ lookTwNone: '还没有专属滑块(留白、圆角、排法……)。',
502
+ lookN: '改了 {n} 处,还没保存',
503
+ lookSave: '直接保存',
504
+ lookSaveTip: '写进设计文件(不经过模型;改前改后各存一版)',
505
+ lookAskHead: '〔外观〕我在右侧外观面板里把这几个设计变量改成了:',
506
+ lookAskTail: '照这个改:改 tokens.css(或样式里 :root 上)的默认值,页面里写死的同一种颜色也换成变量。',
507
+ lookInspire: '灵感库',
508
+ lookRef: '照参考图做…',
509
+ 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: '区块',
510
+ tkn_bg: '背景', tkn_surface: '卡片底', tkn_text: '正文字', tkn_muted: '次要字', tkn_line: '分隔线', tkn_primary: '主色', tkn_accent: '强调色', tkn_onPrimary: '主色上的字', tkn_link: '链接', tkn_danger: '警示色', tkn_success: '成功色', tkn_warning: '提醒色',
511
+ tkn_xs: '注释', tkn_sm: '小字', tkn_md: '中号字', tkn_body: '正文', tkn_lg: '大字', tkn_title: '标题', tkn_display: '大标题',
512
+ tkn_rSm: '小圆角', tkn_rMd: '中圆角', tkn_rLg: '大圆角', tkn_rFull: '全圆', tkn_r: '圆角',
513
+ tkn_space: '间距', tkn_gutter: '页边距',
514
+ tkn_fHead: '标题字体', tkn_fBody: '正文字体', tkn_fMono: '等宽字体',
515
+ tkn_shadow: '阴影', tkn_light: '浅', tkn_dark: '深', tkn_hover: '悬停',
516
+ phoneOnShort: '开着',
517
+ sizesNoShort: '固定画幅',
411
518
  working: '模型正在工作',
412
519
  asking: '模型在等你回答(在对话里)',
413
520
  writing: '正在写 {file}',
521
+ curLayout: '排版',
522
+ curColor: '配色',
523
+ curCopy: '文案',
524
+ stThink: '在想…',
525
+ stNew: '正在做第 {n} 屏',
526
+ stEdit: '正在改「{name}」',
527
+ stWriteAny: '正在改文件',
528
+ stCheck: '正在体检',
529
+ stLook: '正在看效果',
530
+ stPreview: '正在打开预览',
531
+ stBrand: '正在提取品牌',
532
+ stCode: '正在转成代码',
533
+ stCompare: '正在逐像素对比',
534
+ stShell: '正在跑命令',
535
+ stRead: '正在看文件',
536
+ stWeb: '正在查资料',
537
+ stSkill: '正在看设计规范',
538
+ stTime: '已用时 {time}',
539
+ stLines: '已写 {n} 行',
414
540
  born: '已生成 {name}',
415
541
  updated: '已更新{file}',
416
542
  updatedN: '已更新{file} · {n} 处改动',
417
543
  updatedWhole: '已更新{file} · 整页重排',
418
- step1: '让模型做一个网页、App 界面或海报',
419
- step2: '点「选择」,点元素或拖出一块区域',
420
- step3: '在输入框补一句要怎么改,发出去',
421
- tryHead: '不知道怎么开口?点一句试试',
544
+ tryHead: '试试这样说',
422
545
  try1: '帮我设计一个社区健身房的会员 App',
423
546
  try2: '做一个精品咖啡店的落地页',
424
547
  try3: '做一张周末市集的活动海报',
@@ -427,7 +550,7 @@ window.__ModuleLoader__.load({
427
550
  copied: '已复制,粘贴到输入框里',
428
551
  failed: '没放进去:{msg}',
429
552
  openFailed: '打不开:{msg}',
430
- headerTip: '设计工作台',
553
+ headerTip: '设计台',
431
554
  tweaks: '调节滑块',
432
555
  twTip: '模型给这套设计做的几个维度:拖的时候所有屏一起变,还没写进文件。',
433
556
  twEmpty: '这套设计还没有专属滑块。让模型挑几个最值得试的维度(留白、圆角、主色、排法……),做成滑块放在这里。',
@@ -439,15 +562,16 @@ window.__ModuleLoader__.load({
439
562
  twAskTail: '照这个改设计(改样式源文件,tweaks.json 里对应的 value 也改成新值)。',
440
563
  twAskTip: '让模型照这几个值改设计(可以顺手调整相关的地方)',
441
564
  twSaveTip: 'CSS 变量写进 tokens.css 的默认值,属性写到每屏的 <html> 上(改前改后各存一版)',
442
- twNoWrite: '开发服务器的页面不能直接写回,交给模型改',
565
+ twNoWrite: '开发服务器的页面不能直接保存,发到对话让模型改',
566
+ twVer: '正在看旧版本:回到当前版本再保存',
443
567
  twMoved: '设计已经换了,没写回;再拖一次',
444
- twSaved: '已写回 {n} 项',
445
- twMissed: '{names} 在样式里没找到默认值,没写回:交给模型改',
568
+ twSaved: '已保存 {n} 项',
569
+ twMissed: '{names} 在样式里没找到默认值,没保存:发到对话让模型改',
446
570
  systems: '品牌设计系统',
447
571
  sysOpen: '品牌设计系统…',
448
- sysEmptyLine: '有自己的品牌?',
572
+ sysEmptyLine: '用自己的品牌:',
449
573
  sysTip: '从你的网站或项目里提取的颜色、字体、圆角和组件。设为默认后,做新设计都照它来。',
450
- sysEmpty: '还没有。在下面填你的网站地址或项目文件夹,让模型提一套。',
574
+ sysEmpty: '还没有。在上面填你的网站地址或项目文件夹,让模型提一套。',
451
575
  sysDefault: '默认',
452
576
  sysSet: '设为默认',
453
577
  sysUnset: '不再默认',
@@ -461,7 +585,9 @@ window.__ModuleLoader__.load({
461
585
  handoff: '转成代码',
462
586
  hoTip: '把设计做成真项目的代码:先整理交接说明和截图,再一屏一屏写,每屏写完和设计稿逐像素比,对上了再做下一屏。',
463
587
  hoFw: '用什么做',
464
- hoSame: '跟现有项目一样({label})',
588
+ hoSame: '跟现有项目一样:{label}',
589
+ hoIn: '在 {dir} 里',
590
+ hoEdit: '点一下改路径',
465
591
  hoHtml: '纯 HTML / CSS',
466
592
  hoUni: 'uni-app(小程序 + 网页版)',
467
593
  hoWeapp: '微信原生小程序',
@@ -480,10 +606,16 @@ window.__ModuleLoader__.load({
480
606
  // 设计库
481
607
  library: '设计库',
482
608
  libBack: '回到画布',
609
+ homeBtn: '首页',
610
+ dHome: '回首页:新做一套、接着做别的、逛设计库',
611
+ homeBack: '回到「{name}」',
612
+ askNewLine: '(另起一套新设计,放新文件夹里,别改现在这套)',
613
+ askNewNote: '新做一套,现在这套不会被改',
614
+ wkHere: '正在看',
483
615
  libMarket: '市场',
484
616
  libWorks: '我的作品',
485
617
  libInspire: '灵感',
486
- libLine: '以前做的设计、存下的灵感、朋友给的设计包',
618
+ libLine: '以前做的设计、存下的灵感、朋友给的设计包:',
487
619
  libOpen: '打开设计库',
488
620
  libClose: '返回',
489
621
  libFail: '没办成:{msg}',
@@ -497,7 +629,10 @@ window.__ModuleLoader__.load({
497
629
  mkOffline: '连不上市场,下面是上次看到的。',
498
630
  mkOff: '「设计库不联网」开着:不访问任何网址,只显示下载过的。要联网,去「插件」页关掉「设计库不联网」。',
499
631
  mkNeedUpdate: '市场里有新东西要新版设计台才能看:去「插件」页点「更新」。',
500
- mkAt: '目录 {ago} 取的',
632
+ mkAt: '目录更新于 {ago}',
633
+ mkUse0: '可商用',
634
+ mkUseBY: '可商用 · 要署名',
635
+ mkSortT: '排序',
501
636
  mkSeedNote: '这是设计台自带的一批',
502
637
  mkRefresh: '刷新',
503
638
  mkTypeTpl: '模板',
@@ -554,6 +689,9 @@ window.__ModuleLoader__.load({
554
689
  wkNow: '正在看',
555
690
  wkOpen: '打开',
556
691
  wkReveal: '在文件夹里显示',
692
+ wkRevealShort: '文件夹',
693
+ wkZipShort: 'zip',
694
+ wkShareShort: '分享',
557
695
  wkPin: '置顶',
558
696
  wkUnpin: '取消置顶',
559
697
  wkZip: '打成 zip',
@@ -572,7 +710,7 @@ window.__ModuleLoader__.load({
572
710
  agoHour: '{n} 小时前',
573
711
  agoDay: '{n} 天前',
574
712
  inMyStyles: '我的风格',
575
- inSaveStyle: '把当前设计存成风格',
713
+ inSaveStyle: '存当前设计',
576
714
  inSaveStyleTip: '存下现在这套的配色、字体、圆角,以后做新设计一键用',
577
715
  inStyleSaved: '已存成风格「{name}」',
578
716
  inStyleNone: '这套设计没用设计变量(tokens.css),存不了风格',
@@ -644,7 +782,8 @@ window.__ModuleLoader__.load({
644
782
  exZip: '打成 zip(发给朋友)',
645
783
  exShare: '分享到设计市场',
646
784
  shTitle: '分享到设计市场',
647
- shTip: '交上去的东西是公开的:别人能看、能拿去用。先查一遍,过了打成 zip,再到 GitHub 上交(要有 GitHub 账号)。',
785
+ shGh: '投稿要有 GitHub 账号',
786
+ shTip: '交上去的东西是公开的:别人能看、能拿去用。先查一遍,过了打成 zip,再到 GitHub 上交。',
648
787
  shName: '名字',
649
788
  shDesc: '一句话介绍',
650
789
  shDescPh: '比如:街角茶饮店的三屏点单 App',
@@ -653,7 +792,7 @@ window.__ModuleLoader__.load({
653
792
  shAgreePrivate: '里面没有真实的个人信息(手机号、邮箱、证件号、地址、真人照片)',
654
793
  shAgreeRules: '同意按选的授权公开;上架后随时能撤,但别人已经复制走的收不回',
655
794
  shBYNote: 'CC BY 的署名就是你的 GitHub 用户名。',
656
- shGo: '检查并打包',
795
+ shGo: '检查并投稿',
657
796
  shChecking: '在查…',
658
797
  shOk: '查过了,打好了:{name}(在「下载」里)',
659
798
  shSteps: '还差三步:',
@@ -673,10 +812,44 @@ window.__ModuleLoader__.load({
673
812
  title: 'Design Studio',
674
813
  blockedLink: 'Blocked an outside link: {href} (designs from the gallery cannot leave the page)',
675
814
  blockedDownload: 'Blocked a download (designs from the gallery cannot download files)',
676
- guideDesc: 'Live page, pick what to change',
815
+ tabTitle: 'Design',
816
+ guideDesc: 'Design pages, apps and posters; click to change, then turn into code',
677
817
  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:',
818
+ emptyHead: 'What do you want to make?',
819
+ emptySub: 'Describe it in a sentence. Screens appear on the canvas one by one; click anything to change it, then turn it into code.',
820
+ homeTab: 'Start',
821
+ askPh: 'e.g. {ex}',
822
+ askTab: 'use this',
823
+ exAuto: 'An ordering mini program for a corner noodle shop, 3 screens, warm hand-made style',
824
+ exWeb: 'Home page for an independent coffee roaster: signature beans, store map, booking',
825
+ exApp: 'Member app for a neighborhood gym: classes, check-ins, membership card',
826
+ exMini: 'Ordering mini program for a noodle shop: menu, cart, pickup number',
827
+ exTablet: 'Tablet app for kindergarten teachers: roll call, timetable, parent messages',
828
+ exDesktop: 'Desktop bookkeeping app for a small team: transactions, reports, settings',
829
+ exPoster: 'Poster for a weekend night run in the city, fresh sporty style',
830
+ exSlides: 'Five-slide launch deck: problem, solution, numbers, pricing, next steps',
831
+ exEmail: 'Birthday-month offer email for members: one big image, a short note, one button',
832
+ askKindLine: 'Type: {k}',
833
+ askMore: 'More ways to start',
834
+ askMoreD: 'Open existing HTML, use your own brand, open from the library',
835
+ askOpenHtml: 'Open existing HTML…',
836
+ askBrand: 'Use your own brand',
837
+ askLib: 'Open from the library',
838
+ askRef: 'Reference',
839
+ askRefD: 'Design from a screenshot or mockup: pick one, or drop / paste it',
840
+ askStyleAuto: 'Style: auto',
841
+ askStyle: 'Style: {name}',
842
+ askStyleD: 'Pick a style first, or one is picked for the subject',
843
+ askStylePick: 'Pick a style',
844
+ askStyleAutoBtn: 'Auto (picked for the subject)',
845
+ askSend: 'Start designing',
846
+ askSent: 'Sent. The first screen is on its way',
847
+ homeRecent: 'Continue',
848
+ homeRecentAll: 'All designs →',
849
+ waysHtml: 'Open existing HTML',
850
+ moreWays: 'Other ways to start',
851
+ emptyHint: 'Enter a file path or a local port (like 5173)',
852
+ emptyHintPick: 'Click "Browse…", drop it here, or enter a file path / local port (like 5173)',
680
853
  browse: 'Browse…',
681
854
  browseTip: 'Pick a local HTML file to open',
682
855
  pathNone: "Couldn't get this file's path. Type its path to open it.",
@@ -726,7 +899,7 @@ window.__ModuleLoader__.load({
726
899
  refPick: 'Restyle from a reference…',
727
900
  refChoose: 'Choose one',
728
901
  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',
902
+ refEmpty: 'Design from a reference image: drop it, paste it, or',
730
903
  refDrop: 'Drop to use as a reference',
731
904
  refDropSub: 'Saved to refs/ in the design folder; its colors go into the input',
732
905
  refNotImage: 'Images only (PNG / JPG / WebP / GIF)',
@@ -772,9 +945,10 @@ window.__ModuleLoader__.load({
772
945
  gShadow: 'Shadows',
773
946
  gOther: 'Other',
774
947
  exportT: 'Export',
775
- exFrame: 'This screen · in frame',
776
- exRaw: 'This screen · plain',
777
- exLong: 'This screen · full page',
948
+ exFrame: '{name} · in frame',
949
+ exRaw: '{name} · plain',
950
+ exLong: '{name} · full page',
951
+ exThisName: 'This screen',
778
952
  exShow: 'Showcase of all screens',
779
953
  exPdf: 'PDF · one page per screen',
780
954
  exPptx: 'PowerPoint · one slide per screen (as images)',
@@ -785,7 +959,8 @@ window.__ModuleLoader__.load({
785
959
  checking: 'Checking…',
786
960
  checkNone: 'No problems found',
787
961
  checkSum: '{s} screens · {n} problems',
788
- checkFix: 'Ask the model to fix',
962
+ checkFix: 'Send to chat',
963
+ checkFixTip: 'Send these issues to the chat for the model to fix',
789
964
  checkAgain: 'Check again',
790
965
  checkFail: 'Check failed: {msg}',
791
966
  checkNote: 'Only measurable layout problems; whether it looks good is still up to you',
@@ -884,17 +1059,19 @@ window.__ModuleLoader__.load({
884
1059
  cmpNow: 'Now',
885
1060
  dark: 'Dark mode',
886
1061
  variants: 'Two more takes',
887
- pickThis: 'Use this one: move the other takes to drafts/ and build the rest',
1062
+ pickThis: 'Use this take: move the other takes to drafts/ and build the rest',
888
1063
  pickShort: 'Use this',
1064
+ takeN: 'Take {l}',
889
1065
  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
1066
  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
1067
  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
1068
  exThis: 'Export this screen',
893
1069
  demo: 'Play: tap buttons in the page to move between screens',
894
- demoOn: 'Playing · tap to navigate',
1070
+ demoOn: 'Playing',
1071
+ demoHow: 'tap buttons to move',
895
1072
  demoBack: 'Back',
896
1073
  demoRestart: 'Start over',
897
- demoExit: 'Stop playing (Esc)',
1074
+ demoExit: 'Stop playing',
898
1075
  textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
899
1076
  mark: 'Mark up: draw circles, arrows and notes on the page, then hand it to the model as one image',
900
1077
  mkPen: 'Pen',
@@ -909,7 +1086,7 @@ window.__ModuleLoader__.load({
909
1086
  mkUndo: 'Undo',
910
1087
  mkRedo: 'Redo',
911
1088
  mkClear: 'Clear all',
912
- mkHint: 'Draw on the page, then send it to the model',
1089
+ mkHint: 'Draw on the page, then click Send to chat',
913
1090
  mkHow_pen: 'Press and drag to draw freehand',
914
1091
  mkHow_hl: 'Drag over what needs changing to highlight it',
915
1092
  mkHow_arrow: 'Press at the start and drag to what you mean',
@@ -920,13 +1097,13 @@ window.__ModuleLoader__.load({
920
1097
  mkCoach: 'Draw right on the page',
921
1098
  mkCoach1: 'Pick a tool above: pen, arrow, circle, text…',
922
1099
  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',
1100
+ mkCoach3: 'Then click "Send to chat" and add a line on what to do',
924
1101
  mkCancel: 'Cancel',
925
- mkSend: 'Send to model',
1102
+ mkSend: 'Send to chat',
926
1103
  mkCopy: 'Copy',
927
1104
  mkCopyTip: 'Copy the marked-up image to paste somewhere else',
928
1105
  mkCopied: 'Markup image copied. Paste it anywhere',
929
- mkCopyFail: 'Could not copy to the clipboard. Click Copy again, or Send to model',
1106
+ mkCopyFail: 'Could not copy to the clipboard. Click Copy again, or Send to chat',
930
1107
  mkSending: 'Building the image…',
931
1108
  mkTextPh: 'Type a note, Enter to finish',
932
1109
  mkEmpty: 'Nothing drawn yet',
@@ -937,7 +1114,7 @@ window.__ModuleLoader__.load({
937
1114
  mkIn: 'Markup added to the input. Say what to change, then send',
938
1115
  mkMany: 'Too many screens marked: captured the first {n}, the rest are gray',
939
1116
  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',
1117
+ mkNoInput: 'Could not add it to the input. Click the chat input, then Send to chat again',
941
1118
  textSaved: 'Saved to {file}',
942
1119
  textAsk: 'Change the text "{before}" to "{after}" on the page ({where}; only this text, nothing else).\n',
943
1120
  styleEdit: 'Style: click an element, then drag sliders for size, colors, radius and spacing (Esc to stop)',
@@ -961,14 +1138,14 @@ window.__ModuleLoader__.load({
961
1138
  styleVar: 'Token {name}',
962
1139
  styleGradient: 'It is a gradient now; picking a color makes it solid',
963
1140
  styleReset: 'Reset',
964
- styleAsk: 'Ask the model',
965
- styleSave: 'Save to file',
1141
+ styleAsk: 'Send to chat',
1142
+ styleSave: 'Save',
966
1143
  styleSaveTip: 'Writes into this element\'s style attribute in the source (a version is kept before and after)',
967
1144
  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',
1145
+ styleNoWrite: 'Dev-server pages cannot be saved here; send it to the chat',
969
1146
  styleBusyTip: 'The model is working; save when it stops',
970
1147
  styleSaved: 'Style saved to {file}',
971
- styleMiss: 'Could not find this element in the source (maybe a script made it); asking the model instead',
1148
+ styleMiss: 'Could not find this element in the source (maybe a script made it); sending it to the chat instead',
972
1149
  styleN: '{n} changed',
973
1150
  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
1151
  ruleFrom: '- {props}: currently set by {sel} ({at})',
@@ -979,11 +1156,13 @@ window.__ModuleLoader__.load({
979
1156
  styleArrow: '{prop} {from} → {to}',
980
1157
  styleJoin: '; ',
981
1158
  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',
1159
+ stylesHead: 'Or pick a style first',
1160
+ stylesTip: 'Open one to preview, click "Use this style", then say what to make',
1161
+ stylesMore: 'More styles ({n})',
1162
+ stylesLess: 'Show less',
984
1163
  stylesOpen: 'Try another style…',
985
1164
  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)',
1165
+ 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
1166
  stylesAll: 'All styles',
988
1167
  stylePrev: 'Previous (←)',
989
1168
  styleNext: 'Next (→)',
@@ -1009,6 +1188,7 @@ window.__ModuleLoader__.load({
1009
1188
  styleFonts: 'headings {head}; body {body}',
1010
1189
  styleShape: 'radius {r}px; shadow {s}',
1011
1190
  styleStart: 'Design in this style (put these into tokens.css first; no need to ask about the feel again):',
1191
+ styleAuto: 'Home picked this style card for the subject; I did not choose it. If you build the full set right away, use its colors and fonts (put them into tokens.css); if you start with three directions, make direction A in this style and pick two different feels from the style cards for B and C. If my message names another style, or there is a default brand design system, follow that instead:',
1012
1192
  styleRestyle: 'Restyle the current design to this: update tokens.css first (colors, fonts, radius, shadows) so every screen follows; replace hard-coded colors in the pages with tokens; keep layout and copy for now.\n',
1013
1193
  demoNone: 'This screen has no links to other screens yet. Ask the model to connect screens with <a href="detail.html">',
1014
1194
  phone: 'View on phone',
@@ -1026,29 +1206,110 @@ window.__ModuleLoader__.load({
1026
1206
  phoneAuto: 'Only a phone that scanned this code can open it. Closes after 30 minutes idle.',
1027
1207
  phoneNoLan: 'This computer is not on Wi-Fi or a wired network, so a phone cannot reach it.',
1028
1208
  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.",
1209
+ 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.',
1210
+ phoneTrouble: "Won't open?",
1030
1211
  zoomIn: 'Zoom in',
1031
1212
  zoomOut: 'Zoom out',
1032
1213
  fit: 'Fit',
1033
1214
  openScreen: 'Double-click to view this screen alone',
1034
- generating: 'Generating…',
1215
+ generating: 'Designing…',
1035
1216
  waiting: 'The model is working. The page shows up here once it is written',
1217
+ waitingLive: 'The page is being written piece by piece—click to edit once it’s done',
1036
1218
  refresh: 'Reload',
1037
1219
  external: 'Open in browser',
1038
1220
  more: 'More',
1039
1221
  demoShort: 'Play',
1222
+ dPick: 'Click an element or drag a region to send it to the model',
1223
+ dText: 'Click any text in the page and type, Enter to save',
1224
+ dStyle: 'Click an element, then drag sliders for size, colors, radius and spacing',
1225
+ dMark: 'Draw circles, arrows and notes, sent to the model as one image',
1226
+ dPlay: 'Click through it like a real app; buttons jump to their screens',
1227
+ dLib: 'Saved designs, styles and templates, plus the design market',
1228
+ dLook: 'Change colors and fonts, or switch the whole style',
1229
+ dCheck: 'Find overflowing or overlapping text, low contrast and tiny tap targets',
1230
+ dCheckN: '{n} problems last time. Click to see them',
1231
+ dExport: 'Build as code, save images / PDF / PPT, or pack and share',
1232
+ dMore: 'Versions, view on phone, refresh, open in browser',
1233
+ dZoom: 'Hold {mod} and scroll to zoom too',
1234
+ dScreen: 'Switch screens, see all, sizes or type',
1235
+ toolsT: 'Tools',
1040
1236
  editAddr: 'Change address',
1237
+ textShort: 'Text',
1238
+ styleShort: 'Style',
1239
+ markShort: 'Draw',
1240
+ viewT: 'View',
1241
+ canvasShort: 'Canvas',
1242
+ singleShort: 'Single',
1243
+ sizesShort: 'Sizes',
1244
+ look: 'Look',
1245
+ lookTip: 'Look: colors, fonts and overall style',
1246
+ deliver: 'Export',
1247
+ deliverTip: 'Export: build as code, save images / PDF / PPT, pack and share',
1248
+ exFiles: 'Save as file',
1249
+ checkShort: 'Check',
1250
+ zoomT: 'Zoom',
1251
+ zoomReal: 'Actual size',
1252
+ actX: 'Export',
1253
+ actV: 'More takes',
1254
+ actO: 'Open',
1255
+ deliverCode: 'Code',
1256
+ deliverShare: 'Share',
1257
+ screensT: 'Switch screen / open another page',
1258
+ screensHd: 'Screens',
1259
+ screensAll: 'All {n}',
1260
+ screensN: '{name} · {n} screens',
1261
+ openOther: 'Open another page…',
1262
+ phoneOnTip: 'Phone preview is on',
1263
+ lookSet: 'Overall',
1264
+ lookStyle: 'Restyle',
1265
+ lookBrand: 'Brand',
1266
+ lookColorTip: 'Click a swatch to change it; every screen follows',
1267
+ lookTokTip: 'Click to put it in the input, then tell the model how to change it',
1268
+ lookTw: 'Tweaks',
1269
+ lookTwNone: 'No tweaks for this design yet (spacing, corners, layout…).',
1270
+ lookN: '{n} change(s), not saved',
1271
+ lookSave: 'Save',
1272
+ lookSaveTip: 'Write into the design files (no model; a version is kept before and after)',
1273
+ lookAskHead: '[Look] I changed these design tokens in the Look panel:',
1274
+ 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.',
1275
+ lookInspire: 'Inspiration',
1276
+ lookRef: 'From a reference image…',
1277
+ 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',
1278
+ 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',
1279
+ tkn_xs: 'Caption', tkn_sm: 'Small', tkn_md: 'Medium', tkn_body: 'Body', tkn_lg: 'Large', tkn_title: 'Title', tkn_display: 'Display',
1280
+ tkn_rSm: 'Small corners', tkn_rMd: 'Medium corners', tkn_rLg: 'Large corners', tkn_rFull: 'Pill', tkn_r: 'Corners',
1281
+ tkn_space: 'Spacing', tkn_gutter: 'Page margin',
1282
+ tkn_fHead: 'Heading font', tkn_fBody: 'Body font', tkn_fMono: 'Mono font',
1283
+ tkn_shadow: 'Shadow', tkn_light: 'light', tkn_dark: 'dark', tkn_hover: 'hover',
1284
+ phoneOnShort: 'On',
1285
+ sizesNoShort: 'Fixed size',
1041
1286
  working: 'The model is working',
1042
1287
  asking: 'The model is waiting for your answer (in the chat)',
1043
1288
  writing: 'Writing {file}',
1289
+ curLayout: 'Layout',
1290
+ curColor: 'Color',
1291
+ curCopy: 'Copy',
1292
+ stThink: 'Thinking…',
1293
+ stNew: 'Designing screen {n}',
1294
+ stEdit: 'Editing "{name}"',
1295
+ stWriteAny: 'Editing files',
1296
+ stCheck: 'Checking the design',
1297
+ stLook: 'Looking at the result',
1298
+ stPreview: 'Opening the preview',
1299
+ stBrand: 'Extracting the brand',
1300
+ stCode: 'Turning it into code',
1301
+ stCompare: 'Comparing pixel by pixel',
1302
+ stShell: 'Running a command',
1303
+ stRead: 'Reading files',
1304
+ stWeb: 'Looking things up',
1305
+ stSkill: 'Reading the design guide',
1306
+ stTime: '{time} elapsed',
1307
+ stLines: '{n} lines so far',
1044
1308
  born: 'Generated {name}',
1045
1309
  updated: 'Updated{file}',
1046
1310
  updatedN: 'Updated{file} · {n} changes',
1047
1311
  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',
1312
+ tryHead: 'Try saying',
1052
1313
  try1: 'Design a member app for a neighborhood gym',
1053
1314
  try2: 'Make a landing page for a specialty coffee shop',
1054
1315
  try3: 'Make a poster for a weekend street market',
@@ -1069,15 +1330,16 @@ window.__ModuleLoader__.load({
1069
1330
  twAskTail: 'Change the design to match (edit the style sources and update the matching values in tweaks.json).',
1070
1331
  twAskTip: 'Let the model apply these values (and adjust related details)',
1071
1332
  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',
1333
+ twNoWrite: 'Dev-server pages cannot be saved here. Send it to the chat instead',
1334
+ twVer: 'Viewing an old version. Go back to the current one to save',
1073
1335
  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',
1336
+ twSaved: 'Saved {n}',
1337
+ twMissed: 'No default found in the styles for {names}, not saved. Send it to the chat instead',
1076
1338
  systems: 'Brand design systems',
1077
1339
  sysOpen: 'Brand design systems…',
1078
- sysEmptyLine: 'Have your own brand?',
1340
+ sysEmptyLine: 'Use your own brand:',
1079
1341
  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.',
1342
+ sysEmpty: 'None yet. Enter your website or project folder above and let the model extract one.',
1081
1343
  sysDefault: 'Default',
1082
1344
  sysSet: 'Make default',
1083
1345
  sysUnset: 'Stop using',
@@ -1091,7 +1353,9 @@ window.__ModuleLoader__.load({
1091
1353
  handoff: 'Build as code',
1092
1354
  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
1355
  hoFw: 'Build with',
1094
- hoSame: 'Same as the project ({label})',
1356
+ hoSame: 'Same as the project: {label}',
1357
+ hoIn: 'in {dir}',
1358
+ hoEdit: 'Click to edit the path',
1095
1359
  hoHtml: 'Plain HTML / CSS',
1096
1360
  hoUni: 'uni-app (mini program + H5)',
1097
1361
  hoWeapp: 'WeChat native mini program',
@@ -1109,10 +1373,16 @@ window.__ModuleLoader__.load({
1109
1373
  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
1374
  library: 'Library',
1111
1375
  libBack: 'Back to canvas',
1376
+ homeBtn: 'Home',
1377
+ dHome: 'Go home: start a new design, pick up another one, browse the library',
1378
+ homeBack: 'Back to "{name}"',
1379
+ askNewLine: '(Start a new design in a new folder; leave the current one as is.)',
1380
+ askNewNote: 'This starts a new design; the current one stays as is',
1381
+ wkHere: 'Open now',
1112
1382
  libMarket: 'Gallery',
1113
1383
  libWorks: 'My designs',
1114
1384
  libInspire: 'Inspiration',
1115
- libLine: 'Past designs, saved inspiration, design packs from friends',
1385
+ libLine: 'Past designs, saved inspiration, design packs from friends:',
1116
1386
  libOpen: 'Open the library',
1117
1387
  libClose: 'Back',
1118
1388
  libFail: 'That did not work: {msg}',
@@ -1126,7 +1396,10 @@ window.__ModuleLoader__.load({
1126
1396
  mkOffline: 'Cannot reach the gallery; showing what you saw last time.',
1127
1397
  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
1398
  mkNeedUpdate: 'Some gallery items need a newer Design Studio: click Update on the Plugins page.',
1129
- mkAt: 'Catalog fetched {ago}',
1399
+ mkAt: 'Catalog updated {ago}',
1400
+ mkUse0: 'Free to use',
1401
+ mkUseBY: 'Free to use · credit',
1402
+ mkSortT: 'Sort',
1130
1403
  mkSeedNote: 'This is the set bundled with the studio',
1131
1404
  mkRefresh: 'Refresh',
1132
1405
  mkTypeTpl: 'Templates',
@@ -1183,6 +1456,9 @@ window.__ModuleLoader__.load({
1183
1456
  wkNow: 'Open now',
1184
1457
  wkOpen: 'Open',
1185
1458
  wkReveal: 'Show in folder',
1459
+ wkRevealShort: 'Folder',
1460
+ wkZipShort: 'Zip',
1461
+ wkShareShort: 'Share',
1186
1462
  wkPin: 'Pin',
1187
1463
  wkUnpin: 'Unpin',
1188
1464
  wkZip: 'Save as zip',
@@ -1201,7 +1477,7 @@ window.__ModuleLoader__.load({
1201
1477
  agoHour: '{n} h ago',
1202
1478
  agoDay: '{n} d ago',
1203
1479
  inMyStyles: 'My styles',
1204
- inSaveStyle: 'Save this design as a style',
1480
+ inSaveStyle: 'Save current',
1205
1481
  inSaveStyleTip: 'Keep its colors, fonts and corners to reuse in one click',
1206
1482
  inStyleSaved: 'Saved as style "{name}"',
1207
1483
  inStyleNone: 'This design has no design tokens (tokens.css), so there is no style to save',
@@ -1273,7 +1549,8 @@ window.__ModuleLoader__.load({
1273
1549
  exZip: 'Save as zip (to send)',
1274
1550
  exShare: 'Share to the gallery',
1275
1551
  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).',
1552
+ shGh: 'You need a GitHub account to submit',
1553
+ 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
1554
  shName: 'Name',
1278
1555
  shDesc: 'One-line description',
1279
1556
  shDescPh: 'e.g. A three-screen ordering app for a corner tea shop',
@@ -1282,7 +1559,7 @@ window.__ModuleLoader__.load({
1282
1559
  shAgreePrivate: 'It contains no real personal data (phone, email, ID numbers, address, photos of real people)',
1283
1560
  shAgreeRules: 'I agree to publish it under the chosen license; it can be taken down any time, but copies others already made stay',
1284
1561
  shBYNote: 'For CC BY, the credit is your GitHub username.',
1285
- shGo: 'Check and save',
1562
+ shGo: 'Check and submit',
1286
1563
  shChecking: 'Checking…',
1287
1564
  shOk: 'Checked and saved: {name} (in Downloads)',
1288
1565
  shSteps: 'Three more steps:',
@@ -1315,6 +1592,8 @@ window.__ModuleLoader__.load({
1315
1592
  // ── 样式(DSH 设计变量,深浅主题跟着走)──────────────────────────────────
1316
1593
 
1317
1594
  const ACCENT = '#4d6bfe'
1595
+ const TOP_R = 52 // 画布上面一排浮块占的高度:单页、演示、对比让开它,画布排的时候也让开
1596
+ const BOT_R = 64 // 底下一排(工具坞、视图、缩放)占的高度
1318
1597
  const PICK_SRC = 'dsh-design-studio' // 输入框里点选标签的来源名(样式里也用)
1319
1598
  // 输入框里的各种标签:图标按来源名分(DSH 的标签上只带来源名),消息里和悬停卡片按 data-i
1320
1599
  const CHIP_ICON = {
@@ -1335,11 +1614,73 @@ window.__ModuleLoader__.load({
1335
1614
  const EASE = 'cubic-bezier(.2,.8,.2,1)'
1336
1615
  const CSS = `
1337
1616
  .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}
1617
+ /* 浮在画布上的几块:上面一排(左上 设计库 + 屏名,右上 外观 体检 交付 更多)、底部中间工具坞、左下视图和类型、右下缩放 */
1618
+ .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);
1619
+ box-shadow:0 6px 20px rgba(0,0,0,.12),0 0 0 .5px var(--dsw-alias-border-l3)}
1620
+ .ddp-isl .ddp-btn{height:30px;min-width:30px;border-radius:8px}
1621
+ .ddp-isl .ddp-btn svg{width:16px;height:16px}
1622
+ .ddp-isl .ddp-btn .cr{width:10px;height:10px}
1623
+ .ddp-isl .ddp-sep{height:18px;margin:0 3px}
1624
+ .ddp-isl .ddp-menu{top:calc(100% + 8px)}
1625
+ .ddp-top{position:absolute;left:10px;right:10px;top:10px;z-index:9;display:flex;align-items:flex-start;gap:8px;pointer-events:none}
1626
+ .ddp-top>.ddp-isl{pointer-events:auto}
1627
+ .ddp-top>.tl{flex:0 1 auto;min-width:0}
1628
+ .ddp-top>.tl.wide{flex:1 1 auto;max-width:560px}
1629
+ .ddp-top>.tl.wide .ddp-input{height:30px}
1630
+ .ddp-top>.tr{margin-left:auto;flex:none}
1631
+ .ddp-isl .ddp-btn.pri{padding:0 8px 0 9px;background:${ACCENT};color:#fff;font-weight:500}
1632
+ .ddp-isl .ddp-btn.pri:hover,.ddp-isl .ddp-btn.pri.sel{background:#3d5bf0;color:#fff}
1633
+ .ddp-isl .ddp-btn.pri .cr{opacity:.8}
1634
+ .ddp-dock{position:absolute;left:50%;bottom:10px;transform:translateX(-50%);padding:4px;gap:2px}
1635
+ .ddp-dock .ddp-sep{height:26px}
1636
+ /* 悬停说明:深底浅字(跟提示消息一个配色,深色模式自己反过来);第一行名字 + 快捷键键帽,第二行一句用途
1637
+ 说明控制在两行内,text-wrap:pretty 不让一个字单独掉到下一行 */
1638
+ .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;
1639
+ 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)}
1640
+ .ddp-tipbox.on{display:flex;animation:ddp-tip .12s ease}
1641
+ .ddp-tipbox .nm{display:flex;align-items:center;gap:10px}
1642
+ .ddp-tipbox b{font-weight:600}
1643
+ .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}
1644
+ .ddp-tipbox span{opacity:.72;text-wrap:pretty}
1645
+ @keyframes ddp-tip{from{opacity:0}}
1646
+ .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;
1647
+ color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:13px;cursor:pointer;flex:none;white-space:nowrap}
1648
+ .ddp-tool svg{width:18px;height:18px;flex:none}
1649
+ .ddp-tool:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1650
+ .ddp-tool.on,.ddp-tool.on:hover:not(:disabled){background:${ACCENT};color:#fff}
1651
+ .ddp-tool:disabled{opacity:.4;cursor:default}
1652
+ .ddp-isl .ddp-btn.lab{padding:0 9px 0 7px}
1653
+ .ddp-isl .ddp-btn .lb{font-size:12px}
1654
+ /* 左上菜单里的「类型」:四列小格,一点就换 */
1655
+ .ddp-menu.l{min-width:268px}
1656
+ .ddp-kinds{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:2px;padding:0 2px 4px}
1657
+ .ddp-menu .ddp-kinds button{flex-direction:column;justify-content:center;gap:4px;height:50px;padding:0 2px;font-size:11px;text-align:center}
1658
+ .ddp-menu .ddp-kinds button span{max-width:100%}
1659
+ .ddp-menu .ddp-kinds button.sel{background:rgba(77,107,254,.12)}
1660
+ /* 标注时它有自己的上下两条:浮块都收起来 */
1661
+ .ddp-stage:has(>.ddp-mark)>.ddp-top,.ddp-stage:has(>.ddp-mark)>.ddp-isl{display:none}
1662
+ /* 上下两排浮块让出来:提示、消息、横幅、生成中、右边的面板都挪到两排中间 */
1663
+ .ddp-stage.isl>.ddp-hint{top:58px}
1664
+ .ddp-stage.isl>.ddp-toast:not(.mk){top:58px}
1665
+ .ddp-stage.isl>.ddp-toast.low{top:104px}
1666
+ .ddp-stage.isl>.ddp-banner{top:58px}
1667
+ .ddp-stage.isl>.ddp-chip{bottom:76px}
1668
+ .ddp-stage.isl>.ddp-chip.side{left:calc((100% - 296px) / 2)}
1669
+ .ddp-stage.isl>.ddp-tokens{top:58px;max-height:calc(100% - 58px - 74px)}
1670
+ .ddp-kind.ddp-where{flex:0 1 auto;min-width:80px;max-width:320px}
1671
+ .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}
1672
+ .ddp-addr:hover,.ddp-addr.sel{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1673
+ .ddp-addr span.lb2 em{color:var(--dsw-alias-label-tertiary);font-weight:400}
1674
+ .ddp-addr span{flex:1;min-width:0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1675
+ /* 窄了先截名字,「· 3 屏」「· B」留着 */
1676
+ .ddp-addr span.lb2{display:flex;overflow:visible}
1677
+ .ddp-addr span.lb2 b{min-width:0;font-weight:inherit;overflow:hidden;text-overflow:ellipsis}
1678
+ .ddp-addr span.lb2 em{flex:none;font-style:normal;white-space:pre}
1679
+ .ddp-addr .cr{width:10px;height:10px;flex:none;opacity:.55}
1680
+ .ddp-menu.l{left:0;right:auto;max-width:min(320px,calc(100cqw - 16px))}
1681
+ .ddp-menu .ls{max-height:min(360px,55vh);overflow-y:auto}
1682
+ .ddp-menu button>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1683
+ .ddp-menu .sp{height:.5px;margin:4px 6px;background:var(--dsw-alias-border-l3)}
1343
1684
  .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
1685
  .ddp-dot.off{background:var(--dsw-alias-label-tertiary);box-shadow:none}
1345
1686
  .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 +1712,6 @@ window.__ModuleLoader__.load({
1371
1712
  .ddp-menu button.sel,.ddp-menu button.sel svg{color:${ACCENT}}
1372
1713
  .ddp-menu em{margin-left:auto;font-style:normal;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
1373
1714
  .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
1715
  .ddp-scr{position:absolute;inset:0;overflow:hidden}
1376
1716
  .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
1717
  .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 +1743,12 @@ window.__ModuleLoader__.load({
1403
1743
  .ddp-cap.w .sp{stroke:rgba(255,255,255,.25)}
1404
1744
  .ddp-cap.w .rg{stroke:#fff}
1405
1745
  .ddp-body{position:absolute;inset:0}
1406
- .ddp-body.vb{top:46px}
1746
+ .ddp-body.pad{top:${TOP_R}px;bottom:${BOT_R}px}
1747
+ .ddp-body.pad.vb{top:${TOP_R + 46}px}
1748
+ .ddp-body.pad.ease{transition:top .42s ${EASE}}
1749
+ .ddp-body.pad.ease .ddp-fit{transition:transform .42s ${EASE}}
1750
+ /* 网页单页夹在上下两排浮块中间:描一条细边,和画布底色分开 */
1751
+ .ddp-body.pad .ddp-dev.fill{box-shadow:0 0 0 .5px var(--dsw-alias-border-l3)}
1407
1752
  .ddp-single{position:absolute;inset:0;overflow:hidden}
1408
1753
  .ddp-fit{position:absolute;left:50%;top:50%;transform-origin:center center}
1409
1754
  .ddp-fit.fill{left:0;top:0;width:100%;height:100%;transform:none}
@@ -1421,15 +1766,64 @@ window.__ModuleLoader__.load({
1421
1766
  .ddp-lab span{overflow:hidden;text-overflow:ellipsis}
1422
1767
  .ddp-cell.sel .ddp-lab{color:${ACCENT};font-weight:500}
1423
1768
  .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
1769
  .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
1770
  .ddp-cell.ghost .ddp-lab{color:${ACCENT}}
1427
1771
  @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}
1772
+ /* 生成中的占位:鼠标框版面 */
1773
+ .ddp-gd{position:absolute;inset:0;z-index:2;overflow:hidden;background:var(--dsw-alias-bg-layer-1);pointer-events:none}
1774
+ .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));
1775
+ background:color-mix(in srgb,var(--c) 9%,transparent);overflow:hidden}
1776
+ .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}
1777
+ .ddp-gd .c{position:absolute;z-index:2;display:block;width:0;height:0}
1778
+ .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))}
1779
+ .ddp-gd .c path{fill:var(--c);stroke:#fff;stroke-width:1.5;stroke-linejoin:round}
1780
+ .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));
1781
+ 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;
1782
+ 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)}
1783
+ .ddp-gd .c.flip em{left:auto;right:calc(-4px / var(--z, 1))}
1784
+ /* 边写边看:正在写的那页盖在占位上,页面报到了才淡入 */
1785
+ .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}
1786
+ .ddp-draft.on{opacity:1}
1787
+ .ddp-fitd{position:absolute;inset:0;z-index:3;overflow:hidden}
1788
+ .ddp-fitd>div{position:absolute;left:50%;top:50%;transform-origin:center center}
1789
+ /* 顶上的状态条:在做什么 · 用了多久;三个点是三只鼠标的颜色 */
1790
+ .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;
1791
+ padding:0 12px 0 11px;border-radius:999px;overflow:hidden;background:rgba(18,18,24,.84);color:#f1f1f4;font-size:12px;line-height:16px;
1792
+ 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}}
1793
+ .ddp-busy.side{left:calc((100% - 296px) / 2);max-width:calc(100% - 320px)}
1794
+ .ddp-stage.isl>.ddp-busy{top:58px}
1795
+ /* 右侧栏够宽:状态条放进上排两块浮块中间,不挡画布 */
1796
+ @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}}
1797
+ .ddp-busy .dots{display:flex;gap:3px;flex:none}
1798
+ .ddp-busy .dots i{width:5px;height:5px;border-radius:50%;background:#6d87ff;animation:ddp-hop 1.2s ease-in-out infinite}
1799
+ .ddp-busy .dots i:nth-child(2){background:#ff8a4c;animation-delay:.15s}
1800
+ .ddp-busy .dots i:nth-child(3){background:#2fd3a0;animation-delay:.3s}
1801
+ .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)}
1802
+ .ddp-busy .tx{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
1803
+ 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}
1804
+ .ddp-busy.ask .tx{background:none;color:#f1f1f4;animation:none}
1805
+ .ddp-busy .sub{min-width:0;flex:0 1000 auto;color:rgba(241,241,244,.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1806
+ .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}
1807
+ .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}
1808
+ @keyframes ddp-hop{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-3px)}}
1809
+ @keyframes ddp-sweep{from{background-position:-60% 0}to{background-position:160% 0}}
1810
+ @keyframes ddp-drop{from{opacity:0;transform:translate(-50%,-8px)}}
1811
+ /* 状态条占了顶上那格:提示、消息、横幅往下挪一格 */
1812
+ .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}
1813
+ .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}
1814
+ .ddp-stage.isl.bz>.ddp-toast.low{top:146px}
1815
+ .ddp-stage.isl.bz>.ddp-tokens{max-height:calc(100% - 100px - 74px)}
1816
+ @media (prefers-reduced-motion:reduce){
1817
+ .ddp-busy,.ddp-busy *,.ddp-gd *,.ddp-gd .b::after{animation:none!important}
1818
+ .ddp-busy .bar{display:none}
1819
+ .ddp-body.pad.ease,.ddp-body.pad.ease .ddp-fit{transition:none}
1820
+ }
1821
+ .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);
1822
+ box-shadow:0 6px 20px rgba(0,0,0,.12),0 0 0 .5px var(--dsw-alias-border-l3)}
1823
+ .ddp-zoom .ddp-btn{height:30px;min-width:30px;padding:0 4px;border-radius:8px}
1824
+ .ddp-zoom .ddp-btn svg{width:15px;height:15px}
1825
+ .ddp-zoom .ddp-btn .lb{min-width:30px;text-align:right;font-variant-numeric:tabular-nums}
1826
+ .ddp-zoom .ddp-menu{top:auto;bottom:calc(100% + 8px);min-width:168px}
1433
1827
  .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
1828
  background:rgba(18,18,24,.84);color:#ececf0;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
1435
1829
  box-shadow:0 6px 18px rgba(0,0,0,.22);backdrop-filter:blur(8px);pointer-events:none;animation:ddp-in .2s ease}
@@ -1487,7 +1881,7 @@ window.__ModuleLoader__.load({
1487
1881
  .ddp-mk-bar button.go{background:${ACCENT};color:#fff;font-weight:600}
1488
1882
  .ddp-mk-bar button.go:hover:not(:disabled){background:#3d5bf0}
1489
1883
  .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}
1884
+ .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
1885
  .ddp-mk-coach b{display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#fff}
1492
1886
  .ddp-mk-coach ol{margin:0;padding:0 0 0 1.3em;color:rgba(244,244,245,.86)}
1493
1887
  .ddp-mk-coach li+li{margin-top:2px}
@@ -1499,38 +1893,30 @@ window.__ModuleLoader__.load({
1499
1893
  @keyframes ddp-shine{100%{transform:translateX(100%)}}
1500
1894
  .ddp-wait{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px}
1501
1895
  .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}
1896
+ .ddp-wait .card.k-app,.ddp-wait .card.k-miniprogram{width:auto;height:min(62%,560px);aspect-ratio:390/844;border-radius:22px}
1897
+ .ddp-wait .card.k-poster{width:auto;height:min(62%,560px);aspect-ratio:3/4}
1898
+ .ddp-wait .card.k-slides{aspect-ratio:16/9}
1899
+ .ddp-wait .card.k-email{width:min(60%,380px);aspect-ratio:3/4}
1900
+ .ddp-wait .card:has(.ddp-gd){animation:none}
1502
1901
  .ddp-wait p{margin:0;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.6}
1503
1902
  .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}
1903
+ .ddp-home form{display:flex;flex-wrap:wrap;gap:6px;width:100%;margin:0}
1522
1904
  .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
1905
  .ddp-browse:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1524
1906
  .ddp-drop .in.no{border-color:var(--dsw-alias-label-tertiary)}
1525
1907
  .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}
1908
+ .ddp-go{height:26px;padding:0 12px;border:0;border-radius:999px;background:${ACCENT};color:#fff;font:inherit;cursor:pointer}
1909
+ .ddp-go:hover{filter:brightness(.94)}
1910
+ .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))}
1911
+ .ddp-go.sec:hover{filter:none;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1912
+ .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
1913
  .ddp-err{color:var(--dsw-alias-state-error-primary,#e5484d);font-size:11.5px}
1528
1914
  .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
1915
  .ddp-head:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1530
1916
  .ddp-head svg{width:16px;height:16px}
1531
1917
  .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
1918
  .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;
1919
+ .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
1920
  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
1921
  .ddp-tokens header{display:flex;align-items:center;gap:8px;padding:10px 8px 2px 14px;font-size:13px;font-weight:500}
1536
1922
  .ddp-tokens header b{flex:1;font-weight:500}
@@ -1539,7 +1925,9 @@ window.__ModuleLoader__.load({
1539
1925
  .ddp-tokens .tip{margin:0;padding:0 14px 8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1540
1926
  .ddp-tokens .bd{flex:1;min-height:0;overflow:auto;padding:0 8px 10px}
1541
1927
  .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)}
1928
+ .ddp-phone details{margin-top:4px;text-align:left}
1929
+ .ddp-phone details summary{cursor:pointer;text-align:center}
1930
+ .ddp-phone details p{margin-top:4px}
1543
1931
  .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
1932
  .ddp-phone .bd>*{flex:none}
1545
1933
  .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 +1948,9 @@ window.__ModuleLoader__.load({
1560
1948
  .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
1949
  .ddp-tokens footer button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
1562
1950
  .ddp-tokens footer svg{width:15px;height:15px;flex:none}
1951
+ /* 所有面板一样的禁用样子:描边、浅字,不像能点;主按钮都是 DSH 的蓝 */
1952
+ .ddp-tokens footer button:disabled{background:transparent;color:var(--dsw-alias-label-tertiary);box-shadow:none;cursor:default;opacity:1;filter:none}
1953
+ .ddp-tokens footer .go:disabled{box-shadow:inset 0 0 0 1px var(--dsw-alias-separator-primary,rgba(127,127,127,.3))}
1563
1954
  .ddp{position:relative;outline:none}
1564
1955
  .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
1956
  .ddp-drop *{pointer-events:none}
@@ -1567,9 +1958,8 @@ window.__ModuleLoader__.load({
1567
1958
  .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
1959
  .ddp-drop .in svg{width:28px;height:28px;color:${ACCENT};margin-bottom:4px}
1569
1960
  .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}
1961
+ .ddp-home .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
1962
+ .ddp-home .lk:hover{text-decoration:underline;text-underline-offset:2px}
1573
1963
  .ddp-ck{align-items:flex-start}
1574
1964
  .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
1965
  .ddp-ck .no.hi{background:#e5484d}
@@ -1577,10 +1967,10 @@ window.__ModuleLoader__.load({
1577
1967
  .ddp-tok.ddp-ck .vl{white-space:normal;overflow-wrap:anywhere}
1578
1968
  .ddp-check .ok{display:flex;align-items:center;gap:8px;padding:14px 6px;color:var(--dsw-alias-label-secondary)}
1579
1969
  .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}
1970
+ .ddp-check footer.row,.ddp-style footer.row,.ddp-look footer.row{display:flex;gap:6px}
1971
+ .ddp-check footer.row button,.ddp-style footer.row button,.ddp-look footer.row button{flex:1;justify-content:center;padding:0 6px}
1972
+ .ddp-check footer .go,.ddp-style footer .go,.ddp-look footer .go{background:${ACCENT};color:#fff}
1973
+ .ddp-check footer .go:hover,.ddp-style footer .go:hover,.ddp-look footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
1584
1974
  .ddp-check footer button:disabled,.ddp-style footer button:disabled{opacity:.45;cursor:default}
1585
1975
  .ddp-style .bd{padding:0 14px 10px}
1586
1976
  .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 +2038,114 @@ window.__ModuleLoader__.load({
1648
2038
  .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
2039
  .ddp-samview .row .ddp-go{margin-left:auto;height:28px;font-size:12px}
1650
2040
  .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)}
2041
+ /* ── 首页:顶上页签、一句问话、大输入框、接着做、模板 ── */
2042
+ .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%)}
2043
+ .ddp-homebar{flex:none;display:flex;align-items:center;gap:8px;height:48px;padding:0 12px 0 16px}
2044
+ .ddp-homebar .nm{display:flex;align-items:center;gap:8px;min-width:0}
2045
+ .ddp-homebar .nm b{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap}
2046
+ .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)}
2047
+ .ddp-homebar .seg{margin-left:auto}
2048
+ .ddp-homebar>.ddp-btn{margin-left:-6px}
2049
+ .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;
2050
+ color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;flex:0 1 auto}
2051
+ .ddp-homeback svg{width:15px;height:15px;flex:none}
2052
+ .ddp-homeback span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2053
+ .ddp-homeback:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
2054
+ .ddp-homebar.led .nm{display:none}
2055
+ /* 开着设计时叠上来的首页:淡入、轻轻放大;几块依次浮上来;收起反过来 */
2056
+ .ddp-home.over~.ddp-libwrap{z-index:11}
2057
+ .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}
2058
+ .ddp-home.over.out{animation:ddp-homeout .19s ease both;pointer-events:none}
2059
+ .ddp-home .ddp-start>*{animation:ddp-rise .5s ${EASE} both}
2060
+ .ddp-home .ddp-start>:nth-child(2){animation-delay:.05s}
2061
+ .ddp-home .ddp-start>:nth-child(3){animation-delay:.1s}
2062
+ .ddp-home .ddp-start>:nth-child(4){animation-delay:.15s}
2063
+ .ddp-home .ddp-start>:nth-child(n+5){animation-delay:.2s}
2064
+ @keyframes ddp-homein{from{opacity:0;transform:scale(.985)}}
2065
+ @keyframes ddp-homeout{to{opacity:0;transform:scale(.985)}}
2066
+ @keyframes ddp-rise{from{opacity:0;transform:translateY(10px)}}
2067
+ .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}
2068
+ .ddp-asknote svg{width:13px;height:13px;flex:none}
2069
+ .ddp-recent .wk{position:relative}
2070
+ .ddp-recent .wk.here .im{box-shadow:0 0 0 2px ${ACCENT}}
2071
+ .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)}
2072
+ @media (prefers-reduced-motion:reduce){.ddp-home.over,.ddp-home.over.out,.ddp-home .ddp-start>*{animation:none}}
2073
+ .ddp-lib>.ddp-homebar{border-bottom:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
2074
+ .ddp-homebd{flex:1;min-height:0;overflow-y:auto;padding:0 20px 36px}
2075
+ .ddp-start{width:100%;max-width:600px;margin:0 auto;display:flex;flex-direction:column;gap:32px;text-align:left}
2076
+ .ddp-hero{margin:clamp(12px,7vh,52px) 0 -14px;text-align:center}
2077
+ .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}
2078
+ .ddp-hero p{margin:8px auto 0;max-width:440px;font-size:13px;line-height:20px;text-wrap:balance;word-break:keep-all}
2079
+ .ddp-start>.ddp-go{align-self:center;margin:-16px 0}
2080
+ .ddp-askwrap{display:flex;flex-direction:column;gap:14px}
2081
+ .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}
2082
+ .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)}
2083
+ .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}
2084
+ .ddp-askta::placeholder{color:var(--dsw-alias-label-tertiary)}
2085
+ .ddp-ask>.row{display:flex;align-items:center;gap:6px;padding:6px 10px 10px}
2086
+ .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}
2087
+ .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)}
2088
+ .ddp-ask .chip>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
2089
+ .ddp-ask .chip svg{flex:none;width:14px;height:14px}
2090
+ .ddp-ask .chip .cr{width:10px;height:10px;opacity:.6}
2091
+ .ddp-ask .chip.on{background:color-mix(in srgb,${ACCENT} 13%,transparent);color:${ACCENT}}
2092
+ .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)}
2093
+ .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)}
2094
+ .ddp-askplus>.ddp-btn:hover,.ddp-askplus>.ddp-btn.sel{background:color-mix(in srgb,var(--dsw-alias-label-primary) 9%,transparent)}
2095
+ .ddp-askplus>.ddp-btn svg{width:15px;height:15px}
2096
+ .ddp-askplus .ddp-menu{left:0;right:auto;min-width:200px}
2097
+ .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}
2098
+ .ddp-ask .tabhint:hover{color:var(--dsw-alias-label-secondary)}
2099
+ .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}
2100
+ .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}
2101
+ .ddp-ask .tabhint+.send{margin-left:8px}
2102
+ .ddp-ask .send:disabled{opacity:.3;cursor:default}
2103
+ .ddp-ask .send:hover:not(:disabled){filter:brightness(1.08)}
2104
+ .ddp-ask .send svg{width:17px;height:17px}
2105
+ .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}
2106
+ .ddp-askpop .hd{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
2107
+ .ddp-askpop .hd b{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
2108
+ .ddp-askpop .hd .lk{margin-left:auto;font-size:12px}
2109
+ .ddp-askpop .ddp-styles{grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px}
2110
+ .ddp-askkinds{display:flex;flex-wrap:wrap;justify-content:center;gap:5px}
2111
+ .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}
2112
+ .ddp-askkinds button:hover{border-color:var(--dsw-alias-label-tertiary);color:var(--dsw-alias-label-primary)}
2113
+ .ddp-askkinds button.on{border-color:${ACCENT};background:color-mix(in srgb,${ACCENT} 10%,transparent);color:${ACCENT}}
2114
+ .ddp-askkinds svg{width:13px;height:13px}
2115
+ .ddp-homesec>.hd{display:flex;align-items:baseline;gap:8px;margin-bottom:12px}
2116
+ .ddp-homesec>.hd b{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
2117
+ .ddp-homesec>.hd .lk{margin-left:auto;font-size:12px}
2118
+ .ddp-recent,.ddp-tplstart .row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 12px}
2119
+ .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}
2120
+ .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}
2121
+ .ddp-recent .im{aspect-ratio:16/10}
2122
+ .ddp-tplstart .im{aspect-ratio:4/5}
2123
+ .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}
2124
+ .ddp-recent .im svg,.ddp-tplstart .im svg{width:22px;height:22px}
2125
+ .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)}
2126
+ .ddp-recent .wk:focus-visible,.ddp-tplstart .tp:focus-visible{outline:none}
2127
+ .ddp-recent .tx,.ddp-tplstart .tx{display:flex;flex-direction:column;gap:1px;min-width:0;padding:0 2px}
2128
+ .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}
2129
+ .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}
2130
+ @container (max-width:470px){
2131
+ .ddp-homebar .nm b{display:none}
2132
+ .ddp-homebd{padding:0 14px 28px}
2133
+ .ddp-hero h1{font-size:22px;line-height:30px}
2134
+ .ddp-recent,.ddp-tplstart .row{grid-template-columns:repeat(2,minmax(0,1fr))}
2135
+ .ddp-recent .wk:nth-child(3){display:none}
2136
+ .ddp-ask .tabhint{display:none}
2137
+ .ddp-ask .tabhint+.send{margin-left:auto}
2138
+ }
2139
+ @container (max-width:340px){.ddp-ask .chip.ref>span{display:none}.ddp-ask .chip.ref{padding:0 8px}}
2140
+ .ddp-more-st{display:block;margin-top:10px;font-size:12px}
2141
+ .ddp-ways{padding-top:16px;border-top:.5px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3))}
2142
+ .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}
2143
+ .ddp-ways summary::-webkit-details-marker{display:none}
2144
+ .ddp-ways summary:hover{color:var(--dsw-alias-label-primary)}
2145
+ .ddp-ways summary svg{width:12px;height:12px;transform:rotate(-90deg);transition:transform .15s ${EASE}}
2146
+ .ddp-ways[open] summary svg{transform:none}
2147
+ .ddp-ways .in{display:flex;flex-direction:column;gap:10px;margin-top:12px}
2148
+ .ddp-ways .in p{margin:0;line-height:1.55}
1659
2149
  .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
2150
  .ddp-tok:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1661
2151
  .ddp-tok .pv{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}
@@ -1699,13 +2189,18 @@ window.__ModuleLoader__.load({
1699
2189
  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
2190
  .ddp-banner>svg{width:14px;height:14px;flex:none;color:${ACCENT}}
1701
2191
  .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)}
2192
+ /* 试用横幅:「试用中」蓝字、屏名正文、提示淡;窄了先收提示 */
2193
+ .ddp-banner>b{flex:none;font-weight:600;color:${ACCENT}}
2194
+ .ddp-banner>.nm{color:var(--dsw-alias-label-primary)}
2195
+ .ddp-banner>em{flex:none;margin-right:4px;font-style:normal;color:var(--dsw-alias-label-tertiary)}
2196
+ @container (max-width:420px){.ddp-banner>em{display:none}}
2197
+ .ddp-banner .ddp-go{height:24px;padding:0 10px;font-size:12px;flex:none}
2198
+ .ddp-banner .ddp-go.sec.on{background:rgba(77,107,254,.12);color:${ACCENT};box-shadow:inset 0 0 0 1px ${ACCENT}}
1705
2199
  .ddp-banner .ddp-btn{height:24px;min-width:24px;border-radius:999px}
1706
2200
  .ddp-toast{display:flex;align-items:center;gap:10px}
1707
2201
  .ddp-toast.low{top:56px}
1708
2202
  .ddp-toast.mk{top:auto;bottom:62px}
2203
+ .ddp-toast.side{right:308px}
1709
2204
  .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
2205
  .ddp-cmp{position:absolute;inset:0;overflow:hidden}
1711
2206
  .ddp-cmp>.ly{position:absolute;inset:0}
@@ -1722,20 +2217,60 @@ window.__ModuleLoader__.load({
1722
2217
  .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
2218
  .ddp-cmp .tag.l{left:10px}
1724
2219
  .ddp-cmp .tag.r{right:10px;background:${ACCENT}}
1725
- .ddp-act{margin-left:auto;display:flex;gap:1px;flex:none}
2220
+ /* 屏头小按钮:悬停或选中那屏才出来(平时不占地方,给屏名和「用这版」留位置) */
2221
+ .ddp-act{margin-left:auto;display:none;gap:1px;flex:none}
2222
+ .ddp-cell:hover .ddp-act,.ddp-cell.sel .ddp-act{display:flex}
2223
+ .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}
2224
+ .ddp-cell.sel .ddp-vt{background:rgba(77,107,254,.14);color:${ACCENT}}
1726
2225
  .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
2226
  background:rgba(77,107,254,.12);color:${ACCENT};font:inherit;font-size:11px;font-weight:500;cursor:pointer}
1728
2227
  .ddp-take:hover{background:rgba(77,107,254,.2)}
1729
2228
  .ddp-take svg{width:12px;height:12px;flex:none}
1730
2229
  /* 画布缩得很小时只留 ✓,屏名先保住 */
1731
2230
  .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}}
2231
+ .ddp-lab>span:first-of-type{min-width:3em}
2232
+ @container (max-width:130px){.ddp-take{padding:0 4px}.ddp-take span{display:none}}
2233
+ @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
2234
  .ddp-act .ddp-btn{height:22px;min-width:22px;padding:0 4px;color:${ACCENT}}
1735
2235
  .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}}
2236
+ /* 屏头的小按钮:屏够宽就带字(导出 / 再出两版 / 单看),窄了只剩图标,悬停看说明 */
2237
+ .ddp-act .lb{display:none;font-size:11px}
2238
+ @container (min-width:300px){.ddp-act .lb{display:inline}.ddp-act{gap:4px}}
1737
2239
  .ddp-tw .row:first-child{border-top:0}
1738
2240
  .ddp-sys .bd{display:flex;flex-direction:column;gap:8px;padding:2px 10px 10px}
2241
+ .ddp-sys.bare{flex:1;min-height:0;display:flex;flex-direction:column}
2242
+ .ddp-sys>form{display:flex;gap:6px;flex:none;padding:0 10px 10px}
2243
+ /* 外观面板:三页切换;整套里颜色两列、点色块直接改 */
2244
+ .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))}
2245
+ .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}
2246
+ .ddp-lktabs button:hover{color:var(--dsw-alias-label-primary)}
2247
+ .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)}
2248
+ .ddp-look h4{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px;margin-top:12px}
2249
+ .ddp-look .bd>h4:first-child{margin-top:4px}
2250
+ .ddp-look h4 span{font-weight:400}
2251
+ .ddp-lkc{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px}
2252
+ .ddp-lkc .it{display:flex;align-items:center;gap:8px;min-width:0;padding:4px 6px;border-radius:7px;cursor:pointer}
2253
+ .ddp-lkc .it:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
2254
+ .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}
2255
+ .ddp-lkc .sw input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;cursor:pointer}
2256
+ .ddp-lkc .it.on .sw{box-shadow:0 0 0 1.5px ${ACCENT}}
2257
+ .ddp-lkc .tx{flex:1;min-width:0;display:flex;flex-direction:column}
2258
+ .ddp-lkc .nm{font-size:12px;line-height:16px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2259
+ .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}
2260
+ .ddp-lkc .it.on .vl{color:${ACCENT}}
2261
+ .ddp-look .ddp-tok .nm{font:12px/16px inherit}
2262
+ .ddp-lktw{padding:0 6px}
2263
+ .ddp-lktw .row:first-child{border-top:0;padding-top:2px}
2264
+ .ddp-lk-none{margin:0 6px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}
2265
+ .ddp-tokens .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
2266
+ .ddp-tokens .lk:hover{text-decoration:underline;text-underline-offset:2px}
2267
+ .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))}
2268
+ .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}
2269
+ .ddp-lk-more button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
2270
+ .ddp-lk-more svg{width:15px;height:15px;flex:none}
2271
+ .ddp-look footer.row{flex-wrap:wrap;align-items:center}
2272
+ .ddp-look footer.row .cnt{flex-basis:100%;padding:0 2px 2px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2273
+ .ddp-style .who b{margin-right:2px;font-weight:500;color:var(--dsw-alias-label-primary)}
1739
2274
  .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
2275
  .ddp-sysc.def{box-shadow:inset 0 0 0 1.5px ${ACCENT}}
1741
2276
  .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 +2287,42 @@ window.__ModuleLoader__.load({
1752
2287
  .ddp-sys form{display:flex;gap:6px}
1753
2288
  .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
2289
  .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}
2290
+ .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}
2291
+ .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
2292
  .ddp-ho .bd{padding:0 14px 10px}
1758
2293
  .ddp-ho .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
1759
2294
  .ddp-ho .row:first-child{border-top:0}
1760
2295
  .ddp-ho label{display:block;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
1761
2296
  .ddp-ho .pathrow{display:flex;gap:6px;align-items:center}
2297
+ .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}
2298
+ .ddp-ho .pathv:hover{border-color:var(--dsw-alias-label-tertiary)}
2299
+ .ddp-ho .pathv svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-secondary)}
2300
+ .ddp-ho .pathv b{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none;max-width:60%}
2301
+ .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
2302
  .ddp-ho .moved{margin:6px 0 0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
1763
2303
  .ddp-ho .moved.bad{color:var(--dsw-alias-state-error-primary,#d54941)}
1764
2304
  .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
2305
  .ddp-dirbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.5))}
1766
2306
  .ddp-dirbtn:disabled{opacity:.5;cursor:default}
1767
2307
  .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))}
2308
+ .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
2309
  .ddp-ho input[type=text]{width:100%;box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
1771
2310
  .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
2311
  .ddp-ho .opt:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1773
2312
  .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
2313
  .ddp-ho .opt.on{color:var(--dsw-alias-label-primary)}
1775
2314
  .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))}
2315
+ .ddp-ho footer .go{justify-content:center;background:${ACCENT};color:#fff}
2316
+ .ddp-ho footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
2317
+
2318
+ .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
2319
  .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)}
2320
+ .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))}
2321
+ .ddp-lib>header:not(.ddp-homebar) b{font-size:13px;font-weight:600;white-space:nowrap}
2322
+ .ddp-lib .seg,.ddp-homebar .seg{margin-left:auto;display:inline-flex;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
2323
+ .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}
2324
+ .ddp-lib .seg button:hover,.ddp-homebar .seg button:hover{color:var(--dsw-alias-label-primary)}
2325
+ .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
2326
  .ddp-lib>.bd{flex:1;min-height:0;overflow:auto;padding:14px}
1788
2327
  .ddp-pack .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
1789
2328
  .ddp-pack .lk:hover{text-decoration:underline;text-underline-offset:2px}
@@ -1796,11 +2335,11 @@ window.__ModuleLoader__.load({
1796
2335
  .ddp-libnone p{margin:0;max-width:320px}
1797
2336
  .ddp-libfoot{margin:18px 0 0;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary);text-align:center}
1798
2337
  .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}
2338
+ .ddp-work{display:flex;flex-direction:column;gap:4px;min-width:0;container-type:inline-size}
1800
2339
  .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
2340
  .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
2341
  .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}
2342
+ .ddp-work .th img{position:absolute;inset:0;z-index:1;width:100%;height:100%;object-fit:cover;object-position:left top}
1804
2343
  .ddp-work .th .ic{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}
1805
2344
  .ddp-work .th .ic svg{width:26px;height:26px}
1806
2345
  .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 +2350,17 @@ window.__ModuleLoader__.load({
1811
2350
  .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
2351
  .ddp-work .pin{flex:none;color:#f5a524;font-size:11px}
1813
2352
  .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}
2353
+ .ddp-work .acts{display:flex;flex-wrap:wrap;align-items:center;gap:2px;min-width:0;margin-left:-4px}
2354
+ /* 卡片窄(侧栏窄、多列):操作只留图标(悬停有说明),别挤进旁边那件 */
2355
+ @container (max-width:259px){.ddp-work .acts button svg+span{display:none}}
1815
2356
  .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
2357
  .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
2358
  .ddp-work .acts button:disabled{opacity:.45;cursor:default}
1818
2359
  .ddp-work .acts button.on{color:#f5a524}
1819
2360
  .ddp-work .acts svg,.ddp-insp .rm svg,.ddp-market .rm svg{width:14px;height:14px}
1820
2361
  .ddp-work .acts .rm{margin-left:auto}
2362
+ .ddp-work .acts button span{font-size:11px}
2363
+ .ddp-work .acts button svg+span{margin-left:3px}
1821
2364
  .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
2365
  .ddp-insp h4{margin:18px 0 8px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
1823
2366
  .ddp-insp h4:first-child{margin-top:0}
@@ -1835,11 +2378,23 @@ window.__ModuleLoader__.load({
1835
2378
  .ddp-imgs{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px}
1836
2379
  .ddp-imgs:empty{display:none}
1837
2380
  .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}
2381
+ .ddp-market .ddp-mkbar .tabs{display:flex;gap:14px;flex:none}
2382
+ .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}
2383
+ .ddp-market .ddp-mkbar .tabs button em{font-style:normal;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2384
+ .ddp-market .ddp-mkbar .tabs button:hover{color:var(--dsw-alias-label-primary)}
2385
+ .ddp-market .ddp-mkbar .tabs button.on{color:var(--dsw-alias-label-primary);font-weight:500;border-bottom-color:${ACCENT}}
2386
+ .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
2387
  .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}
2388
+ /* 窄了:搜索框缩成一个放大镜,点进去再展开 */
2389
+ @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)}}
2390
+ .ddp-mkkinds{display:flex;align-items:center;gap:6px;margin:0 0 12px;overflow-x:auto;scrollbar-width:none}
2391
+ .ddp-mkkinds::-webkit-scrollbar{display:none}
2392
+ .ddp-mkkinds>button{flex:none}
2393
+ .ddp-mkkinds button:disabled{opacity:.4;cursor:default}
2394
+ .ddp-mkbar .sort{display:flex;gap:2px;flex:none}
2395
+ .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}
2396
+ .ddp-mkbar .sort button:hover{color:var(--dsw-alias-label-primary)}
2397
+ .ddp-mkbar .sort button.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1843
2398
  .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
2399
  .ddp-mkkinds button:hover,.ddp-mkdots button:hover{color:var(--dsw-alias-label-primary)}
1845
2400
  .ddp-mkkinds button.on,.ddp-mkdots button.on{background:${ACCENT};color:#fff}
@@ -1859,25 +2414,14 @@ window.__ModuleLoader__.load({
1859
2414
  .ddp-mkdots{display:flex;flex-wrap:wrap;gap:6px}
1860
2415
  .ddp-mkd .acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
1861
2416
  .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}
2417
+ .ddp-pbtn{background:${ACCENT};color:#fff;font-weight:600}
2418
+ .ddp-pbtn:hover:not(:disabled){filter:brightness(.94)}
1863
2419
  .ddp-sbtn{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1864
2420
  .ddp-pbtn:disabled,.ddp-sbtn:disabled{opacity:.5;cursor:default}
1865
2421
  .ddp-mkd .facts{margin:6px 0 0;padding-left:18px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
1866
2422
  .ddp-mkd .ddp-libfoot a{color:${ACCENT};text-decoration:none}
1867
2423
  .ddp-mkd .ddp-libfoot a:hover{text-decoration:underline}
1868
2424
  .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
2425
  .ddp-img{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
1882
2426
  .ddp-img img{display:block;width:100%;height:100%;object-fit:cover}
1883
2427
  .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 +2440,8 @@ window.__ModuleLoader__.load({
1896
2440
  .ddp-urlform{display:flex;gap:6px}
1897
2441
  .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
2442
  .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}
2443
+ .ddp-urlform button{flex:none;height:28px;padding:0 12px;border:0;border-radius:8px;background:${ACCENT};color:#fff;font:12px inherit;cursor:pointer}
2444
+ .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
2445
  .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
2446
  .ddp-pack .which svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-tertiary)}
1903
2447
  .ddp-pack .which span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1905,6 +2449,8 @@ window.__ModuleLoader__.load({
1905
2449
  .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
2450
  .ddp-pack .lic label.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1907
2451
  .ddp-pack .lic input{flex:none;margin:3px 0 0;accent-color:${ACCENT}}
2452
+ .ddp-share .tip.gh{display:flex;align-items:center;gap:6px;margin-bottom:0;color:var(--dsw-alias-label-primary);font-weight:500}
2453
+ .ddp-share .tip.gh svg{width:14px;height:14px;flex:none;color:${ACCENT}}
1908
2454
  .ddp-share .agree{display:flex;flex-direction:column;gap:2px;margin-top:10px}
1909
2455
  .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
2456
  .ddp-share .agree label:hover{color:var(--dsw-alias-label-primary)}
@@ -1925,14 +2471,13 @@ window.__ModuleLoader__.load({
1925
2471
  .ddp-pack .note ul{margin:2px 0 0;padding-left:16px}
1926
2472
  .ddp-pack footer.row{display:flex;gap:6px}
1927
2473
  .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}
2474
+ .ddp-pack footer .go{background:${ACCENT};color:#fff}
2475
+ .ddp-pack footer .go:hover{background:${ACCENT};color:#fff;filter:brightness(.94)}
2476
+
1931
2477
  .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}}
2478
+ /* 浮块窄了:先截屏名,到 440 再收按钮上的字,400 收工具坞里的演示(演示时横幅上能退出) */
2479
+ @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}}
2480
+ @container (max-width:380px){.ddp-tool{width:32px}.ddp-kind.ddp-where{min-width:64px}}
1936
2481
  /* 标注底栏:右侧栏窄时「这支笔怎么用」折成两行,不被三个按钮挤成省略号 */
1937
2482
  @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
2483
  /* 画标注时预览不动,画布右下角的缩放按钮被标注层盖着也点不到:先收起来,免得和底栏叠在一起 */
@@ -2006,9 +2551,11 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2006
2551
  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
2552
  play: () => svg([h('path', { key: 1, d: 'M8 5.5v13l10.5-6.5z', strokeLinejoin: 'round' })]),
2008
2553
  back: () => svg([h('path', { key: 1, d: 'M14.5 6l-6 6 6 6', strokeWidth: 2.2 })]),
2554
+ 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
2555
  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
2556
  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
2557
  plus: () => svg([h('path', { key: 1, d: 'M12 5v14M5 12h14' })]),
2558
+ send: () => svg([h('path', { key: 1, d: 'M12 19V5.5M6 11.5l6-6 6 6', strokeWidth: 2.2 })]),
2012
2559
  minus: () => svg([h('path', { key: 1, d: 'M5 12h14' })]),
2013
2560
  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
2561
  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 +2577,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2030
2577
  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
2578
  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
2579
  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' })]),
2580
+ 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
2581
  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
2582
  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
2583
  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),
@@ -2371,7 +2919,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2371
2919
  { i: 'style', label: (v, t) => t('chipTwFix'), alts: both('twFixText'), card: (v, t) => [['b', t('chipTwFix')], ['p', v.errs]] },
2372
2920
  { i: 'look', label: (v, t) => t('chipSysEx'), alts: both('sysExtractText'), card: (v, t) => [['b', t('chipSysEx')], ['code', v.src]] },
2373
2921
  { i: 'look', label: (v, t) => t('chipSysAp'), alts: both('sysApplyText'), card: (v, t) => [['b', t('chipSysAp')], ['code', v.name], ['span', v.dir]] },
2374
- { i: 'look', label: (v, t) => t('chipLook', { name: v.name }), alts: L.map((x) => tplSrc(x.styleText, { name: '[^\\n]*?', ask: lit(['styleStart', 'styleRestyle']) })),
2922
+ { i: 'look', label: (v, t) => t('chipLook', { name: v.name }), alts: L.map((x) => tplSrc(x.styleText, { name: '[^\\n]*?', ask: lit(['styleStart', 'styleRestyle', 'styleAuto']) })),
2375
2923
  card: (v, t) => [['b', t('chipLook', { name: v.name })], ['p', v.desc], ['sw', hexes(v.colors)], ['span', v.fonts && v.fonts.split(/[;;]/)[0]]] },
2376
2924
  { i: 'look', label: (v) => v.name, alts: [{ src: `〔设计变量〕([^\\n]*?): ([^\\n]*)((${WHERE_RE}))`, keys: ['name', 'value', 'where'] }],
2377
2925
  card: (v) => [['b', v.name], ['code', v.value], ['sw', hexes(v.value)], ['span', fileOf(v)]] },
@@ -2612,6 +3160,106 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2612
3160
  }
2613
3161
 
2614
3162
  /** 点了一个变量 → 输入框里的一段字 */
3163
+ /** 设计变量的人话名字:--bg → 背景,--fs-micro → 注释,--radius-md → 中圆角;认不出的把名字去掉 -- 和连字符 */
3164
+ function tokenLabel(t, tok) {
3165
+ const raw = String(tok.name).replace(/^--/, '')
3166
+ const words = raw.toLowerCase().split(/[-_]+/).filter(Boolean)
3167
+ const has = (re) => words.some((w) => re.test(w))
3168
+ const kind = tokenKind(tok)
3169
+ 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))
3170
+ const mod = (left) => {
3171
+ const out = []
3172
+ for (const w of left) {
3173
+ if (/^(soft|light|lighter|pale|tint|weak|subtle|faint|bg|50|100|200)$/.test(w)) out.push(t('tkn_light'))
3174
+ else if (/^(dark|darker|deep|strong|bold|700|800|900)$/.test(w)) out.push(t('tkn_dark'))
3175
+ else if (/^(hover|active|pressed|focus)$/.test(w)) out.push(t('tkn_hover'))
3176
+ else out.push(w)
3177
+ }
3178
+ return [...new Set(out)].join(' ')
3179
+ }
3180
+ const name = (key, re) => { const m = mod(rest(re)); return m ? `${t('tkn_' + key)} · ${m}` : t('tkn_' + key) }
3181
+ if (kind === 'color') {
3182
+ if (words.includes('on') && words.length > 1) return t('tkn_onPrimary')
3183
+ const roles = [
3184
+ [/^(muted|subtle|dim|faint|placeholder|secondary-text|text2|hint)$/, 'muted'],
3185
+ [/^(line|border|divider|stroke|outline|hairline|sep|separator|rule)$/, 'line'],
3186
+ [/^(surface|card|panel|elev|elevated|sheet|raised|layer|paper)$/, 'surface'],
3187
+ [/^(text|fg|ink|foreground|body|copy)$/, 'text'],
3188
+ [/^(bg|background|page|canvas|base|backdrop)$/, 'bg'],
3189
+ [/^(primary|brand|main|theme)$/, 'primary'],
3190
+ [/^(accent|highlight|secondary)$/, 'accent'],
3191
+ [/^(link|links)$/, 'link'],
3192
+ [/^(danger|error|destructive|alert)$/, 'danger'],
3193
+ [/^(success|positive|ok)$/, 'success'],
3194
+ [/^(warning|warn|caution)$/, 'warning'],
3195
+ ]
3196
+ for (const [re, key] of roles) if (has(re)) return name(key, re)
3197
+ } else if (kind === 'size') {
3198
+ 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']]
3199
+ for (const [re, key] of sizes) if (has(re)) return name(key, re)
3200
+ } else if (kind === 'radius') {
3201
+ const rs = [[/^(xs|sm|small)$/, 'rSm'], [/^(md|base|medium|default)$/, 'rMd'], [/^(lg|xl|large|2xl)$/, 'rLg'], [/^(full|pill|round|circle|999)$/, 'rFull']]
3202
+ for (const [re, key] of rs) if (has(re)) return name(key, re)
3203
+ const m = mod(rest(/^$/))
3204
+ return m ? `${t('tkn_r')} ${m}` : t('tkn_r')
3205
+ } else if (kind === 'space') {
3206
+ if (has(/^(gutter|page|edge|inset)$/)) return t('tkn_gutter')
3207
+ const m = rest(/^$/).join(' ')
3208
+ return m ? `${t('tkn_space')} ${m}` : t('tkn_space')
3209
+ } else if (kind === 'font') {
3210
+ if (has(/^(head|heading|display|title|serif|brand)$/)) return t('tkn_fHead')
3211
+ if (has(/^(mono|code)$/)) return t('tkn_fMono')
3212
+ if (has(/^(body|text|sans|base|ui)$/)) return t('tkn_fBody')
3213
+ } else if (kind === 'shadow') {
3214
+ const m = rest(/^$/).join(' ')
3215
+ return m ? `${t('tkn_shadow')} ${m}` : t('tkn_shadow')
3216
+ }
3217
+ return raw.replace(/[-_]+/g, ' ')
3218
+ }
3219
+
3220
+ /** 一套变量的人话名字(名字 → 名字):撞名的后面带上原名的尾巴,比如「卡片底(paper)」 */
3221
+ function tokenLabels(t, tokens) {
3222
+ const out = new Map()
3223
+ const seen = new Map()
3224
+ for (const tok of tokens || []) {
3225
+ const l = tokenLabel(t, tok)
3226
+ out.set(tok.name, l)
3227
+ seen.set(l, (seen.get(l) || 0) + 1)
3228
+ }
3229
+ for (const tok of tokens || []) {
3230
+ const l = out.get(tok.name)
3231
+ 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, '')})`)
3232
+ }
3233
+ return out
3234
+ }
3235
+
3236
+ /** 调样式面板头上:这是个什么(按钮「去预约」),不是一串选择器 */
3237
+ function elemLabel(t, name, text) {
3238
+ const n = String(name || '').toLowerCase()
3239
+ const tag = (/^[a-z][a-z0-9]*/.exec(n) || [''])[0]
3240
+ const cls = n.slice(tag.length)
3241
+ let k = 'box'
3242
+ if (/btn|button/.test(cls) || tag === 'button') k = 'button'
3243
+ else if (/card/.test(cls)) k = 'card'
3244
+ else if (/tag|chip|badge|pill/.test(cls)) k = 'tag'
3245
+ else if (/avatar/.test(cls)) k = 'avatar'
3246
+ else if (/icon|ico\b/.test(cls) || tag === 'svg' || tag === 'i') k = 'icon'
3247
+ else if (tag === 'a') k = 'a'
3248
+ else if (tag === 'img' || tag === 'picture' || tag === 'video') k = 'img'
3249
+ else if (/^h[1-6]$/.test(tag)) k = 'h'
3250
+ else if (tag === 'p') k = 'p'
3251
+ else if (tag === 'input' || tag === 'textarea' || tag === 'select') k = 'input'
3252
+ else if (tag === 'li') k = 'li'
3253
+ else if (tag === 'ul' || tag === 'ol') k = 'ul'
3254
+ else if (tag === 'nav' || /nav|tabbar|tab-bar/.test(cls)) k = 'nav'
3255
+ else if (tag === 'header') k = 'header'
3256
+ else if (tag === 'footer') k = 'footer'
3257
+ else if (/^(span|b|strong|em|small|label|time)$/.test(tag)) k = 'text'
3258
+ const s = String(text || '').replace(/\s+/g, ' ').trim()
3259
+ const short = [...s].length > 14 ? [...s].slice(0, 13).join('') + '…' : s
3260
+ return { role: t('el_' + k), text: short }
3261
+ }
3262
+
2615
3263
  function formatToken(tok, st) {
2616
3264
  return `〔设计变量〕${tok.name}: ${String(tok.value).replace(/\s*\n\s*/g, ' ')}(${whereOf(tok.href, st)})\n`
2617
3265
  }
@@ -2783,7 +3431,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2783
3431
  }
2784
3432
 
2785
3433
  /** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
2786
- function formatStyle(t, pre, restyle) {
3434
+ function formatStyle(t, pre, restyle, auto) {
2787
3435
  const en = t('lang') === 'en'
2788
3436
  const c = pre.c
2789
3437
  const fonts = t('styleFonts', { head: `${pre.head} · ${pre.w}`, body: SANS })
@@ -2794,10 +3442,47 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
2794
3442
  fonts,
2795
3443
  shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
2796
3444
  notes: en ? pre.nen : pre.nzh,
2797
- ask: t(restyle ? 'styleRestyle' : 'styleStart'),
3445
+ ask: t(restyle ? 'styleRestyle' : auto ? 'styleAuto' : 'styleStart'),
2798
3446
  })
2799
3447
  }
2800
3448
 
3449
+ /** 首页「风格:自动」时按题材替模型挑一张风格卡(模型自己定时,消费类 App 总落回米色暖调)。
3450
+ * 用户话里已经说了风格 → 不挑(null),照他的话来;没认出题材 → 按类型给一张不是米色的 */
3451
+ const AUTO_STYLE = [
3452
+ ['sport', /跑步|健身|运动|赛事|马拉松|骑行|瑜伽|球馆|\b(runners?|running|fitness|gym|workouts?|sports?|marathons?|cycling|athletes?)\b/i],
3453
+ ['cyber', /电音|夜店|电竞|游戏|音乐节|livehouse|酒吧|\b(game|gaming|esports?|nightclub|club|rave|festival)\b/i],
3454
+ ['sale', /促销|大促|双十一|双11|618|秒杀|优惠|团购|外卖|\b(sale|deals?|coupons?|promo|black friday)\b/i],
3455
+ ['tech', /后台|管理系统|仪表盘|看板|数据|金融|理财|银行|证券|\b(saas|crm|erp|dashboard|admin|analytics|finance|fintech|bank|banking|crypto|b2b|devops|api)\b/i],
3456
+ ['dark', /奢|高端|珠宝|腕表|名表|红酒|威士忌|私人|黑卡|\b(luxury|premium|jewel|jewelry|jewellery|wristwatch(es)?|timepieces?|wine|whisky|concierge)\b/i],
3457
+ ['cute', /儿童|幼儿|宠物|猫|狗|萌|甜品|蛋糕|玩具|\b(kids?|children|pets?|cats?|dogs?|puppy|toys?|dessert|candy)\b/i],
3458
+ ['fresh', /健康|医疗|医院|诊所|户外|露营|母婴|植物|花店|有机|环保|民宿|旅行|旅游|度假|\b(health|medical|clinic|hospital|outdoor|camping|baby|plants?|florist|organic|eco|travel|trip|stays?|vacation|hotel|bnb)\b/i],
3459
+ ['playful', /奶茶|果茶|饮品|饮料|社交|交友|学生|校园|潮牌|\b(bubble tea|boba|drinks?|smoothie|social|dating|students?|campus|streetwear)\b/i],
3460
+ ['guochao', /春节|新年|中秋|端午|国潮|老字号|文创|庙会/],
3461
+ ['retro', /咖啡|黑胶|唱片|市集|古着|理发|面包|\b(vinyl|records?|vintage|barber|bakery|coffee)\b/i],
3462
+ ['glass', /音乐|播放器|天气|智能家居|\b(music|player|weather|smart home|podcast)\b/i],
3463
+ ['editorial', /新闻|杂志|博客|阅读|读书|书店|媒体|播客|\b(news|magazine|blog|reading|books?|media|newsletter)\b/i],
3464
+ ['swiss', /设计工作室|建筑|展览|美术馆|画廊|作品集|\b(agency|studio|architecture|exhibition|gallery|museum|portfolio)\b/i],
3465
+ ]
3466
+ const AUTO_BY_KIND = { slides: 'swiss', web: 'minimal', desktop: 'tech', email: 'minimal', poster: 'playful', tablet: 'minimal' }
3467
+ const AUTO_SPREAD = ['minimal', 'editorial', 'playful', 'fresh', 'tech', 'glass', 'dark', 'sport']
3468
+ // 「北欧风」「清新风:首页…」「中国风的界面」算说了风格;先去掉「通风」「台风」「新风系统」「风控」「风景」这类不是风格的词
3469
+ const NOT_STYLE_FENG = /通风|台风|新风(系统|机|量)|风险|风控|风景|风扇|风味|风暴|风光|风速|风向|风筝|风铃|风车|吹风|防风|挡风|避风|屏风|旋风|狂风|微风|寒风|疾风|顺风|逆风|作风|中风|风湿|风水/g
3470
+ const SAID_STYLE = /风格|配色|色调|主色|色系|[^\s。,,、::;;((]风|\b(style|styled|aesthetic|vibe|palette|colou?r scheme|look and feel)\b/i
3471
+ // 「风格你定」「配色:随意」「any style」:等于没说(「配色定为蓝白」是说了)
3472
+ const STYLE_FREE = /(风格|配色|色调|主色)[::]?\s?(你定|您定|你来定|您来定|你决定|你看着办|随意|随便|不限|都行|无所谓|自由发挥)|\b(any|whatever)\s(style|colou?rs?|palette)\b|\b(style|colou?rs?|palette)\s(is\s)?up\sto\syou\b/gi
3473
+ function autoStyle(text, kind) {
3474
+ const s = String(text || '')
3475
+ if (!s.trim()) return null
3476
+ const said = s.replace(STYLE_FREE, ' ').replace(NOT_STYLE_FENG, ' ')
3477
+ if (SAID_STYLE.test(said) || STYLES.some((x) => said.includes(x.zh) || new RegExp(`\\b${x.en}\\s+(style|look|theme|aesthetic|vibe)\\b`, 'i').test(said))) return null
3478
+ const by = (id) => STYLES.find((x) => x.id === id) || null
3479
+ for (const [id, re] of AUTO_STYLE) if (re.test(s)) return by(id)
3480
+ if (AUTO_BY_KIND[kind]) return by(AUTO_BY_KIND[kind])
3481
+ let n = 0
3482
+ for (const ch of s) n = (n * 31 + ch.codePointAt(0)) >>> 0
3483
+ return by(AUTO_SPREAD[n % AUTO_SPREAD.length])
3484
+ }
3485
+
2801
3486
  // ── 设计库:〔模板〕〔准备分享〕标签、我的风格 ──────────────────────────────
2802
3487
 
2803
3488
  const HEAD_STACK = { SANS, SERIF, ROUND, MONO, GROTESK }
@@ -3569,6 +4254,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
3569
4254
  const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'desktop', 'poster', 'slides', 'email']
3570
4255
  const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', desktop: 'kindDesktop', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
3571
4256
  const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
4257
+ const HOLD_MAX = 15000 // 草稿放到一半真文件就落地了:最多再放这么久(一般几秒放完自己说一声)
3572
4258
  const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
3573
4259
  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
4260
  // 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule 小程序右上角的胶囊、win 桌面软件的窗口按钮
@@ -3662,7 +4348,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
3662
4348
 
3663
4349
  /** 整块居中放进舞台(最大 100%) */
3664
4350
  function fitCam(L, sw, sh) {
3665
- const z = clampZ(Math.min(1, L.z))
4351
+ // 排的时候按整块舞台算的缩放;右边被面板挡着时 sw 变窄,再缩到放得下
4352
+ 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
4353
  return { z, x: (sw - L.bw * z) / 2, y: PAD_TOP + Math.max(0, (sh - PAD_TOP - PAD - L.bh * z) / 2) }
3667
4354
  }
3668
4355
 
@@ -4003,6 +4690,226 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4003
4690
 
4004
4691
  // ── 小部件 ───────────────────────────────────────────────────────────────
4005
4692
 
4693
+ // 模型在做什么:按它调的工具说人话(写文件另算,见 busyPhase)
4694
+ const TOOL_PHASE = [
4695
+ [/^check_design$/, 'stCheck'], [/^look_design$/, 'stLook'], [/^preview_page$/, 'stPreview'], [/^extract_design_system$/, 'stBrand'],
4696
+ [/^handoff_design$/, 'stCode'], [/^compare_design$/, 'stCompare'], [/^ask_user_question$/, 'asking'],
4697
+ [/^(bash|shell|sh|zsh|powershell|pwsh|exec|run_command|terminal)$/i, 'stShell'],
4698
+ [/^skill$/i, 'stSkill'],
4699
+ [/web|fetch|browse|http/i, 'stWeb'],
4700
+ [/^(write|edit|multi_?edit|create_file|str_replace\w*|apply_patch|patch)$/i, 'stWriteAny'],
4701
+ [/read|grep|glob|list|^ls$|find|search|view/i, 'stRead'],
4702
+ ]
4703
+ const clock = (ms) => {
4704
+ const s = Math.max(0, Math.floor(ms / 1000))
4705
+ const p2 = (n) => String(n).padStart(2, '0')
4706
+ return s >= 3600 ? `${Math.floor(s / 3600)}:${p2(Math.floor(s / 60) % 60)}:${p2(s % 60)}` : `${Math.floor(s / 60)}:${p2(s % 60)}`
4707
+ }
4708
+
4709
+ /** 顶上的状态条:在做什么 · 第几屏 · 用了多久(自己每秒走一下,不带着整个右侧栏重画) */
4710
+ function BusyBar({ t, phase, since, side }) {
4711
+ const [, tick] = useState(0)
4712
+ useEffect(() => {
4713
+ const id = setInterval(() => tick((n) => n + 1), 1000)
4714
+ return () => clearInterval(id)
4715
+ }, [])
4716
+ const tm = clock(Date.now() - since)
4717
+ return h('div', { className: 'ddp-busy' + (phase.asking ? ' ask' : '') + (side ? ' side' : '') },
4718
+ phase.asking ? h('i', { className: 'q', 'aria-hidden': true }) : h('span', { className: 'dots', 'aria-hidden': true }, h('i'), h('i'), h('i')),
4719
+ h('span', { className: 'tx', role: 'status' }, phase.text),
4720
+ phase.sub ? h('span', { className: 'sub' }, phase.sub) : null,
4721
+ h('span', { className: 'tm', title: t('stTime', { time: tm }) }, tm),
4722
+ phase.asking ? null : h('i', { className: 'bar', 'aria-hidden': true }))
4723
+ }
4724
+
4725
+ // 生成中的占位:三只带名字的鼠标轮流框出版面(按类型给个大概的样子),框里闪骨架,一遍遍画。位置都是百分比,放哪都行
4726
+ const GD_PLAN = {
4727
+ 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]],
4728
+ 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]],
4729
+ poster: [[8, 6, 84, 50], [8, 61, 70, 8], [8, 72, 50, 4], [8, 84, 36, 7]],
4730
+ slides: [[6, 10, 60, 13], [6, 32, 40, 52], [52, 32, 42, 52]],
4731
+ email: [[10, 4, 80, 7], [10, 14, 80, 30], [10, 48, 60, 5], [10, 56, 80, 14], [10, 75, 30, 6]],
4732
+ }
4733
+ const GD_C = ['#4d6bfe', '#e8590c', '#0ca678']
4734
+ /** 占位里的鼠标什么时候动:换了在做的事、或者又写了 15 行就加一;不在生成就不动 */
4735
+ function pulseStep(pr, busy, key, ln) {
4736
+ if (!busy) {
4737
+ pr.key = ''
4738
+ pr.lines = 0
4739
+ return pr.n
4740
+ }
4741
+ if (ln < pr.lines) pr.lines = ln
4742
+ if (key !== pr.key) {
4743
+ pr.key = key
4744
+ pr.lines = ln
4745
+ pr.n++
4746
+ } else if (ln - pr.lines >= 15) {
4747
+ pr.lines = ln
4748
+ pr.n++
4749
+ }
4750
+ return pr.n
4751
+ }
4752
+ const planOf = (kind) => GD_PLAN[kind === 'miniprogram' ? 'app' : kind === 'tablet' || kind === 'desktop' ? 'web' : kind] || GD_PLAN.web
4753
+ const reduced = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches } catch { return false } }
4754
+
4755
+ /**
4756
+ * 生成中的占位。三只带名字的鼠标先把版面框一遍,框完就停住——不循环;之后模型每真有一点动静
4757
+ * (换了在做的事、又写了一截),才有一只去重新框一块。模型停着,它们也停着。
4758
+ */
4759
+ function GhostDraw({ t, kind, pulse = 0 }) {
4760
+ const ref = useRef(null)
4761
+ const plan = planOf(kind)
4762
+ const S = useRef(null)
4763
+ const EASE_K = 'cubic-bezier(.3,.7,.2,1)'
4764
+ useEffect(() => {
4765
+ const el = ref.current
4766
+ if (!el) return
4767
+ const boxes = [...el.querySelectorAll('.b')]
4768
+ const curs = [...el.querySelectorAll('.c')]
4769
+ 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() }
4770
+ S.current = s
4771
+ if (s.still) return
4772
+ const STEP = 520
4773
+ const MOVE = 320
4774
+ const DRAG = 420
4775
+ const paths = curs.map((_, i) => [{ ms: 0, x: s.pos[i].x, y: s.pos[i].y, o: 0 }])
4776
+ plan.forEach(([x, y, w, hh], i) => {
4777
+ const s0 = 250 + i * STEP
4778
+ const path = paths[i % curs.length]
4779
+ const last = path[path.length - 1]
4780
+ 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 })
4781
+ const box = boxes[i]
4782
+ if (box) {
4783
+ s.box[i] = box.animate([
4784
+ { opacity: 0, clipPath: 'inset(0 100% 100% 0)' },
4785
+ { offset: 0.001, opacity: 1, clipPath: 'inset(0 100% 100% 0)', easing: EASE_K },
4786
+ { opacity: 1, clipPath: 'inset(0 0 0 0)' },
4787
+ ], { duration: DRAG, delay: s0 + MOVE, fill: 'both' })
4788
+ }
4789
+ })
4790
+ let end = 0
4791
+ curs.forEach((c, i) => {
4792
+ const path = paths[i]
4793
+ const D = path[path.length - 1].ms || 1
4794
+ end = Math.max(end, D)
4795
+ const kf = []
4796
+ for (const p of path) {
4797
+ const o = p.ms / D
4798
+ if (kf.length && o <= kf[kf.length - 1].offset) continue
4799
+ kf.push({ offset: o, left: p.x + '%', top: p.y + '%', opacity: p.o, easing: EASE_K })
4800
+ }
4801
+ if (kf[kf.length - 1].offset < 1) kf.push({ ...kf[kf.length - 1], offset: 1 })
4802
+ const last = path[path.length - 1]
4803
+ s.pos[i] = { x: last.x, y: last.y }
4804
+ c.classList.toggle('flip', last.x > 78)
4805
+ s.cur[i] = c.animate(kf, { duration: D, fill: 'forwards' })
4806
+ })
4807
+ s.till = performance.now() + end
4808
+ return () => { s.cur.forEach((a) => a && a.cancel()); s.box.forEach((a) => a && a.cancel()) }
4809
+ }, [plan])
4810
+ // 模型真有动静:轮到的那只去重新框一块(框过的那块先收起再跟着鼠标拉开)
4811
+ useEffect(() => {
4812
+ const s = S.current
4813
+ if (!s || s.still || pulse === s.seen) return
4814
+ s.seen = pulse
4815
+ const now = performance.now()
4816
+ if (now < s.till) return // 头一遍还没框完 / 上一下还没做完:这一下不插队
4817
+ const k = pulse % s.curs.length
4818
+ let j = Math.floor(Math.random() * plan.length)
4819
+ if (j === s.last) j = (j + 1) % plan.length
4820
+ s.last = j
4821
+ const [x, y, w, hh] = plan[j]
4822
+ const MOVE = 420
4823
+ const DRAG = 480
4824
+ const from = s.pos[k]
4825
+ const to = { x: Math.min(97, x + w), y: Math.min(97, y + hh) }
4826
+ const c = s.curs[k]
4827
+ const prev = s.cur[k]
4828
+ s.cur[k] = c.animate([
4829
+ { left: from.x + '%', top: from.y + '%', opacity: 1, easing: EASE_K },
4830
+ { offset: MOVE / (MOVE + DRAG), left: x + '%', top: y + '%', opacity: 1, easing: EASE_K },
4831
+ { left: to.x + '%', top: to.y + '%', opacity: 1 },
4832
+ ], { duration: MOVE + DRAG, fill: 'forwards' })
4833
+ if (prev) prev.cancel()
4834
+ s.pos[k] = to
4835
+ c.classList.toggle('flip', to.x > 78)
4836
+ const box = s.boxes[j]
4837
+ if (box) {
4838
+ const pb = s.box[j]
4839
+ s.box[j] = box.animate([
4840
+ { opacity: 1, clipPath: 'inset(0 100% 100% 0)', easing: EASE_K },
4841
+ { opacity: 1, clipPath: 'inset(0 0 0 0)' },
4842
+ ], { duration: DRAG, delay: MOVE, fill: 'both' })
4843
+ if (pb) pb.cancel()
4844
+ }
4845
+ s.till = now + MOVE + DRAG
4846
+ }, [pulse])
4847
+ const names = [t('curLayout'), t('curColor'), t('curCopy')]
4848
+ return h('div', { className: 'ddp-gd', ref, 'aria-hidden': true },
4849
+ ...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] } })),
4850
+ ...GD_C.map((c, i) => h('span', { key: 'c' + i, className: 'c', style: { '--c': c, left: (30 + i * 20) + '%', top: '112%' } },
4851
+ 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' })),
4852
+ h('em', null, names[i]))))
4853
+ }
4854
+
4855
+ /**
4856
+ * 边写边看:预览服务器上正在写的那一页(/某屏.html?__ddp_draft=…),浏览器边收边画。
4857
+ * 页面里的小脚本报到后才显出来(之前看到的是底下的占位),画布缩放告诉它,鼠标和字看起来一样大。
4858
+ */
4859
+ function DraftFrame({ url, z = 1, lang, onEnd }) {
4860
+ const ref = useRef(null)
4861
+ const [live, setLive] = useState(false)
4862
+ const zr = useRef(z)
4863
+ zr.current = z
4864
+ const endR = useRef(onEnd)
4865
+ endR.current = onEnd
4866
+ const src = url + (url.includes('?') ? '&' : '?') + 'lang=' + (lang === 'en' ? 'en' : 'zh')
4867
+ let origin = '*'
4868
+ try { origin = new URL(url).origin } catch { /* 地址不对就不发 */ }
4869
+ const tell = (zz) => {
4870
+ const f = ref.current
4871
+ try { if (f && f.contentWindow) f.contentWindow.postMessage({ dshDraft: 'zoom', z: zz }, origin) } catch { /* 页面已经换了 */ }
4872
+ }
4873
+ useEffect(() => {
4874
+ setLive(false)
4875
+ const end = () => { if (endR.current) endR.current(url) }
4876
+ // 一直没报到(草稿已经收走、给的是真文件):别干等,算放完了
4877
+ let quiet = setTimeout(end, 6000)
4878
+ const on = (e) => {
4879
+ const f = ref.current
4880
+ if (!f || e.source !== f.contentWindow || !e.data) return
4881
+ if (e.data.dshDraft === 'end') return end() // 写完、鼠标也退场了:可以换真文件了
4882
+ if (e.data.dshDraft !== 'hello') return
4883
+ clearTimeout(quiet)
4884
+ quiet = null
4885
+ setLive(true)
4886
+ tell(zr.current)
4887
+ }
4888
+ addEventListener('message', on)
4889
+ return () => { clearTimeout(quiet); removeEventListener('message', on) }
4890
+ }, [src])
4891
+ useEffect(() => { if (live) tell(z) }, [z, live])
4892
+ 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 })
4893
+ }
4894
+
4895
+ /** 没有外框的地方(第一屏的等待卡片):按设备大小排好,再整体缩进容器里 */
4896
+ function FitDraft({ url, kind, lang, onEnd }) {
4897
+ const ref = useRef(null)
4898
+ const [sc, setSc] = useState(0)
4899
+ const [w, hh] = sizeOf('', kind)
4900
+ useEffect(() => {
4901
+ const el = ref.current
4902
+ if (!el) return
4903
+ const m = () => setSc(Math.min(el.clientWidth / w, el.clientHeight / hh) || 0)
4904
+ m()
4905
+ const ro = new ResizeObserver(m)
4906
+ ro.observe(el)
4907
+ return () => ro.disconnect()
4908
+ }, [w, hh])
4909
+ return h('div', { ref, className: 'ddp-fitd' },
4910
+ sc ? h('div', { style: { width: w, height: hh, transform: `translate(-50%, -50%) scale(${sc})` } }, h(DraftFrame, { url, z: sc, lang, onEnd })) : null)
4911
+ }
4912
+
4006
4913
  function Skeleton({ gone }) {
4007
4914
  return h('div', { className: 'ddp-skel' + (gone ? ' gone' : '') },
4008
4915
  h('b', { style: { height: 22, width: '38%' } }),
@@ -4057,23 +4964,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4057
4964
  err ? h('div', { className: 'ddp-err', style: className ? { width: '100%' } : { position: 'absolute', top: 40, left: 12 } }, err) : null)
4058
4965
  }
4059
4966
 
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
4967
  /**
4078
4968
  * 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
4079
4969
  * fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
@@ -4252,9 +5142,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4252
5142
  setCur(id)
4253
5143
  const p = P.current
4254
5144
  const build = first ? !!p.born : !!(changes && changes.whole && p.busy)
4255
- postTo(id, { type: 'reveal', build })
5145
+ postTo(id, { type: 'zoom', z: p.zoom || 1 }) // 先定缩放:生成动画里的鼠标、名字按它定大小
5146
+ postTo(id, { type: 'reveal', build, lang: p.t && p.t('lang') === 'en' ? 'en' : 'zh' })
4256
5147
  postTo(id, { type: 'busy', on: !!p.busy })
4257
- postTo(id, { type: 'zoom', z: p.zoom || 1 })
4258
5148
  if (p.picking) postTo(id, pickMsg())
4259
5149
  if (p.demo) postTo(id, { type: 'demo', on: true })
4260
5150
  if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
@@ -4506,27 +5396,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4506
5396
  }
4507
5397
 
4508
5398
  /** 类型菜单:网页 / 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
5399
 
4531
5400
  // ── 画笔标注:一层透明画布盖在预览上,画的东西按台面上的坐标(CSS 像素)一笔一笔记着,能撤销、能擦 ──
4532
5401
  // 一笔:pen / hl { p: [[x, y]…] } · arrow / rect / oval { a: [x, y], b: [x, y] } · text { x, y, f, v };都带颜色 c,线宽 w
@@ -4793,9 +5662,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
4793
5662
  const [pal, setPal] = useState(false) // 颜色板开着
4794
5663
  const [hist, setHist] = useState({ list: [[]], i: 0 }) // 每画一笔存一份整张的,撤销 / 重做就是前后挪
4795
5664
  const [txt, setTxt] = useState(null) // 正在写的字:{ x, y, v }
4796
- // 头几次打开时页面中间教一下怎么用(画了第一笔就收起)
5665
+ // 第一次打开时在底栏上面教一下怎么用(画了第一笔就收起;以后底栏那句够用了)
4797
5666
  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 { /* 存不了就每次都教 */ }
5667
+ 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
5668
  return true
4800
5669
  })
4801
5670
  const shapes = hist.list[hist.i]
@@ -5062,39 +5931,76 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5062
5931
  h('button', { type: 'button', className: 'go' + (shapes.length || (txt && txt.v.trim()) ? '' : ' idle'), disabled: sending, onClick: send }, t('mkSend')))))
5063
5932
  }
5064
5933
 
5065
- /** 设计规范面板:按组列出变量,点一个放进输入框 */
5066
- function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onSystems, onSaveStyle, onLibrary, onClose }) {
5934
+ /** 设计变量的小预览(字号、圆角、间距、阴影、字体) */
5935
+ function tokenPreview(kind, v) {
5936
+ const px = Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
5937
+ switch (kind) {
5938
+ case 'font': return h('span', { style: { fontFamily: v, fontSize: 15, fontWeight: 600 } }, 'Aa')
5939
+ case 'size': return h('span', { style: { fontSize: Math.min(22, px), fontWeight: 600, lineHeight: 1 } }, '字')
5940
+ case 'space': return h('i', { className: 'bar', style: { width: Math.max(2, Math.min(24, px / 2)) } })
5941
+ case 'radius': return h('i', { className: 'rd', style: { borderTopLeftRadius: Math.min(14, px) } })
5942
+ case 'shadow': return h('i', { className: 'sh', style: { boxShadow: v } })
5943
+ default: return null
5944
+ }
5945
+ }
5946
+
5947
+ /**
5948
+ * 外观面板:整套(配色直接改、别的变量交给模型、调节滑块)|换风格(风格卡、参考图、我的风格)|品牌(设计系统)。
5949
+ * 整套里改的颜色和拖的滑块先在页面上看效果,底下「直接保存」写进文件,或「交给模型」。
5950
+ */
5951
+ 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
5952
  const groups = {}
5068
5953
  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 },
5954
+ const names = tokenLabels(t, tokens)
5955
+ const list = (tw && tw.list) || []
5956
+ const errs = (tw && tw.errors) || []
5957
+ const n = Object.keys(tv).length + list.filter((k) => hasOwn(live, k.id)).length
5958
+ const tabs = [['set', t('lookSet')], ['style', t('lookStyle')], ['brand', t('lookBrand')]]
5959
+ let body
5960
+ if (tab === 'style') {
5961
+ body = [
5962
+ h('p', { className: 'tip', key: 'tip' }, t('stylesSwap')),
5963
+ h('div', { className: 'bd', key: 'bd' }, h(StyleCards, { t, onPick: onPickStyle }),
5964
+ h('div', { className: 'ddp-lk-more' },
5965
+ onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('lookRef'))) : null,
5966
+ onSaveStyle && tokens.length ? h('button', { title: t('inSaveStyleTip'), onClick: onSaveStyle }, h(I.plus), h('span', null, t('tkSaveStyle'))) : null,
5967
+ onInspire ? h('button', { onClick: onInspire }, h(I.box), h('span', null, t('lookInspire'))) : null)),
5968
+ ]
5969
+ } else if (tab === 'brand') {
5970
+ body = [brand]
5971
+ } else {
5972
+ const colors = groups.color || []
5973
+ body = [
5974
+ h('div', { className: 'bd', key: 'bd' },
5975
+ tokens.length ? null : h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onTokMake }, t('tokensAsk')))),
5976
+ colors.length ? h('h4', null, t('gColor'), h('span', null, t('lookColorTip'))) : null,
5977
+ colors.length ? h('div', { className: 'ddp-lkc' }, ...colors.map((tok) => {
5978
+ const v = hasOwn(tv, tok.name) ? tv[tok.name] : tok.value
5979
+ return h('label', { key: tok.name, className: 'it' + (hasOwn(tv, tok.name) ? ' on' : ''), title: `${tok.name}: ${v}` },
5980
+ h('span', { className: 'sw', style: { background: v } },
5981
+ h('input', { type: 'color', value: colorHex(v, tokens), 'aria-label': names.get(tok.name), onChange: (e) => onTv(tok.name, e.target.value) })),
5982
+ h('span', { className: 'tx' }, h('span', { className: 'nm' }, names.get(tok.name)), h('span', { className: 'vl' }, v)))
5983
+ })) : null,
5984
+ ...TOKEN_GROUPS.filter((g) => g !== 'color' && groups[g]).map((g) => h('div', { key: g },
5087
5985
  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)
5986
+ ...groups[g].map((tok) => h('button', { key: tok.name, className: 'ddp-tok', title: `${tok.name}: ${tok.value}\n${t('lookTokTip')}`, onClick: () => onTokAsk(tok) },
5987
+ h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, tokenPreview(g, tok.value)),
5988
+ h('span', { className: 'tx' }, h('span', { className: 'nm' }, names.get(tok.name)), h('span', { className: 'vl' }, tok.value)))))),
5989
+ h('h4', null, t('lookTw')),
5990
+ list.length
5991
+ ? 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)
5992
+ : h('p', { className: 'ddp-lk-none' }, errs.length ? errs.join(';') : t('lookTwNone'), ' ', h('button', { className: 'lk', onClick: () => onTwMake(errs) }, t(errs.length ? 'twFix' : 'twMake')))),
5993
+ n ? h('footer', { className: 'row', key: 'ft' },
5994
+ h('span', { className: 'cnt' }, t('lookN', { n })),
5995
+ h('button', { onClick: onReset }, h('span', null, t('styleReset'))),
5996
+ h('button', { onClick: onAsk, title: t('twAskTip') }, h('span', null, t('styleAsk'))),
5997
+ h('button', { className: 'go', onClick: onSave, disabled: !write.ok, title: write.ok ? t('lookSaveTip') : write.tip }, h('span', null, t('lookSave')))) : null,
5998
+ ]
5999
+ }
6000
+ return h('div', { className: 'ddp-tokens ddp-look', onPointerDown: (e) => e.stopPropagation() },
6001
+ h('header', null, h(I.palette), h('b', null, t('look')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
6002
+ 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))),
6003
+ ...body)
5098
6004
  }
5099
6005
 
5100
6006
  /** 设计体检的结果:按屏列出问题(和页面上的编号一样),点一条在页面上滚过去、那个框闪一下 */
@@ -5118,7 +6024,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5118
6024
  n ? h('p', { className: 'tip' }, t('checkNote')) : null,
5119
6025
  h('footer', { className: 'row' },
5120
6026
  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')))))
6027
+ n ? h('button', { className: 'go', onClick: onFix, disabled: ck.running, title: t('checkFixTip') }, h(I.sparkle), h('span', null, t('checkFix'))) : null))
5122
6028
  }
5123
6029
 
5124
6030
  /**
@@ -5136,13 +6042,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5136
6042
  // 现在用的是哪个变量:改过的看改成了什么,没改的看样式表里怎么写的
5137
6043
  const varOf = (k) => { const m = VAR_ONLY.exec(String(cur(k) || '').trim()); return m ? m[1] : k in sty.live ? '' : (sty.vars || {})[k] || '' }
5138
6044
  const set = (props, v) => onSet(Object.fromEntries(props.map((p) => [p, v])))
6045
+ const names = tokenLabels(t, tokens)
5139
6046
  const chips = (kind, props) => (groups[kind] || []).slice(0, 10).map((tok) => h('button', {
5140
6047
  key: tok.name,
5141
6048
  className: 'chip' + (kind === 'color' ? ' c' : '') + (varOf(props[0]) === tok.name ? ' on' : ''),
5142
6049
  title: `${tok.name}: ${tok.value}`,
5143
6050
  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))
6051
+ }, kind === 'color' ? h('i', { style: { background: tok.value } }) : names.get(tok.name)))
6052
+ const nice = (v) => names.get(v) || v.replace(/^--/, '')
6053
+ const shown = (k, text) => (varOf(k) ? h('em', { className: 'v', title: t('styleVar', { name: varOf(k) }) }, nice(varOf(k))) : h('em', null, text))
5146
6054
  const slider = (key, props, max, kind) => {
5147
6055
  const v = Math.round(pxOf(cur(props[0]), tokens) ?? 0)
5148
6056
  const c = kind ? chips(kind, props) : []
@@ -5160,7 +6068,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5160
6068
  const w = Math.min(700, Math.max(400, Math.round(parseInt(cur('font-weight'), 10) / 100) * 100 || 400))
5161
6069
  const n = Object.keys(sty.live).length
5162
6070
  return box(
5163
- h('p', { className: 'who', title: sty.selector }, h('code', null, sty.name), sty.text ? ` · ${sty.text}` : ''),
6071
+ 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
6072
  h('div', { className: 'bd' },
5165
6073
  slider('styleFont', ['font-size'], 72, 'size'),
5166
6074
  h('div', { className: 'row', key: 'w' },
@@ -5173,10 +6081,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5173
6081
  slider('stylePadX', ['padding-left', 'padding-right'], 48, 'space'),
5174
6082
  sty.layout ? slider('styleGap', ['gap'], 48, 'space') : null),
5175
6083
  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')))))
6084
+ n ? h('footer', { className: 'row' },
6085
+ h('button', { onClick: onReset }, h('span', null, t('styleReset'))),
6086
+ h('button', { onClick: onAsk, title: t('styleAskTip') }, h('span', null, t('styleAsk'))),
6087
+ h('button', { className: 'go', onClick: onSave, disabled: !write.ok, title: write.ok ? t('lookSaveTip') : write.tip }, h('span', null, t('styleSave')))) : null)
5180
6088
  }
5181
6089
 
5182
6090
  /** 风格样板:用这个风格画的一小屏(顶栏、标题、按钮、商品卡、数据卡);卡片上缩小着放,点开是原大 */
@@ -5207,12 +6115,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5207
6115
  }
5208
6116
 
5209
6117
  /** 一排风格卡:每张是缩小的样板;点一张看原大样板(左右翻),「用这个风格」才交给 onPick */
5210
- function StyleCards({ t, onPick }) {
6118
+ function StyleCards({ t, onPick, limit }) {
5211
6119
  const en = t('lang') === 'en'
5212
6120
  // 内置的 20 张 + 设计库 › 灵感里存的「我的风格」
5213
6121
  const mine = useInspire().filter((x) => x.type === 'style' && x.style && x.style.c).map(myStyleOf)
5214
6122
  const LIST = mine.length ? [...STYLES, ...mine] : STYLES
5215
6123
  const [on, setOn] = useState(-1) // 正在看样板的那个;-1 是看全部
6124
+ const [all, setAll] = useState(false) // 空白页只放前 limit 张,点「更多风格」全展开
5216
6125
  const was = useRef(-1)
5217
6126
  const box = useRef(null)
5218
6127
  useEffect(() => {
@@ -5224,6 +6133,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5224
6133
  // 列表和样板高矮不一样,外面滚动的位置会变:把样板 / 刚才那张卡滚进来
5225
6134
  if (was.current >= 0 || on >= 0) (on >= 0 ? el : f || el).scrollIntoView({ block: 'nearest' })
5226
6135
  if (on >= 0) was.current = on
6136
+ if (on >= 0 && limit && on >= limit) setAll(true) // 样板里翻到了后面的卡:回全部时要看得到它
5227
6137
  }, [on])
5228
6138
  const n = LIST.length
5229
6139
  if (on >= 0) {
@@ -5245,17 +6155,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5245
6155
  h('button', { className: 'ddp-btn nx', title: t('styleNext'), onClick: () => go(1) }, h(I.back)),
5246
6156
  h('button', { className: 'ddp-go', onClick: () => onPick(x) }, t('styleUse'))))
5247
6157
  }
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) },
6158
+ const cut = limit && !all && n > limit
6159
+ 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
6160
  h('span', { className: 'pv', 'aria-hidden': 'true' }, h(StyleSample, { t, x })),
5250
6161
  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 })))
6162
+ if (!limit || n <= limit) return grid
6163
+ 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
6164
  }
5260
6165
 
5261
6166
  // ── 调节滑块、品牌设计系统、转成代码 ─────────────────────────────────────
@@ -5283,41 +6188,32 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5283
6188
  return t('twAskHead') + '\n' + rows.join('\n') + '\n' + t('twAskTail') + '\n'
5284
6189
  }
5285
6190
 
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
- }
6191
+ /** 调节滑块的一行:模型给这套设计做的维度(tweaks.json),拖的时候所有屏一起变 */
6192
+ function tweakRow(t, k, live, onSet) {
6193
+ const v = hasOwn(live, k.id) ? live[k.id] : k.value
6194
+ 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)))
6195
+ const note = k.note ? h('div', { className: 'note' }, k.note) : null
6196
+ if (k.kind === 'range') {
5310
6197
  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)
6198
+ 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
6199
  }
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')))))
6200
+ if (k.kind === 'color') {
6201
+ return h('div', { className: 'row', key: k.id }, lab,
6202
+ 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)
6203
+ }
6204
+ return h('div', { className: 'row', key: k.id }, lab,
6205
+ 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)
6206
+ }
6207
+
6208
+ /** 外观面板里改的:滑块 + 直接改的设计变量 → 交给模型的一段字 */
6209
+ function formatLook(t, list, live, tokens, tv) {
6210
+ let out = Object.keys(live).length ? formatTweaks(t, list, live) : ''
6211
+ const rows = Object.entries(tv).map(([name, v]) => {
6212
+ const tok = tokens.find((x) => x.name === name)
6213
+ return `- ${name}(${tok ? tokenLabel(t, tok) : name}):${tok ? tok.value : '?'} → ${v}`
6214
+ })
6215
+ if (rows.length) out += t('lookAskHead') + '\n' + rows.join('\n') + '\n' + t('lookAskTail') + '\n'
6216
+ return out
5321
6217
  }
5322
6218
 
5323
6219
  async function getJson(path) {
@@ -5348,7 +6244,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5348
6244
  return h('button', { type: 'button', className: 'ddp-dirbtn', title: t('pickDir'), 'aria-label': t('pickDir'), disabled: busy, onClick: pick }, h(I.folder))
5349
6245
  }
5350
6246
 
5351
- function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose }) {
6247
+ function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose, bare }) {
5352
6248
  const [data, setData] = useState(null)
5353
6249
  const [err, setErr] = useState('')
5354
6250
  const [src, setSrc] = useState('')
@@ -5366,9 +6262,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5366
6262
  onExtract(v)
5367
6263
  setSrc('')
5368
6264
  }
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))),
6265
+ return h('div', { className: bare ? 'ddp-sys bare' : 'ddp-tokens ddp-sys', onPointerDown: (e) => e.stopPropagation() },
6266
+ 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
6267
  h('p', { className: 'tip' }, t('sysTip')),
6268
+ h('form', { onSubmit: go },
6269
+ h('input', { value: src, placeholder: t('sysPh'), 'aria-label': t('sysPh'), onChange: (e) => setSrc(e.target.value) }),
6270
+ h(DirButton, { t, onPicked: setSrc, onSay }),
6271
+ h('button', { type: 'submit', disabled: !src.trim() }, t('sysExtract'))),
5372
6272
  h('div', { className: 'bd' },
5373
6273
  err ? h('div', { className: 'none' }, err) : null,
5374
6274
  data && !list.length ? h('div', { className: 'none' }, t('sysEmpty')) : null,
@@ -5379,12 +6279,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5379
6279
  x.sources && x.sources.length ? h('small', { title: x.sources.join('\n') }, x.sources[0]) : null,
5380
6280
  h('div', { className: 'acts' },
5381
6281
  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')))))
6282
+ hasDesign ? h('button', { onClick: () => onApply(x) }, t('sysApply')) : null)))))
5388
6283
  }
5389
6284
 
5390
6285
  const FW_LIST = ['react', 'vue', 'next', 'html']
@@ -5392,6 +6287,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5392
6287
  // 小程序的设计:先列 uni-app、原生小程序
5393
6288
  const FW_MINI = ['uniapp', 'weapp']
5394
6289
 
6290
+ /** 「uni-app(HBuilderX,Vue 3)」→「uni-app · HBuilderX · Vue 3」:放进「跟现有项目一样:…」里别括号套括号 */
6291
+ const dirParts = (x) => String(x || '').split(/[\\/]+/).filter(Boolean)
6292
+ const dirName = (x) => dirParts(x).pop() || String(x || '')
6293
+ const dirParent = (x) => { const q = dirParts(x); return q.length > 1 ? q[q.length - 2] : '' }
6294
+ const flatLabel = (x) => String(x || '').replace(/\s*[((]\s*/g, ' · ').replace(/\s*[,,、]\s*/g, ' · ').replace(/\s*[))]\s*/g, ' ').replace(/\s+/g, ' ').trim()
6295
+
5395
6296
  /** 转成代码:用什么做、放哪、转几屏;点了把一段话放进输入框,模型调 handoff_design 照交接包做,每屏做完 compare_design */
5396
6297
  function HandoffPanel({ t, sid, root, fileTarget, count, cur, onGo, onSay, onClose }) {
5397
6298
  const [info, setInfo] = useState(null)
@@ -5400,6 +6301,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5400
6301
  const [one, setOne] = useState(false)
5401
6302
  const [moved, setMoved] = useState(null) // 选的文件夹不能直接放:{ dest, why }('nested' 在里面新建、'beside' 放设计目录旁边)
5402
6303
  const [checking, setChecking] = useState('') // 'busy' 正在问服务器这个文件夹;别的字是没问成的原因
6304
+ const [typing, setTyping] = useState(false) // 路径平时只显示文件夹名,点了才变输入框
5403
6305
  // 现在是哪套设计、第几次选:选文件夹的窗口开着时换了设计、或者又选了一次 / 手改了路径,旧的结果作废
5404
6306
  // hand:用户自己点过框架(换设计前一直听用户的,不再按选的文件夹自动换)
5405
6307
  const live = useRef({ root, seq: 0, hand: false })
@@ -5450,7 +6352,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5450
6352
  const p = info.project || {}
5451
6353
  const same = !!p.framework
5452
6354
  // 「跟现有项目一样」认得出 HBuilderX 的 uni-app、微信原生小程序(没有 package.json 也行)
5453
- const sameLabel = p.framework === 'weapp' ? t('hoWeapp') : p.label
6355
+ const sameLabel = flatLabel(p.framework === 'weapp' ? t('hoWeapp') : p.label)
5454
6356
  const list = info.kind === 'miniprogram' ? [...FW_MINI, ...FW_LIST] : FW_LIST
5455
6357
  const name = (k) => (k === 'html' ? t('hoHtml') : k === 'uniapp' ? t('hoUni') : k === 'weapp' ? t('hoWeapp') : FW_LABEL[k])
5456
6358
  const opts = [...(same ? [['same', t('hoSame', { label: sameLabel })]] : []), ...list.map((k) => [k, name(k)])]
@@ -5462,7 +6364,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5462
6364
  ...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
6365
  h('div', { className: 'row' }, h('label', null, t('hoDest')),
5464
6366
  h('div', { className: 'pathrow' },
5465
- h('input', { type: 'text', value: dest, spellCheck: false, 'aria-label': t('hoDest'), onChange: (e) => typeDest(e.target.value) }),
6367
+ typing || !dest.trim()
6368
+ ? 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() } })
6369
+ : 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
6370
  h(DirButton, { t, onPicked: pickDest, onSay })),
5467
6371
  checking ? h('p', { className: 'moved' + (checking === 'busy' ? '' : ' bad') }, checking === 'busy' ? t('hoChecking') : checking)
5468
6372
  : moved && moved.dest === dest ? h('p', { className: 'moved' }, t(moved.why === 'beside' ? 'hoBeside' : 'hoMoved', { name: moved.dest.split(/[\\/]/).pop() })) : null),
@@ -5487,40 +6391,155 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5487
6391
  window.removeEventListener('keydown', esc)
5488
6392
  }
5489
6393
  }, [open])
6394
+ // 菜单比剩下的地方高(屏多、侧栏矮):限高、整个菜单能滚,底下的入口也点得到
6395
+ react.useLayoutEffect(() => {
6396
+ const wrap = ref.current
6397
+ const m = open && wrap ? wrap.querySelector(':scope > .ddp-menu') : null
6398
+ const box = wrap ? wrap.closest('.ddp-stage, .ddp') : null
6399
+ if (!m || !box) return
6400
+ const b = box.getBoundingClientRect()
6401
+ const r = m.getBoundingClientRect()
6402
+ const up = r.bottom <= wrap.getBoundingClientRect().top + 1
6403
+ const room = Math.floor(up ? r.bottom - b.top - 8 : b.bottom - r.top - 8)
6404
+ if (r.height > room) { m.style.maxHeight = Math.max(120, room) + 'px'; m.style.overflowY = 'auto' }
6405
+ }, [open])
5490
6406
  }
5491
6407
 
5492
6408
  /** 导出菜单:这一屏(带外框 / 纯画面 / 整页长图)、整套展示图 */
5493
- function ExportMenu({ t, kind, count, open, setOpen, onPick, disabled, zip }) {
5494
- const ref = useRef(null)
5495
- useDismiss(open, setOpen, ref)
6409
+ /** 导出的几种:这一屏(带外框 / 纯画面 / 整页长图)、整套展示图、PDF、PPT */
6410
+ function exportItems(t, kind, count, name) {
5496
6411
  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')]) // 海报、幻灯片画幅是定的,没有「整页」
6412
+ const n = { name: name || t('exThisName') } // 写出是哪一屏,别只说「这一屏」
6413
+ if (FRAMED[kind]) items.push(['frame', I.single, t('exFrame', n)])
6414
+ items.push(['raw', I.image, t('exRaw', n)])
6415
+ if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong', n)]) // 海报、幻灯片画幅是定的,没有「整页」
5500
6416
  if (count > 1) items.push(['show', I.canvas, t('exShow')])
5501
6417
  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)
6418
+ return items
5509
6419
  }
5510
6420
 
5511
6421
  /**
5512
- * 「更多」:右侧栏窄的时候,工具栏上放不下的几个按钮收进这里(哪几个收起来由容器宽度的 CSS 决定)。
5513
- * items: [显示档 'o1' | 'o2', 图标, 名字, 选中, 不可用, 点了做什么(字符串就是在浏览器里打开的地址)]
6422
+ * 工具栏上的下拉菜单(视图、交付、更多都用它)。
6423
+ * items: [类名, 图标, 名字, 选中, 不可用, 点了做什么(字符串就是在浏览器里打开的地址), 右边小字]
6424
+ * 字符串 'hd:名字' 是小标题,'sep' 是分隔线
5514
6425
  */
5515
- function MoreMenu({ t, open, setOpen, has, items }) {
6426
+ // 浮块按钮的悬停说明:系统 title 要等一秒多、只有一行灰字;这里停一下就出,上面名字、下面一句用途
6427
+ // 用法:按钮带上 tipA(名字, 说明);HoverTip 放进浮块所在的那层,自己盯着里面带 data-tip 的东西
6428
+ 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 })
6429
+ function HoverTip() {
6430
+ const ref = useRef(null)
6431
+ const [tip, setTip] = useState(null) // { n, d, k, cx, y, up }
6432
+ useEffect(() => {
6433
+ const el = ref.current
6434
+ const box = el && el.parentElement
6435
+ if (!box) return
6436
+ let cur = null // 鼠标停在哪个按钮上
6437
+ let timer = 0
6438
+ let showing = false
6439
+ let warmTill = 0 // 刚看过一个:挪到旁边的按钮立刻出,不再等
6440
+ const show = (b) => {
6441
+ if (!b.isConnected || b.getAttribute('aria-expanded') === 'true') return
6442
+ const r = b.getBoundingClientRect()
6443
+ const s = box.getBoundingClientRect()
6444
+ const up = r.top + r.height / 2 > s.top + s.height / 2 // 下半截的按钮往上弹
6445
+ showing = true
6446
+ 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 })
6447
+ }
6448
+ const hide = () => {
6449
+ clearTimeout(timer)
6450
+ if (showing) warmTill = Date.now() + 600
6451
+ showing = false
6452
+ setTip(null)
6453
+ }
6454
+ const enter = (b) => {
6455
+ if (b === cur) return
6456
+ cur = b
6457
+ clearTimeout(timer)
6458
+ if (!b) return hide()
6459
+ if (showing || Date.now() < warmTill) show(b)
6460
+ else timer = setTimeout(() => show(b), 350)
6461
+ }
6462
+ const over = (e) => { const b = e.target.closest ? e.target.closest('[data-tip]') : null; enter(b && box.contains(b) ? b : null) }
6463
+ const leave = () => enter(null)
6464
+ const down = () => { clearTimeout(timer); showing = false; warmTill = 0; setTip(null) } // 点了就收,等鼠标挪开再说
6465
+ 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) } }
6466
+ const blur = () => { cur = null; hide() }
6467
+ const key = (e) => { if (e.key === 'Escape') down() }
6468
+ box.addEventListener('pointerover', over)
6469
+ box.addEventListener('pointerleave', leave)
6470
+ box.addEventListener('pointerdown', down, true)
6471
+ box.addEventListener('focusin', focus)
6472
+ box.addEventListener('focusout', blur)
6473
+ box.addEventListener('keydown', key, true)
6474
+ return () => {
6475
+ clearTimeout(timer)
6476
+ box.removeEventListener('pointerover', over)
6477
+ box.removeEventListener('pointerleave', leave)
6478
+ box.removeEventListener('pointerdown', down, true)
6479
+ box.removeEventListener('focusin', focus)
6480
+ box.removeEventListener('focusout', blur)
6481
+ box.removeEventListener('keydown', key, true)
6482
+ }
6483
+ }, [])
6484
+ // 量出自己多宽再摆(画出来之前):横着别出边,上弹的贴着按钮上沿
6485
+ react.useLayoutEffect(() => {
6486
+ const el = ref.current
6487
+ if (!el || !tip) return
6488
+ const W = el.parentElement ? el.parentElement.clientWidth : 0
6489
+ const w = el.offsetWidth
6490
+ el.style.left = Math.max(8, Math.min(W - w - 8, tip.cx - w / 2)) + 'px'
6491
+ el.style.top = (tip.up ? tip.y - el.offsetHeight : tip.y) + 'px'
6492
+ }, [tip])
6493
+ // 说明已经在按钮的 aria-label 里,读屏不用再念一遍
6494
+ return h('div', { ref, className: 'ddp-tipbox' + (tip ? ' on' : ''), 'aria-hidden': 'true' },
6495
+ tip ? h('div', { className: 'nm' }, h('b', null, tip.n), tip.k ? h('kbd', null, tip.k) : null) : null,
6496
+ tip && tip.d ? h('span', null, tip.d) : null)
6497
+ }
6498
+
6499
+ function DropMenu({ t, cls, btnCls, title, tip, icon, label, caret, has, open, setOpen, disabled, items, foot }) {
5516
6500
  const ref = useRef(null)
5517
6501
  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)),
6502
+ return h('div', { className: 'ddp-kind' + (cls ? ' ' + cls : ''), ref },
6503
+ 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) },
6504
+ icon ? h(icon) : null, label ? h('span', { className: 'lb' }, label) : null, caret ? h(I.caret) : null),
5520
6505
  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)
6506
+ ...items.filter(Boolean).map((x, i) => {
6507
+ if (typeof x === 'string') {
6508
+ if (x.startsWith('hd:')) return h('div', { key: 'h' + i, className: 'hd' }, x.slice(3))
6509
+ return h('div', { key: 's' + i, className: 'sp ' + x.slice(4), role: 'separator' })
6510
+ }
6511
+ const [tier, Icon, name, on, off, run, em] = x
6512
+ const kids = [h(Icon), h('span', null, name), em != null && em !== '' ? h('em', null, em) : null]
6513
+ return typeof run === 'string'
6514
+ ? h('a', { key: name, className: tier, role: 'menuitem', href: run, target: '_blank', rel: 'noopener noreferrer', onClick: () => setOpen(false) }, ...kids)
6515
+ : h('button', { key: name, className: tier + (on ? ' sel' : ''), role: 'menuitem', disabled: off, onClick: () => { setOpen(false); run() } }, ...kids)
6516
+ }),
6517
+ foot ? h('div', { className: 'ft' }, foot) : null) : null)
6518
+ }
6519
+
6520
+ // 左上屏名 ▾:换屏(画布 / 某一屏)、多尺寸、类型、打开别的页面——「在看什么、怎么看」都在这一个菜单里
6521
+ function ScreenMenu({ t, label, title, tip, src, busy, list, cur, canvas, open, setOpen, onPick, onAll, onOther, sizes, kind, onKind }) {
6522
+ const ref = useRef(null)
6523
+ useDismiss(open, setOpen, ref)
6524
+ const n = list.length
6525
+ const close = (run) => () => { setOpen(false); run() }
6526
+ return h('div', { className: 'ddp-kind ddp-where', ref },
6527
+ 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) },
6528
+ h('i', { className: 'ddp-dot' + (busy ? ' wait' : '') }),
6529
+ (() => { const m = /^(.+?)( · [^·]+)$/.exec(label || ''); return m ? h('span', { className: 'lb2' }, h('b', null, m[1]), h('em', null, m[2])) : h('span', null, label) })(),
6530
+ h(I.caret)),
6531
+ open ? h('div', { className: 'ddp-menu l', role: 'menu' },
6532
+ h('div', { className: 'hd' }, n > 1 ? t('screensHd') : t('screensT')),
6533
+ 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),
6534
+ 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)) },
6535
+ h(I.single), h('span', null, x.label)))),
6536
+ sizes ? h('div', { className: 'sp', role: 'separator' }) : null,
6537
+ 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,
6538
+ onKind ? h('div', { className: 'hd' }, t('kind')) : null,
6539
+ 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)) },
6540
+ h(I[k]), h('span', null, t(KIND_KEY[k]))))) : null,
6541
+ h('div', { className: 'sp', role: 'separator' }),
6542
+ h('button', { role: 'menuitem', onClick: close(onOther) }, h(I.folder), h('span', null, t('openOther')))) : null)
5524
6543
  }
5525
6544
 
5526
6545
  /** 二维码矩阵(一行一串 0 / 1)→ SVG(四周留 4 格白边) */
@@ -5553,10 +6572,12 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5553
6572
  seen ? h('span', { key: 'st', className: 'st ok' }, h(I.check), t('phoneSeen')) : h('span', { key: 'st', className: 'st' }, h('i'), t('phoneWait')),
5554
6573
  h('p', { key: 'fl', className: 'fine' }, t('phoneFollow')),
5555
6574
  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')),
6575
+ h('button', { className: 'ddp-go sec', onClick: onStop }, t('phoneStop')),
6576
+ h('button', { className: 'ddp-go', onClick: () => onCopy(res.url) }, t('phoneCopy'))),
6577
+ h('details', { key: 'more', className: 'fine', open: false },
6578
+ h('summary', null, t('phoneTrouble')),
6579
+ h('p', null, t('phoneHelp')),
6580
+ h('p', null, t('phoneAuto'))),
5560
6581
  ]
5561
6582
  }
5562
6583
  return h('div', { className: 'ddp-tokens ddp-phone', onPointerDown: (e) => e.stopPropagation() },
@@ -5652,9 +6673,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5652
6673
  w.exists
5653
6674
  ? [
5654
6675
  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)),
6676
+ h('button', { key: 'r', title: t('wkReveal'), onClick: () => op(w, 'reveal') }, h(I.folder), h('span', null, t('wkRevealShort'))),
6677
+ h('button', { key: 'z', title: t('wkZip'), onClick: () => onPack(w.dir) }, h(I.download), h('span', null, t('wkZipShort'))),
6678
+ h('button', { key: 's', title: t('wkShare'), onClick: () => onShare(w.dir) }, h(I.share), h('span', null, t('wkShareShort'))),
5658
6679
  h('button', { key: 'p', title: t(w.pinned ? 'wkUnpin' : 'wkPin'), className: w.pinned ? 'on' : '', onClick: () => op(w, w.pinned ? 'unpin' : 'pin') }, '★'),
5659
6680
  ]
5660
6681
  : dirPicker() ? h('button', { key: 'l', onClick: () => relocate(w) }, t('wkRelocate')) : null,
@@ -5797,28 +6818,108 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5797
6818
  }
5798
6819
  const mkRevoked = (v, id, rev) => !!v && (v.revoked || []).some((r) => r.id === id && (!r.rev || !rev || r.rev === rev))
5799
6820
 
6821
+ // 首页顶上一条:左边名字,右边「开始 / 市场 / 我的作品 / 灵感」——设计库并进首页的页签
6822
+ const HOME_TABS = [['', 'homeTab'], ['market', 'libMarket'], ['works', 'libWorks'], ['inspire', 'libInspire']]
6823
+ function HomeBar({ t, tab, onTab, lead }) {
6824
+ // 左右方向键在几栏之间挪焦点,回车 / 空格再打开(按到哪就开哪会一路弹设计库)
6825
+ const onKey = (e) => {
6826
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return
6827
+ const bs = [...e.currentTarget.querySelectorAll('[role=tab]')]
6828
+ const i = Math.max(0, bs.indexOf(document.activeElement))
6829
+ const n = e.key === 'Home' ? 0 : e.key === 'End' ? bs.length - 1 : (i + (e.key === 'ArrowRight' ? 1 : -1) + bs.length) % bs.length
6830
+ e.preventDefault()
6831
+ if (bs[n]) bs[n].focus()
6832
+ }
6833
+ return h('header', { className: 'ddp-homebar' + (lead ? ' led' : '') },
6834
+ lead || null,
6835
+ h('div', { className: 'nm' }, h('i', { className: 'mk', 'aria-hidden': true }), h('b', null, t('title'))),
6836
+ 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)))))
6837
+ }
6838
+
6839
+ // 首页大输入框:说一句 + 类型 + 风格 → onSend(发给模型)。没写字时 Tab 用占位里的例子
6840
+ const ASK_KINDS = ['app', 'miniprogram', 'web', 'poster', 'slides', 'tablet', 'desktop', 'email']
6841
+ const ASK_EX = { web: 'exWeb', app: 'exApp', miniprogram: 'exMini', tablet: 'exTablet', desktop: 'exDesktop', poster: 'exPoster', slides: 'exSlides', email: 'exEmail' }
6842
+ function HomeAsk({ t, onSend, onRef, plus, draft, note, autoFocus }) {
6843
+ const d0 = (draft && draft.current) || {}
6844
+ const [text, setText] = useState(d0.text || '')
6845
+ const [kind, setKind] = useState(d0.kind || '')
6846
+ const [sty, setSty] = useState(d0.sty || null)
6847
+ useEffect(() => { if (draft) draft.current = { text, kind, sty } }, [text, kind, sty])
6848
+ useEffect(() => { if (autoFocus && ta.current) ta.current.focus({ preventScroll: true }) }, [])
6849
+ const [pop, setPop] = useState('') // 'plus' | 'style'
6850
+ const ta = useRef(null)
6851
+ const card = useRef(null)
6852
+ useDismiss(pop === 'style', () => setPop(''), card)
6853
+ const en = t('lang') === 'en'
6854
+ const ex = t(ASK_EX[kind] || 'exAuto')
6855
+ const back = () => { if (ta.current) ta.current.focus() }
6856
+ const go = () => {
6857
+ const s = text.trim()
6858
+ if (!s) return back()
6859
+ if (onSend({ text: s, kind, style: sty }) !== false) setText('')
6860
+ }
6861
+ const onKey = (e) => {
6862
+ if (e.nativeEvent && e.nativeEvent.isComposing) return
6863
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); go() } else if (e.key === 'Tab' && !e.shiftKey && !text) { e.preventDefault(); setText(ex) }
6864
+ }
6865
+ return h('div', { className: 'ddp-askwrap' },
6866
+ h('div', { className: 'ddp-ask', ref: card },
6867
+ 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 }),
6868
+ h('div', { className: 'row' },
6869
+ h(DropMenu, { t, cls: 'ddp-askplus', tip: [t('askMore'), t('askMoreD')], icon: I.plus, open: pop === 'plus', setOpen: (v) => setPop(v ? 'plus' : ''), items: plus }),
6870
+ h('button', { className: 'chip ref', ...tipA(t('askRef'), t('askRefD')), onClick: onRef }, h(I.image), h('span', null, t('askRef'))),
6871
+ 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') },
6872
+ sty ? h('i', { className: 'sw', style: { background: `linear-gradient(135deg,${sty.c.primary} 0 50%,${sty.c.accent} 50%)` } }) : h(I.palette),
6873
+ h('span', null, sty ? t('askStyle', { name: en ? sty.en : sty.zh }) : t('askStyleAuto')), h(I.caret)),
6874
+ !text ? h('button', { className: 'tabhint', tabIndex: -1, onClick: () => { setText(ex); back() } }, h('kbd', null, 'Tab'), t('askTab')) : null,
6875
+ h('button', { className: 'send', disabled: !text.trim(), ...tipA(t('askSend'), '', '↵'), onClick: go }, h(I.send))),
6876
+ pop === 'style' ? h('div', { className: 'ddp-askpop', role: 'dialog', 'aria-label': t('askStylePick') },
6877
+ h('div', { className: 'hd' }, h('b', null, t('askStylePick')), h('button', { className: 'lk', onClick: () => { setSty(null); setPop(''); back() } }, t('askStyleAutoBtn'))),
6878
+ h(StyleCards, { t, onPick: (x) => { setSty(x); setPop(''); back() } })) : null),
6879
+ note ? h('p', { className: 'ddp-asknote' }, h(I.info), h('span', null, note)) : null,
6880
+ 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) },
6881
+ h(I[k]), h('span', null, t(KIND_KEY[k]))))))
6882
+ }
6883
+
6884
+ // 首页「接着做」:最近的三套(找不到文件夹的不放),点了直接打开
6885
+ function HomeWorks({ t, onOpen, onAll, cur, onBack }) {
6886
+ const norm = (x) => String(x || '').replace(/[\\/]+$/, '')
6887
+ const [list, setList] = useState(null)
6888
+ useEffect(() => {
6889
+ let dead = false
6890
+ getJson('works').then((d) => { if (!dead) setList(d.works || []) }, () => { if (!dead) setList([]) })
6891
+ return () => { dead = true }
6892
+ }, [])
6893
+ const ws = (list || []).filter((w) => w.exists).slice(0, 3)
6894
+ if (!ws.length) return null
6895
+ return h('section', { className: 'ddp-homesec' },
6896
+ h('div', { className: 'hd' }, h('b', null, t('homeRecent')), h('button', { className: 'lk', onClick: onAll }, t('homeRecentAll'))),
6897
+ 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)) },
6898
+ here ? h('em', { className: 'now' }, t('wkHere')) : null,
6899
+ 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)),
6900
+ 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(' · ')))) })))
6901
+ }
6902
+
5800
6903
  function TplStart({ t, onOpen, onMore }) {
5801
6904
  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) : []
6905
+ 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
6906
  if (!list.length) return null
5804
- return h('div', { className: 'ddp-tplstart' },
6907
+ return h('section', { className: 'ddp-homesec ddp-tplstart' },
5805
6908
  h('div', { className: 'hd' }, h('b', null, t('tplStartHead')), h('button', { className: 'lk', onClick: onMore }, t('tplStartMore'))),
5806
6909
  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
6910
  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))))))
6911
+ 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
6912
  }
5810
6913
 
5811
- function MarketTab({ t, focus, onUseTpl, onUseStyle, onSay }) {
6914
+ function MarketTab({ t, cur, setCur, onUseTpl, onUseStyle, onSay }) {
5812
6915
  const [d, setD] = useState(null)
5813
6916
  const [err, setErr] = useState('')
5814
6917
  const [typePick, setType] = useState('template')
5815
6918
  const [kind, setKind] = useState('')
5816
6919
  const [q, setQ] = useState('')
5817
6920
  const [sort, setSort] = useState('featured')
5818
- const [cur, setCur] = useState(focus || '')
5819
6921
  const [busy, setBusy] = useState(false)
5820
6922
  const mine = useInspire() // 收藏过的风格打勾(按市场 id 认)
5821
- useEffect(() => { if (focus) setCur(focus) }, [focus])
5822
6923
  const load = (refresh) => {
5823
6924
  if (refresh) setBusy(true)
5824
6925
  return loadMarket(refresh).then((x) => { setD(x); setErr('') }, (e) => setErr(e.message)).finally(() => setBusy(false))
@@ -5858,27 +6959,25 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5858
6959
  .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
6960
  const kinds = MK_KINDS.filter((k) => d.designs.some((e) => e.kind === k))
5860
6961
  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(' · ')
6962
+ const by = e.author && !/^orderg-x$/i.test(e.author.login || '') ? e.author.name || e.author.login : '' // 官方做的不写作者(每张都一样)
6963
+ 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
6964
  return h('div', { key: e.id, className: 'ddp-work ddp-mk' },
5864
6965
  h('button', { className: 'th', title: mkPick(t, e.description) || mkPick(t, e.title), onClick: () => setCur(e.id) },
5865
6966
  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
6967
  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))
6968
+ e.featured && sort !== 'featured' ? h('em', null, t('mkSortFeatured')) : null), // 按精选排时精选的本来就在前面,不再每张标
6969
+ h('div', { className: 'nm' }, h('b', { title: mkPick(t, e.title) }, mkPick(t, e.title))),
6970
+ h('div', { className: 'mt', title: meta }, meta))
5872
6971
  }
5873
6972
  return h('div', { className: 'ddp-market' },
5874
6973
  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,
6974
+ 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
6975
  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,
6976
+ 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),
6977
+ // 类型一排横着滑;设计系统、风格不分类型:灰着,不收起来(下面的卡片不跳)
6978
+ kinds.length > 1 ? h('div', { className: 'ddp-mkkinds' },
6979
+ kinds.length > 1 ? [h('button', { key: '', className: kind === '' && type === 'template' ? 'on' : '', disabled: type !== 'template', onClick: () => setKind('') }, t('mkAllKinds')),
6980
+ ...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
6981
  d.off || d.error || d.needUpdate ? h('p', { className: 'ddp-libtip warn' }, d.needUpdate ? t('mkNeedUpdate') : d.off ? t('mkOff') : t('mkOffline')) : null,
5883
6982
  !list.length ? h('div', { className: 'ddp-libnone' }, t('mkNoResult'))
5884
6983
  : type === 'style'
@@ -5947,7 +7046,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5947
7046
  : 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
7047
  : e.thumb ? h('img', { src: `${API}mthumb?id=${encodeURIComponent(e.id)}&r=${encodeURIComponent(e.rev || '')}`, alt: '' }) : null
5949
7048
  return h('div', { className: 'ddp-mkd' },
5950
- h('button', { className: 'lk back', onClick: onBack }, '← ', t('mkBack')),
5951
7049
  h('h3', null, mkPick(t, e.title)),
5952
7050
  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
7051
  mkPick(t, e.description) ? h('p', null, mkPick(t, e.description)) : null,
@@ -5967,18 +7065,26 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
5967
7065
  e.derivedFrom ? h('li', null, t('mkDerived', { x: String(e.derivedFrom) })) : null),
5968
7066
  h('p', { className: 'ddp-libfoot' },
5969
7067
  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)))
7068
+ h('a', { href: report, target: '_blank', rel: 'noopener noreferrer', title: e.id }, t('mkReport'))))
5971
7069
  }
5972
7070
 
5973
- function LibraryView({ t, tab, setTab, focus, hasDesign, curRoot, onClose, onOpen, onPack, onShare, onSaveStyle, onUseStyle, onUseTpl, onRef, onExtract, onMarket, onSay }) {
7071
+ function LibraryView({ t, tab, setTab, focus, hasDesign, homey, curRoot, onClose, onOpen, onPack, onShare, onSaveStyle, onUseStyle, onUseTpl, onRef, onExtract, onMarket, onSay }) {
5974
7072
  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)))),
7073
+ const [mkCur, setMkCur] = useState(focus || '') // 市场里点开的那一套(详情页)
7074
+ useEffect(() => { if (focus) setMkCur(focus) }, [focus])
7075
+ const inDetail = tab === 'market' && !!mkCur
7076
+ // 只有一个返回:在详情页回列表,不然回画布 / 空白页
7077
+ const back = () => (inDetail ? setMkCur('') : onClose())
7078
+ const backTip = inDetail ? t('mkBack') : t(hasDesign ? 'libBack' : 'libClose')
7079
+ return h('div', { className: 'ddp-lib', onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => { if (e.key === 'Escape') { e.stopPropagation(); back() } } },
7080
+ homey
7081
+ ? 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 })
7082
+ : h('header', null,
7083
+ h('button', { className: 'ddp-btn', title: backTip, 'aria-label': backTip, onClick: back }, h(I.back)),
7084
+ h('b', null, t('library')),
7085
+ 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
7086
  h('div', { className: 'bd' },
5981
- tab === 'market' ? h(MarketTab, { t, focus, onUseTpl, onUseStyle, onSay })
7087
+ tab === 'market' ? h(MarketTab, { t, cur: mkCur, setCur: setMkCur, onUseTpl, onUseStyle, onSay })
5982
7088
  : tab === 'works' ? h(WorksTab, { t, curRoot, onOpen, onPack, onShare, onMarket, onSay })
5983
7089
  : h(InspireTab, { t, hasDesign, onSaveStyle, onUseStyle, onRef, onExtract, onMarket, onSay })))
5984
7090
  }
@@ -6031,7 +7137,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6031
7137
  const SHARE_AGREE = [['own', 'shAgreeOwn'], ['brand', 'shAgreeBrand'], ['private', 'shAgreePrivate'], ['rules', 'shAgreeRules']]
6032
7138
 
6033
7139
  /** 分享到设计市场:填名字、介绍、授权,勾四个确认 → 按分享的规矩查(个人信息硬拦)→ 打 zip → 打开预填好的 GitHub 投稿页 */
6034
- function SharePanel({ t, sid, root, onFix, onSay, onClose }) {
7140
+ function SharePanel({ t, sid, root, brand, onFix, onSay, onClose }) {
6035
7141
  const L = t('lang') === 'en' ? 'en' : 'zh'
6036
7142
  const [info, setInfo] = useState(null)
6037
7143
  const [title, setTitle] = useState('')
@@ -6048,7 +7154,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6048
7154
  post('pack', { session: sid, dir: root, info: true, lang: L }).then((d) => {
6049
7155
  if (!on) return
6050
7156
  setInfo(d)
6051
- setTitle(d.title || '')
7157
+ setTitle(d.title && d.title !== baseName(root) ? d.title : brand || d.title || '')
6052
7158
  setDesc(d.description || '')
6053
7159
  setLic(d.license === 'CC-BY-4.0' ? 'CC-BY-4.0' : 'CC0-1.0')
6054
7160
  }, (e) => { if (on) setInfo({ error: e.message }) })
@@ -6069,6 +7175,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6069
7175
  const done = res && res.ok && res.share
6070
7176
  return h('div', { className: 'ddp-tokens ddp-pack ddp-share', onPointerDown: (e) => e.stopPropagation() },
6071
7177
  h('header', null, h(I.share), h('b', null, t('shTitle')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
7178
+ h('p', { className: 'tip gh' }, h(I.info), h('span', null, t('shGh'))),
6072
7179
  h('p', { className: 'tip' }, t('shTip')),
6073
7180
  h('div', { className: 'bd' },
6074
7181
  root ? h('p', { className: 'which', title: root }, h(I.folder), h('span', null, baseName(root))) : h('div', { className: 'none' }, t('pkNoDesign')),
@@ -6125,6 +7232,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6125
7232
  const [cam, setCam] = useState(null)
6126
7233
  const [anim, setAnim] = useState(false)
6127
7234
  const [drag, setDrag] = useState(false)
7235
+ const [zoomMenu, setZoomMenu] = useState(false)
6128
7236
  const auto = useRef(true)
6129
7237
  const camRef = useRef(cam)
6130
7238
  camRef.current = cam
@@ -6135,8 +7243,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6135
7243
  // 多尺寸:每块自己的类型和大小(items[i] = { kind, size })
6136
7244
  const items = props.items || null
6137
7245
  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('|') : ''
7246
+ // 上下各有一排浮块:排的时候只用中间那块高度,镜头整体往下挪 rt
7247
+ const rt = props.top || 0
7248
+ const bh = Math.max(0, box.h - rt - (props.bottom || 0))
7249
+ const L = box.w > 0 && bh > 0 ? (boxes ? layoutRow(boxes, box.w, bh) : layoutGrid(n, dw, dh, box.w, bh)) : null
7250
+ // 右边开着面板(inset 像素被挡着):排法不变,镜头只用左边看得见的那块(太窄就不让,照旧整块)
7251
+ const ins = props.inset && box.w - props.inset >= 240 ? props.inset : 0
7252
+ const vw = box.w - ins
7253
+ const layoutKey = L ? [n, L.cols, dw, dh, box.w, box.h, rt, bh, ins, boxes ? boxes.join(';') : ''].join('|') : ''
7254
+ const fitAt = () => { const c = fitCam(L, vw, bh); return { ...c, y: c.y + rt } }
6140
7255
 
6141
7256
  useEffect(() => {
6142
7257
  const el = ref.current
@@ -6154,16 +7269,16 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6154
7269
  if (!L) return
6155
7270
  if (auto.current || !camRef.current) {
6156
7271
  setAnim(!!camRef.current)
6157
- setCam(fitCam(L, box.w, box.h))
6158
- }
7272
+ setCam(fitAt())
7273
+ } else if (ins && props.sel) showKey(props.sel) // 自己挪过镜头:只把选中的那屏挪出来,别被面板挡着
6159
7274
  }, [layoutKey])
6160
7275
 
6161
7276
  // 让某一屏露出来(体检里点了一条):整屏不在看得见的地方就把镜头挪过去;右边 inset 像素被面板挡着
6162
7277
  const rv = props.reveal
6163
- useEffect(() => {
7278
+ function showKey(key) {
6164
7279
  const c = camRef.current
6165
- if (!rv || !L || !c) return
6166
- const i = screens.findIndex((x) => x.key === rv.key)
7280
+ if (!key || !L || !c) return
7281
+ const i = screens.findIndex((x) => x.key === key)
6167
7282
  if (i < 0) return
6168
7283
  const [cw, ch] = boxes ? boxes[i] : [dw, dh]
6169
7284
  const vw = Math.max(120, box.w - (props.inset || 0))
@@ -6174,12 +7289,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6174
7289
  let x = c.x
6175
7290
  let y = c.y
6176
7291
  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
7292
+ if (y0 < rt + LAB || y0 + hh > rt + bh - 8) y += (hh > bh - LAB - 8 ? rt + LAB : rt + (bh - hh) / 2) - y0
6178
7293
  if (x === c.x && y === c.y) return
6179
7294
  auto.current = false
6180
7295
  setAnim(true)
6181
7296
  setCam({ ...c, x, y })
6182
- }, [rv && rv.n])
7297
+ }
7298
+ useEffect(() => { if (rv) showKey(rv.key) }, [rv && rv.n])
6183
7299
 
6184
7300
  const zoomAt = (factor, px, py, smooth) => {
6185
7301
  const c = camRef.current
@@ -6199,7 +7315,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6199
7315
  if (!c) return
6200
7316
  if (e.ctrlKey || e.metaKey) {
6201
7317
  const r = el.getBoundingClientRect()
6202
- zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top, false)
7318
+ // 触控板捏合每次只给几个像素;鼠标滚轮(Windows 上 Ctrl + 滚轮)一格就是 100 上下,不收住一格放大近 3 倍
7319
+ const dy = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY
7320
+ zoomAt(Math.exp(-Math.max(-25, Math.min(25, dy)) * 0.01), e.clientX - r.left, e.clientY - r.top, false)
6203
7321
  } else {
6204
7322
  auto.current = false
6205
7323
  setAnim(false)
@@ -6247,7 +7365,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6247
7365
  if (!L) return
6248
7366
  auto.current = true
6249
7367
  setAnim(true)
6250
- setCam(fitCam(L, box.w, box.h))
7368
+ setCam(fitAt())
6251
7369
  }
6252
7370
 
6253
7371
  const z = cam ? cam.z : 1
@@ -6266,7 +7384,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6266
7384
  className: 'ddp-cell' + (on ? ' sel' : ''),
6267
7385
  style: { left: L.pos[i].x, top: L.pos[i].y, width: cw, height: ch },
6268
7386
  },
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),
7387
+ 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
7388
  props.render(sc, z, i),
6271
7389
  picking ? null : h('div', { className: 'ddp-shield', 'data-key': sc.key, title: items ? '' : t('openScreen'), style: { borderRadius: fr.r } })))
6272
7390
  })
@@ -6274,7 +7392,9 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6274
7392
  const p = L.pos[n - 1]
6275
7393
  cells.push(h('div', { key: '__ghost', className: 'ddp-cell ghost', style: { left: p.x, top: p.y, width: dw, height: dh } },
6276
7394
  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, {}))))
7395
+ h(Device, { kind, w: size[0], h: size[1], label: '' },
7396
+ h(GhostDraw, { key: 'gd', t, kind, pulse: props.pulse || 0 }),
7397
+ props.draft ? h(DraftFrame, { key: 'df', url: props.draft, z, lang: t('lang'), onEnd: props.onDraftEnd }) : null)))
6278
7398
  }
6279
7399
  }
6280
7400
  let flowSvg = null
@@ -6296,11 +7416,15 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6296
7416
  className: 'ddp-world' + (anim ? ' anim' : ''),
6297
7417
  style: { transform: `translate(${cam.x}px, ${cam.y}px) scale(${z})`, '--z': z },
6298
7418
  }, ...cells, flowSvg) : null,
7419
+ // 右下缩放:「43% ▾」,点开是放大 / 缩小 / 适合画布 / 实际大小
6299
7420
  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))))
7421
+ h(DropMenu, { t, tip: [t('zoomT'), t('dZoom', { mod: MOD.replace(/\+$/, '') })], label: Math.round(z * 100) + '%', caret: true, open: zoomMenu, setOpen: setZoomMenu, items: [
7422
+ ['', I.plus, t('zoomIn'), false, z >= ZMAX, () => center(1.25)],
7423
+ ['', I.minus, t('zoomOut'), false, z <= ZMIN, () => center(1 / 1.25)],
7424
+ 'sep',
7425
+ ['', I.fit, t('fit'), false, false, fit],
7426
+ ['', I.single, t('zoomReal'), Math.abs(z - 1) < 0.005, false, () => center(1 / z)],
7427
+ ] })))
6304
7428
  }
6305
7429
 
6306
7430
  // ── 右侧栏标签页 ─────────────────────────────────────────────────────────
@@ -6317,7 +7441,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6317
7441
  const [styleEdit, setStyleEdit] = useState(false) // 调样式:点元素、拖滑块,写回它的 style 属性
6318
7442
  const [sty, setSty] = useState(null) // 调样式选中的元素:{ key, url, name, selector, text, loc, base, vars, gradient, layout, live }
6319
7443
  const [editing, setEditing] = useState(false)
6320
- const [menu, setMenu] = useState(false)
6321
7444
  const [toast, setToast] = useState('')
6322
7445
  const [flash, setFlash] = useState('')
6323
7446
  const [reloadSeq, setReloadSeq] = useState(0)
@@ -6326,14 +7449,19 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6326
7449
  const setTop = (k, dk) => setTops((cur) => (cur[k] === dk ? cur : { ...cur, [k]: dk }))
6327
7450
  const [sel, setSel] = useState('')
6328
7451
  const [panel, setPanel] = useState(false)
6329
- const [styles, setStyles] = useState(false) // 换风格面板
7452
+ const [lookTab, setLookTab] = useState('set') // 外观面板开在哪一页:set 整套 / style 换风格 / brand 品牌
7453
+ const [tv, setTv] = useState({}) // 外观面板里直接改的设计变量:名字 → 值(还没写进文件)
6330
7454
  const [dropping, setDropping] = useState('') // 正拖着文件进来:dragKind 给的种类
6331
7455
  const [dropSoft, setDropSoft] = useState(false) // 拖动在预览页面里:蒙层只是提示、不挡事件(松手由页面转告)
6332
7456
  const softTimer = useRef(0)
6333
7457
  const refInput = useRef(null)
6334
7458
  const pageInput = useRef(null)
7459
+ const homeRef = useRef(null)
7460
+ const homeKind = useRef('') // 首页发出去时选的类型:第一屏出来前的占位按它画
6335
7461
  const [exMenu, setExMenu] = useState(false)
6336
- const [more, setMore] = useState(false) // 窄的时候收起来的几个按钮
7462
+ const [more, setMore] = useState(false) // ⋯ 菜单(窄的时候收起来的几个按钮也在这里)
7463
+ const [scrMenu, setScrMenu] = useState(false)
7464
+ const brandMem = useRef({}) // 目录 → 产品名(画布上认出来的),单页时去掉标题里的它
6337
7465
  const [hist, setHist] = useState(false) // 版本面板
6338
7466
  const [versions, setVersions] = useState([])
6339
7467
  const [ver, setVer] = useState(null) // 正在看的旧版本
@@ -6349,6 +7477,27 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6349
7477
  const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
6350
7478
  const [pane, setPane] = useState('') // 右边的面板:tweaks 调节滑块、systems 品牌设计系统、handoff 转成代码、pack 打成 zip、zip 设计包没过检查
6351
7479
  const [lib, setLib] = useState('') // 设计库:'' 关着;开着时是哪一栏(market / works / inspire)
7480
+ // 开着设计时叠上来的首页:'' 没开 / 'on' 开着 / 'off' 正在收起(放完收起动画再拿掉)
7481
+ const [homeAt, setHomeAt] = useState('')
7482
+ const homeDraft = useRef({ text: '', kind: '', sty: null }) // 首页输入框里写了一半的:收起再打开还在
7483
+ const homeOpen = () => { setLib(''); setLibFocus(''); setHomeAt('on') }
7484
+ const homeClose = () => {
7485
+ setHomeAt((c) => (c === 'on' ? 'off' : c))
7486
+ setTimeout(() => setHomeAt((c) => (c === 'off' ? '' : c)), reduced() ? 0 : 190)
7487
+ }
7488
+ const homeAway = () => { if (homeAt === 'on') homeClose() } // 从首页打开了设计、进了打包面板:首页收起,别挡着
7489
+ // 首页 / 设计库叠在上面时,Esc 只归它们:底下的调样式、改字、点选、试用别跟着退(没存的改动会丢)
7490
+ const overlayUp = () => { try { return !!document.querySelector('.ddp-home.over, .ddp-libwrap') } catch { return false } }
7491
+ useEffect(() => {
7492
+ if (homeAt !== 'on') return undefined
7493
+ const esc = (e) => {
7494
+ if (e.key !== 'Escape' || e.defaultPrevented) return
7495
+ if (document.querySelector('.ddp-home.over [aria-expanded="true"], .ddp-libwrap')) return // 先收菜单、设计库
7496
+ homeClose()
7497
+ }
7498
+ window.addEventListener('keydown', esc)
7499
+ return () => window.removeEventListener('keydown', esc)
7500
+ }, [homeAt])
6352
7501
  const [libFocus, setLibFocus] = useState('') // 从空白页「从模板开始」点进来的那一套
6353
7502
  const libDefault = () => (marketStore.has ? 'market' : 'works')
6354
7503
  // 市场还没读过(刚开页面):等读完(本地缓存,很快)再定开哪一页
@@ -6387,6 +7536,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6387
7536
  // 这一轮开始时已有的屏记下来:这轮里出了新的一屏,就在画布末尾留个「生成中…」等下一屏;
6388
7537
  // 一阵子(GHOST_MS)没再出新屏多半是在改了,占位撤掉;正在写一个还没有的 HTML 时一定留着
6389
7538
  const marks = useRef({ site: '', born: new Map(), built: new Set(), turnKeys: null, lastNew: 0 })
7539
+ const draftedR = useRef(new Map()) // 边写边看过的屏 → 什么时候:真文件出来时不再播一遍生成动画
7540
+ const pulseR = useRef({ key: '', lines: 0, n: 0 }) // 模型真有动静就加一(占位里的鼠标按它动)
7541
+ const singleDraftR = useRef('') // 单屏里正在看着写的那屏:写完换过去看它
7542
+ const playR = useRef({ url: '', key: '', hold: 0, end: false, meta: '', lump: false }) // 在放的草稿(见下面 dr)
6390
7543
  const M = marks.current
6391
7544
  if (M.site !== site) {
6392
7545
  M.site = site
@@ -6402,13 +7555,27 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6402
7555
  M.born.set(sc.key, busy)
6403
7556
  if (busy) M.lastNew = Date.now()
6404
7557
  }
6405
- const isBorn = (key) => !!M.born.get(key) && !M.built.has(key)
7558
+ const isBorn = (key) => !!M.born.get(key) && !M.built.has(key) && !(Date.now() - (draftedR.current.get(key) || 0) < 180000)
7559
+ // 状态条从哪一刻开始计时:服务端说这一轮开始多久了(右侧栏中途才打开也对得上)
7560
+ const turnT = useRef({ on: false, at: 0 })
7561
+ if (busy && !turnT.current.on) turnT.current = { on: true, at: Date.now() - Math.max(0, Number(st && st.turnFor) || 0) }
7562
+ else if (!busy && turnT.current.on) turnT.current = { on: false, at: 0 }
7563
+ // 开始/结束生成时单屏的设备要让位(给状态条腾一条):这一下缓过去,别一跳
7564
+ const [ease, setEase] = useState(false)
7565
+ const busySeen = useRef(busy)
7566
+ useEffect(() => {
7567
+ if (busySeen.current === busy) return
7568
+ busySeen.current = busy
7569
+ setEase(true)
7570
+ const id = setTimeout(() => setEase(false), 520)
7571
+ return () => clearTimeout(id)
7572
+ }, [busy])
6406
7573
 
6407
7574
  const infoOf = (key) => info[site + '|' + key] || null
6408
7575
  const ik = (sc) => (sc.v ? 'v' + sc.v + '|' : '') + sc.key // 旧版本的屏单独记标题
6409
7576
  const focus = (st && st.focus) || ''
6410
7577
  const main = infoOf(focus) || (screens[0] && infoOf(screens[0].key)) || {}
6411
- const meta = parseMeta(main.meta || '')
7578
+ const meta = parseMeta(main.meta || (st && st.draft && st.draft.meta) || (playR.current.end ? '' : playR.current.meta) || '') // 第一屏还在放草稿(真的没摆出来):按草稿开头写的类型
6412
7579
  const tokens = main.tokens || []
6413
7580
  const kind = (st && st.kind) || meta.kind || 'web'
6414
7581
  const size = sizeOf((st && st.size) || meta.size, kind)
@@ -6416,14 +7583,68 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6416
7583
  const canvas = view === 'canvas'
6417
7584
  const writingNew = !!(st && st.writing) && /\.html?$/i.test(st.writing) && !screens.some((s) => s.key === st.writing)
6418
7585
  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)
7586
+ // 边写边看:模型正在写一屏新的,预览服务器上已经有它的草稿(远程看的时候本机地址打不开,还是放占位)。
7587
+ // 正文先到的草稿 key 是空的(还不知道是哪屏),名字到了才填上;写完落地后草稿还留一会儿,靠它认出新屏
7588
+ const sd = st && st.draft && st.draft.url && (busy || st.draft.live) && !away(st.draft.url) ? st.draft : null // 说完了还报的:刚写完的那份
7589
+ // 看着放的那份草稿:整页一下子到的(lump,预览服务器几秒里一截截放出来),真文件落地了也先放完再换。
7590
+ // 一路流着写的早看过了,落地就换真文件(不等放完:页面引的外部字体卡着的话 load 要等很久)。
7591
+ // 右侧栏头一回看到它时它得是还在写 / 刚写完的(live),中途才打开右侧栏的不补放旧的
7592
+ const PL = playR.current
7593
+ 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 }
7594
+ else if (sd && sd.url === PL.url) {
7595
+ if (sd.key) PL.key = sd.key
7596
+ if (sd.meta) PL.meta = sd.meta
7597
+ if (sd.lump) PL.lump = true
7598
+ }
7599
+ const play = playR.current
7600
+ const landed = (key) => !!key && screens.some((x) => x.key === key)
7601
+ const playing = !!play.url && play.lump && !play.end && !(sd && sd.url !== play.url) && (!play.hold || Date.now() - play.hold < HOLD_MAX)
7602
+ const dr = sd && !landed(sd.key) ? sd : playing ? { key: play.key, url: play.url } : null
7603
+ if (dr && dr.url === play.url && !play.hold && (!sd || landed(dr.key))) play.hold = Date.now() // 从这时起是「放完再换」:最多等 HOLD_MAX
7604
+ const hideKey = dr && landed(dr.key) ? dr.key : '' // 放着的这屏已经落地:画布上先不摆真的那块(草稿就在占位那格里放)
7605
+ if (sd && sd.key) draftedR.current.set(sd.key, Date.now())
7606
+ const ghost = canvas && !ver && ((busy && (writingNew || fresh)) || !!dr)
6420
7607
  const [, setTick] = useState(0)
7608
+ const draftEnd = (u) => {
7609
+ const p = playR.current
7610
+ if (p.url !== u || p.end) return
7611
+ p.end = true
7612
+ setTick((n) => n + 1)
7613
+ }
7614
+ useEffect(() => {
7615
+ if (!play.hold || play.end) return
7616
+ const id = setTimeout(() => setTick((n) => n + 1), Math.max(0, play.hold + HOLD_MAX - Date.now()) + 50)
7617
+ return () => clearTimeout(id)
7618
+ }, [play.url, play.hold, play.end])
6421
7619
  useEffect(() => {
6422
7620
  if (!fresh || writingNew) return
6423
7621
  const id = setTimeout(() => setTick((n) => n + 1), Math.max(0, M.lastNew + GHOST_MS - Date.now()) + 50)
6424
7622
  return () => clearTimeout(id)
6425
7623
  }, [fresh, writingNew, M.lastNew])
6426
7624
  const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
7625
+ // 状态条上那句:在等你回答 / 正在做第几屏 / 正在改哪屏 / 按调的工具说 / 没调工具就是在想
7626
+ const busyPhase = () => {
7627
+ if (!st) return { text: t('working') }
7628
+ if (st.asking) return { text: t('asking'), asking: true }
7629
+ const newN = screens.length + 1 // 按画布上排第几屏说(已有 3 屏、再出一屏 = 第 4 屏),和新屏出来的位置对得上
7630
+ const w = st.writing || ''
7631
+ const ln = Number(st.lines) || 0
7632
+ const more = ln >= 10 ? t('stLines', { n: ln }) : '' // 写了多少行:写大文件的几分钟里看得到在动
7633
+ if (w) {
7634
+ if (!/\.html?$/i.test(w)) return { text: t('writing', { file: w }), sub: more }
7635
+ const sc = screens.find((x) => x.key === w)
7636
+ return sc ? { text: t('stEdit', { name: labelOf(sc) }), sub: more } : { text: t('stNew', { n: newN }), sub: more ? `${w} · ${more}` : w }
7637
+ }
7638
+ if (st.draft && !st.draft.key && /^(write|create_file)$/i.test(String(st.tool || ''))) return { text: t('stNew', { n: newN }), sub: more } // 正文先到:看得出是新的一屏,名字还没到
7639
+ const tool = String(st.tool || '')
7640
+ if (!tool) return { text: t('stThink') }
7641
+ const m = TOOL_PHASE.find(([re]) => re.test(tool))
7642
+ if (m && m[1] === 'stWriteAny' && !url) return { text: t('stNew', { n: 1 }), sub: more } // 第一屏还没出来:写的就是它
7643
+ return { text: m ? t(m[1]) : t('working'), sub: m && m[1] === 'stWriteAny' ? more : '' }
7644
+ }
7645
+ const busyBar = (side) => h(BusyBar, { key: 'busy', t, phase: busyPhase(), since: turnT.current.at || Date.now(), side })
7646
+ // 占位里的鼠标什么时候动:换了在做的事、或者又写了 15 行——模型停着它们就停着
7647
+ const pulse = pulseStep(pulseR.current, busy, busy ? busyPhase().text + '|' + ((st && st.tool) || '') : '', Number(st && st.lines) || 0)
6427
7648
  // 画布上的标题去掉几屏都带的产品名(「首页 · 梅林练」→「首页」);产品名留给导出的展示图做大标题
6428
7649
  const shortOf = (sc) => {
6429
7650
  const sp = splitTitles(shown.map(labelOf))
@@ -6448,8 +7669,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6448
7669
  return Object.keys(out).length === Object.keys(c).length ? c : out
6449
7670
  })
6450
7671
  }, [tsig])
6451
- useEffect(() => { setTw({}) }, [site])
6452
- const tweak = Object.keys(tw).length ? tweakMsg(tlist, tw) : null
7672
+ const twGen = useRef(0) // 换一次设计加一:晚到的保存回包认得出是上一套的
7673
+ useEffect(() => { twGen.current++; setTw({}); setTv({}) }, [site])
7674
+ // 拖的滑块 + 外观面板里直接改的颜色:一起发给页面先看效果
7675
+ 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
7676
 
6454
7677
  // 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
6455
7678
  const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
@@ -6490,6 +7713,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6490
7713
  try {
6491
7714
  await openTarget(sid, p)
6492
7715
  setEditing(false)
7716
+ homeAway() // 从叠着的首页 / 设计库里打开的:收起来,看新打开的
7717
+ setLib('')
6493
7718
  } catch (e) {
6494
7719
  say(t('openFailed', { msg: e.message }))
6495
7720
  }
@@ -6561,6 +7786,16 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6561
7786
  if (!patch.focus && poll && poll.state) poll.set({ ...poll.state, ...patch })
6562
7787
  post('view', { session: sid, ...patch }).then((s) => poll && poll.set(s), (e) => say(t('openFailed', { msg: e.message })))
6563
7788
  }
7789
+ // 单屏里看着写完的那屏:真文件出来了就换过去看它(不然写完一闪又回到原来那屏)
7790
+ useEffect(() => {
7791
+ let k = singleDraftR.current
7792
+ if (k === '*' && sd && sd.key) k = singleDraftR.current = sd.key
7793
+ if (!k || dr) return
7794
+ if (screens.some((x) => x.key === k)) {
7795
+ singleDraftR.current = ''
7796
+ if (!canvas && focus !== k) change({ view: 'single', focus: k })
7797
+ } else if (!busy) singleDraftR.current = ''
7798
+ })
6564
7799
 
6565
7800
  // 量舞台大小,单页时按比例缩放
6566
7801
  useEffect(() => {
@@ -6674,7 +7909,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6674
7909
  setPicking(false)
6675
7910
  setTextEdit(false)
6676
7911
  setPanel(false)
6677
- setStyles(false)
6678
7912
  setHist(false)
6679
7913
  setPhone(false)
6680
7914
  closeCheck()
@@ -6718,7 +7952,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6718
7952
  }
6719
7953
  useEffect(() => {
6720
7954
  if (!styleEdit) return undefined
6721
- const onKey = (e) => { if (e.key === 'Escape') setStyleEdit(false) }
7955
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setStyleEdit(false) }
6722
7956
  window.addEventListener('keydown', onKey)
6723
7957
  return () => window.removeEventListener('keydown', onKey)
6724
7958
  }, [styleEdit])
@@ -6734,14 +7968,14 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6734
7968
 
6735
7969
  useEffect(() => {
6736
7970
  if (!textEdit) return undefined
6737
- const onKey = (e) => { if (e.key === 'Escape') setTextEdit(false) }
7971
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setTextEdit(false) }
6738
7972
  window.addEventListener('keydown', onKey)
6739
7973
  return () => window.removeEventListener('keydown', onKey)
6740
7974
  }, [textEdit])
6741
7975
 
6742
7976
  useEffect(() => {
6743
7977
  if (!picking) return undefined
6744
- const onKey = (e) => { if (e.key === 'Escape') setPicking(false) }
7978
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setPicking(false) }
6745
7979
  window.addEventListener('keydown', onKey)
6746
7980
  return () => window.removeEventListener('keydown', onKey)
6747
7981
  }, [picking])
@@ -6798,7 +8032,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6798
8032
  }
6799
8033
  useEffect(() => {
6800
8034
  if (!demo) return undefined
6801
- const onKey = (e) => { if (e.key === 'Escape') setDemo(null) }
8035
+ const onKey = (e) => { if (e.key === 'Escape' && !overlayUp()) setDemo(null) }
6802
8036
  window.addEventListener('keydown', onKey)
6803
8037
  return () => window.removeEventListener('keydown', onKey)
6804
8038
  }, [!!demo])
@@ -6964,15 +8198,21 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
6964
8198
  insert(g.letter ? t('pickText', { name: labelOf(sc), where, others, base: g.stem }) : t('pickKeep', { name: labelOf(sc), where, others }), 'insertedSend')
6965
8199
  }
6966
8200
 
6967
- // 几个方向并排时每屏标题旁都挂着「就要这个」,不用先点中那一屏才看得到
8201
+ // 同一屏的几个版本:标 A / B / C(原版或 -a 是 A)
8202
+ const takeTag = (sc) => {
8203
+ const g = !sc.v && takesOf(shown.map((x) => x.key), sc.key)
8204
+ return g ? (g.letter || 'a').toUpperCase() : ''
8205
+ }
8206
+ const tagOf = (sc) => { const L = takeTag(sc); return L ? h('b', { key: 'g', className: 'ddp-vt', title: t('takeN', { l: L }) }, L) : null }
8207
+ // 几个方向并排时每屏标题旁都挂着「用这版」,不用先点中那一屏才看得到
6968
8208
  const badge = (sc) => !sc.v && takesOf(shown.map((x) => x.key), sc.key)
6969
8209
  ? h('button', { key: 'p', className: 'ddp-take', title: t('pickThis'), onClick: () => pickDirection(sc) }, h(I.check), h('span', null, t('pickShort')))
6970
8210
  : null
6971
8211
 
6972
8212
  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)),
8213
+ 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'))),
8214
+ 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'))),
8215
+ 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
8216
  ]
6977
8217
 
6978
8218
  // 对比:拖中间的把手
@@ -7097,14 +8337,13 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7097
8337
  setTextEdit(false)
7098
8338
  setStyleEdit(false)
7099
8339
  setPanel(false)
7100
- setStyles(false)
7101
8340
  setHist(false)
7102
8341
  setPhone(false)
7103
8342
  closeCheck()
7104
8343
  setPane('')
7105
8344
  // 之前点选 / 调样式留下的「已选」框别夹在标注里
7106
8345
  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)
8346
+ setMarkSize(box.w && box.h > TOP_R + BOT_R ? { w: box.w, h: box.h - TOP_R - BOT_R } : null)
7108
8347
  setMark(true)
7109
8348
  }
7110
8349
  // 换了设计、看旧版本、演示:画的东西对不上了,收掉
@@ -7294,13 +8533,14 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7294
8533
  useEffect(() => { if (checkOff && ck) closeCheck() }, [checkOff])
7295
8534
 
7296
8535
  const toggleDemo = () => (demo ? setDemo(null) : startDemo())
7297
- const toggleHist = () => { setHist(!hist); setPanel(false); setStyles(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8536
+ const openLook = (tab) => { if (tab) setLookTab(tab); setPanel(true); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8537
+ const toggleHist = () => { setHist(!hist); setPanel(false); setPhone(false); closeCheck(); setStyleEdit(false) }
7298
8538
  // 右边同一时间只开一个面板:别的开了,这几个关掉;开这几个时别的关掉
7299
- const oldOpen = !!(panel || styles || hist || phone || ck || styleEdit)
8539
+ const oldOpen = !!(panel || hist || phone || ck || styleEdit)
7300
8540
  useEffect(() => { if (oldOpen) setPane('') }, [oldOpen])
7301
8541
  const openPane = (p) => {
7302
8542
  const next = pane === p ? '' : p
7303
- if (next) { setPanel(false); setStyles(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
8543
+ if (next) { setPanel(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
7304
8544
  setPane(next)
7305
8545
  }
7306
8546
  const setTweak = (k, v) => setTw((c) => {
@@ -7308,12 +8548,24 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7308
8548
  return { ...c, [k.id]: v }
7309
8549
  })
7310
8550
  const saveTweaks = async () => {
8551
+ if (ver) return say(t('twVer')) // 旧版本的屏不收调节:存了改的是当前设计
7311
8552
  try {
7312
8553
  // 带上拖的是哪套设计:请求路上会话换了设计,服务端就不写
7313
- const r = await post('tweak', { session: sid, root: st && st.target ? st.target.root : '', values: tw })
8554
+ const sentTw = tw
8555
+ const sentTv = tv
8556
+ const gen = twGen.current
8557
+ const r = await post('tweak', { session: sid, root: st && st.target ? st.target.root : '', values: sentTw, vars: sentTv })
7314
8558
  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')) }))
8559
+ 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
8560
  else showFlash(t('twSaved', { n: r.changed.length }))
8561
+ // 写进去的(设计变量、滑块):放掉,页面重新读文件就是新值了(滑块和配色管同一个颜色时以配色为准,滑块的旧值也得放掉);
8562
+ // 没写进去的留着,还能交给模型;请求路上又改过的也留着
8563
+ if (r.changed && r.changed.length && gen === twGen.current) {
8564
+ const ids = new Set(r.changed.map((x) => x.id))
8565
+ 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 }
8566
+ setTv((c) => drop(c, sentTv))
8567
+ setTw((c) => drop(c, sentTw))
8568
+ }
7317
8569
  } catch (e) {
7318
8570
  say(t('failed', { msg: e.message }))
7319
8571
  }
@@ -7330,58 +8582,88 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7330
8582
  }), 'insertedSend')
7331
8583
  setPane('')
7332
8584
  }
7333
- const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
8585
+ const togglePhone = () => { setPhone(!phone); setPanel(false); setHist(false); closeCheck(); setStyleEdit(false) }
7334
8586
  const reload = () => setReloadSeq((x) => x + 1)
7335
8587
 
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
- ] }))
8588
+ // 浮在画布上的几块,各管一件事,按钮都带字:左上 设计库 | 屏名 ▾(换屏、多尺寸、类型都在这个菜单里);
8589
+ // 右上 外观 体检 | 导出 ▾ ⋯;底部中间 选择 改字 样式 画笔 | 试用;右下缩放(画布自己带)
8590
+ // 鼠标停一下出说明卡(HoverTip):名字 + 一句用途
8591
+ // 窄了(样式表里):先截屏名,再收字
8592
+ const curKey = canvas ? '' : (focus || (screens[0] && screens[0].key) || '')
8593
+ const curSc = screens.find((x) => x.key === curKey)
8594
+ // 产品名:画布上几屏标题都带它时认得出来;单页只拿得到一屏的标题,用记下的那个把它去掉(「订单 · 巷口面馆」→「订单」)
8595
+ const brand0 = splitTitles(shown.map(labelOf)).brand
8596
+ if (brand0) brandMem.current[site] = brand0
8597
+ const brand = brand0 || brandMem.current[site] || ''
8598
+ const dropBrand = (x) => {
8599
+ if (!brand || x === brand) return x
8600
+ const p = x.split(/\s*[·||—]\s*|\s+[-–]\s+/).map((y) => y.trim()).filter(Boolean)
8601
+ return p.length >= 2 && (p[0] === brand || p[p.length - 1] === brand) ? p.filter((y) => y !== brand).join(' · ') : x
8602
+ }
8603
+ const stLabel = (st && st.label) || ''
8604
+ const scrLabel = canvas && screens.length > 1
8605
+ ? t('screensN', { name: brand || stLabel, n: screens.length })
8606
+ : curSc ? dropBrand(shortOf(curSc)) + (takeTag(curSc) ? ' · ' + takeTag(curSc) : '') : stLabel
8607
+ // 菜单里点某一屏:单看这一屏(「全部 n 屏」回画布)
8608
+ const pickScreen = (key) => {
8609
+ setMulti(false)
8610
+ if (demo) setDemo(null)
8611
+ setSel(key)
8612
+ if (canvas || key !== focus) change({ view: 'single', focus: key })
8613
+ }
8614
+ // 导出菜单写出是哪一屏(和 doExport 取同一屏)
8615
+ const exSc = shown.find((x) => x.key === (canvas ? sel || focus : focus)) || shown[0]
8616
+ const exName = exSc ? dropBrand(shortOf(exSc)) + (takeTag(exSc) ? ' · ' + takeTag(exSc) : '') : ''
8617
+ const libOpen = () => { setLibFocus(''); if (lib) setLib(''); else libDefaultThen((k) => setLib((c) => c || k)) }
8618
+ const dockTight = box.w > 0 && box.w <= 400 // 侧栏很窄:底部工具坞只放得下四个,「试用」进「更多」
8619
+ 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))
8620
+ const islands = editing
8621
+ ? [h('div', { key: 'top', className: 'ddp-top' },
8622
+ h('div', { className: 'ddp-isl tl wide' },
8623
+ 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 })))]
8624
+ : [
8625
+ h('div', { key: 'top', className: 'ddp-top' },
8626
+ h('div', { className: 'ddp-isl tl' },
8627
+ 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'))),
8628
+ h('span', { className: 'ddp-sep' }),
8629
+ 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,
8630
+ onPick: pickScreen, onAll: () => { setMulti(false); if (demo) setDemo(null); change({ view: 'canvas' }) }, onOther: () => setEditing(true),
8631
+ sizes: mark ? null : { on: inMulti, off: !sizeSet || !!ver, tip: sizeSet ? t('sizes') : t('sizesNo'), note: sizeSet ? '' : t('sizesNoShort'), run: () => setMulti(!inMulti) },
8632
+ kind, onKind: mark ? null : (k) => change({ kind: k }) })),
8633
+ h('div', { className: 'ddp-isl tr' },
8634
+ 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'))),
8635
+ 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()) },
8636
+ h(I.stetho), h('span', { className: 'lb' }, t('checkShort')), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
8637
+ h('span', { className: 'ddp-sep' }),
8638
+ 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: [
8639
+ 'hd:' + t('deliverCode'),
8640
+ ['', I.code, t('handoff'), pane === 'handoff', !!ver, () => openPane('handoff')],
8641
+ 'hd:' + t('exFiles'),
8642
+ ...exportItems(t, kind, shown.length, exName).map(([mode, Icon, label]) => ['', Icon, label, false, false, () => doExport(mode)]),
8643
+ fileTarget ? 'hd:' + t('deliverShare') : null, // 只有本地的设计才能打包、分享
8644
+ fileTarget ? ['', I.box, t('exZip'), false, false, () => doExport('zip')] : null,
8645
+ fileTarget ? ['', I.share, t('exShare'), false, false, () => doExport('share')] : null,
8646
+ ], foot: t('exWhere') }),
8647
+ 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: [
8648
+ dockTight ? ['', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo] : null, // 侧栏太窄,底部工具坞放不下「试用」:挪到这里
8649
+ dockTight ? 'sep' : null,
8650
+ ['', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
8651
+ ['', I.scan, t('phone'), phone, !url, togglePhone, phoneOn ? t('phoneOnShort') : ''],
8652
+ anyDark || dark ? ['', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
8653
+ 'sep',
8654
+ ['', I.refresh, t('refresh'), false, false, reload],
8655
+ ['', I.external, t('external'), false, !url, url],
8656
+ ] }))),
8657
+ h('div', { key: 'dock', className: 'ddp-isl ddp-dock', role: 'toolbar', 'aria-label': t('toolsT') },
8658
+ tool('pick', picking, t('dPick'), !url || !!ver || !!demo || mark, togglePick, I.pick, t('pick')),
8659
+ tool('', textEdit, t('dText'), !url || !!ver || !!demo || busy || mark, toggleText, I.text, t('textShort')),
8660
+ tool('', styleEdit, t('dStyle'), styleOff || mark, toggleStyle, I.sliders, t('styleShort')),
8661
+ tool('', mark, t('dMark'), markOff, toggleMark, I.pen, t('markShort')),
8662
+ dockTight ? null : h('span', { className: 'ddp-sep dm' }),
8663
+ dockTight ? null : tool('dm', !!demo, t('dPlay'), !url || !!ver, toggleDemo, I.play, t('demoShort'))),
8664
+ ]
7378
8665
 
7379
8666
  // 模型用提问卡问你、在等回答:不转圈,换成一个橙点
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
8667
  // ── 参考图:拖进来 / 粘贴 / 从设计规范面板选;存进设计目录的 refs/,尺寸和主要颜色放进输入框 ──
7386
8668
  const addRef = async (file) => {
7387
8669
  if (!file || !/^image\//.test(file.type)) return say(t('refNotImage'))
@@ -7477,9 +8759,10 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7477
8759
  setTab: (k) => { setLibFocus(''); setLib(k) },
7478
8760
  focus: libFocus,
7479
8761
  hasDesign: !!url,
8762
+ homey: !url || homeAt === 'on',
7480
8763
  curRoot: fileTarget ? st.target.root : '',
7481
8764
  onClose: () => { setLib(''); setLibFocus('') },
7482
- onOpen: (w) => openTarget(sid, w.dir).then(() => { setLib(''); setEditing(false) }, (e) => say(t('wkOpenFail', { msg: e.message }))),
8765
+ onOpen: (w) => openTarget(sid, w.dir).then(() => { setLib(''); setEditing(false); homeAway() }, (e) => say(t('wkOpenFail', { msg: e.message }))),
7483
8766
  onPack: openPack,
7484
8767
  onShare: openShare,
7485
8768
  onSaveStyle: saveMyStyle,
@@ -7491,17 +8774,20 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7491
8774
  onSay: say,
7492
8775
  }),
7493
8776
  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)
8777
+ // 开着设计时点「首页」:首页叠在画布上(设计库再叠在它上面)
8778
+ const root = (...kids) => h('div', rootProps, ...kids, url && homeAt ? homeView(true) : null, libLayer, dropLayer, fileInput, pageFileInput)
7495
8779
 
7496
8780
  // ── 设计库:打成 zip、拖进来的设计包、存成我的风格 ──
7497
8781
  function openPack(dir) {
7498
8782
  setPackDir(dir || '')
7499
8783
  setLib('')
8784
+ homeAway()
7500
8785
  if (pane !== 'pack') openPane('pack')
7501
8786
  }
7502
8787
  function openShare(dir) {
7503
8788
  setPackDir(dir || '')
7504
8789
  setLib('')
8790
+ homeAway()
7505
8791
  if (pane !== 'share') openPane('share')
7506
8792
  }
7507
8793
  const sameDir = (a, b) => String(a || '').replace(/[\\/]+$/, '') === String(b || '').replace(/[\\/]+$/, '')
@@ -7532,6 +8818,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7532
8818
  if (d.error === 'check') {
7533
8819
  setZipIssues({ errors: d.errors || [], more: d.more || 0 })
7534
8820
  setLib('')
8821
+ homeAway()
7535
8822
  if (pane !== 'zip') openPane('zip')
7536
8823
  return undefined
7537
8824
  }
@@ -7542,6 +8829,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7542
8829
  }
7543
8830
  setLib('')
7544
8831
  setEditing(false)
8832
+ homeAway()
7545
8833
  if (pane === 'zip' || pane === 'pack' || pane === 'share') { setPane(''); setZipIssues(null) }
7546
8834
  if (poll && d.snap) poll.set(d.snap)
7547
8835
  insert(formatTpl(t, d))
@@ -7560,6 +8848,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7560
8848
  }
7561
8849
  setLib('')
7562
8850
  setEditing(false)
8851
+ homeAway()
7563
8852
  if (pane === 'zip' || pane === 'pack' || pane === 'share') { setPane(''); setZipIssues(null) }
7564
8853
  if (poll && d.snap) poll.set(d.snap)
7565
8854
  insert(formatTpl(t, d))
@@ -7585,20 +8874,6 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7585
8874
 
7586
8875
  // 调节滑块 / 品牌设计系统 / 转成代码(右边的面板)
7587
8876
  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
8877
  if (pane === 'systems') {
7603
8878
  return h(SystemsPanel, {
7604
8879
  t,
@@ -7614,7 +8889,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7614
8889
  return h(PackPanel, { t, sid, root: packDir || (fileTarget ? st.target.root : ''), onFix: fixPack, onSay: say, onClose: () => setPane('') })
7615
8890
  }
7616
8891
  if (pane === 'share') {
7617
- return h(SharePanel, { t, sid, root: packDir || (fileTarget ? st.target.root : ''), onFix: fixPack, onSay: say, onClose: () => setPane('') })
8892
+ 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
8893
  }
7619
8894
  if (pane === 'zip' && zipIssues) {
7620
8895
  return h(ZipIssues, { t, issues: zipIssues, onClose: () => { setPane(''); setZipIssues(null) } })
@@ -7625,46 +8900,114 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7625
8900
  return null
7626
8901
  }
7627
8902
 
8903
+ // 首页大输入框发出去:放进 DSH 的输入框直接发;输入框里本来有字、或旧版 DSH 没有 submit:放进去等用户自己发
8904
+ const homeSend = ({ text, kind, style }) => {
8905
+ const ia = props.inputActions
8906
+ const cel = composerEl()
8907
+ const was = cel ? composerText(cel) : null
8908
+ const had = was === null || !!was.trim() // 读不出来(有引用标签、分屏)当成有字:只放进去,不替用户发
8909
+ const msg = text + (kind ? '\n' + t('askKindLine', { k: t(KIND_KEY[kind]) }) : '') + (url ? '\n' + t('askNewLine') : '')
8910
+ homeKind.current = kind || ''
8911
+ const pick = style || (had ? null : autoStyle(text, kind)) // 没选风格:按题材替模型挑一张(不然总是米色暖调);输入框里本来有东西(风格卡、参考图…)就不挑
8912
+ const sty = pick ? formatStyle(t, pick, false, !style) : ''
8913
+ if (!insert(msg, 'inserted', false)) {
8914
+ insert([msg, sty].filter(Boolean).join('\n\n')) // 插不进:整段复制
8915
+ return false
8916
+ }
8917
+ if (sty) insert(sty, 'inserted', false)
8918
+ if (url) homeClose() // 开着设计时从首页发的:收起首页,看着新的一套长出来
8919
+ if (had || !ia || typeof ia.submit !== 'function') { say(t('insertedSend')); return true }
8920
+ setTimeout(() => {
8921
+ try { ia.submit(); say(t('askSent')) } catch (e) { console.warn('[dsh-design-studio] submit 失败', e); say(t('insertedSend')) }
8922
+ }, 80)
8923
+ return true
8924
+ }
8925
+ // 「+」里的「打开已有的 HTML」:桌面版直接选文件;网页版展开下面的地址框
8926
+ const openHtml = () => {
8927
+ if (browse) return browse()
8928
+ const d = homeRef.current && homeRef.current.querySelector('.ddp-ways')
8929
+ if (!d) { if (url) { homeClose(); setEditing(true) } return } // 叠着的首页没有地址框:用左上角的
8930
+ d.open = true
8931
+ d.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
8932
+ const i = d.querySelector('input')
8933
+ if (i) i.focus({ preventScroll: true })
8934
+ }
8935
+ const toLib = (k, id) => { setLibFocus(id || ''); setLib(k) }
8936
+ 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 },
8937
+ h(HomeBar, {
8938
+ t,
8939
+ tab: '',
8940
+ onTab: (k) => { if (k) toLib(k) },
8941
+ 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,
8942
+ }),
8943
+ h('div', { className: 'ddp-homebd' },
8944
+ h('div', { className: 'ddp-start' },
8945
+ h('div', { className: 'ddp-hero' }, h('h1', null, t('emptyHead')), h('p', null, t('emptySub'))),
8946
+ !over && st && st.hint ? h('button', {
8947
+ className: 'ddp-go',
8948
+ onClick: () => post('open', { session: sid, target: st.hint }).then((x) => { if (poll) poll.set(x) }, (e) => say(t('openFailed', { msg: e.message }))),
8949
+ }, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
8950
+ h(HomeAsk, {
8951
+ t,
8952
+ draft: homeDraft,
8953
+ autoFocus: over,
8954
+ note: over ? t('askNewNote') : '',
8955
+ onSend: homeSend,
8956
+ onRef: pickRef,
8957
+ plus: [
8958
+ ['', I.page, t('askOpenHtml'), false, false, openHtml],
8959
+ ['', I.palette, t('askBrand'), false, false, () => { if (over) homeClose(); openPane('systems') }],
8960
+ ['', I.library, t('askLib'), false, false, () => { setLibFocus(''); libDefaultThen((k) => setLib((c) => c || k)) }],
8961
+ ],
8962
+ }),
8963
+ h(HomeWorks, {
8964
+ t,
8965
+ cur: over && fileTarget ? st.target.root : '',
8966
+ onBack: homeClose,
8967
+ onOpen: (w) => openTarget(sid, w.dir).then(() => { setEditing(false); if (over) homeClose() }, (e) => say(t('wkOpenFail', { msg: e.message }))),
8968
+ onAll: () => toLib('works'),
8969
+ }),
8970
+ h(TplStart, { t, onOpen: (id) => toLib('market', id), onMore: () => toLib('market') }),
8971
+ over ? null : h('details', { className: 'ddp-ways' },
8972
+ h('summary', null, h(I.caret), t('waysHtml')),
8973
+ h('div', { className: 'in' },
8974
+ h('p', null, t(browse ? 'emptyHintPick' : 'emptyHint')),
8975
+ h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {}, onBrowse: browse }))))),
8976
+ h(HoverTip, { key: 'tip' }))
8977
+
7628
8978
  if (!url) {
7629
8979
  const toastEl = toast ? h('div', { className: 'ddp-toast' }, h('span', null, toast.text)) : null
7630
8980
  if (st ? st.loading : !waited) return root(h('div', { className: 'ddp-stage', ref: stageRef }), toastEl)
7631
8981
  if (busy) {
7632
- // 模型在做第一屏:先放一个生成中的占位
8982
+ // 模型在做第一屏:先放一个生成中的占位(按选的类型画个大概的样子)
8983
+ const waitKind = homeKind.current || (st && st.kind) || 'web'
7633
8984
  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'))),
8985
+ h('div', { className: 'ddp-stage bz', ref: stageRef },
8986
+ h('div', { className: 'ddp-wait' },
8987
+ h('div', { className: 'card k-' + waitKind },
8988
+ h(GhostDraw, { key: 'gd', t, kind: waitKind, pulse }),
8989
+ dr ? h(FitDraft, { key: 'df', url: dr.url, kind: waitKind, lang: t('lang'), onEnd: draftEnd }) : null),
8990
+ h('p', null, t(dr ? 'waitingLive' : 'waiting'))),
7636
8991
  h('div', { className: 'ddp-glow' }),
7637
8992
  h('div', { className: 'ddp-ring' }),
7638
- busyChip('')),
8993
+ busyBar(false)),
7639
8994
  toastEl)
7640
8995
  }
7641
8996
  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)) }))),
8997
+ homeView(false),
7659
8998
  pane === 'systems' || pane === 'pack' || pane === 'share' || pane === 'zip' ? sidePane() : null,
7660
8999
  toastEl)
7661
9000
  }
7662
9001
 
9002
+ // 上下两排浮块中间那块的高度:单页、演示、对比按它缩放
9003
+ const ah = Math.max(0, box.h - TOP_R - BOT_R)
9004
+ const cvBody = !demo && (inMulti || (canvas && !(ver && cmp)))
7663
9005
  let body
9006
+ let busyGap = false
7664
9007
  if (demo) {
7665
9008
  const fill = kind === 'web'
7666
9009
  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))
9010
+ const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (ah - 16 - 46) / dh))
7668
9011
  const top = demo.layers[demo.layers.length - 1]
7669
9012
  body = h('div', { className: 'ddp-demo' }, ...demo.layers.map((l) => {
7670
9013
  const isTop = l.id === top.id
@@ -7701,6 +9044,8 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7701
9044
  body = h(Canvas, {
7702
9045
  key: 'multi',
7703
9046
  t,
9047
+ top: TOP_R,
9048
+ bottom: BOT_R,
7704
9049
  screens: fake,
7705
9050
  items,
7706
9051
  kind,
@@ -7738,7 +9083,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7738
9083
  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
9084
  const fill = kind === 'web'
7740
9085
  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))
9086
+ const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (ah - 16 - (ver ? 46 : 0)) / dh))
7742
9087
  // 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
7743
9088
  const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
7744
9089
  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 +9104,23 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7759
9104
  body = h(Canvas, {
7760
9105
  key: 'grid',
7761
9106
  t,
7762
- screens: shown,
9107
+ top: TOP_R + (ver ? 46 : 0), // 看旧版本时上面还有一条横幅
9108
+ bottom: BOT_R,
9109
+ screens: hideKey && !ver ? shown.filter((x) => x.key !== hideKey) : shown,
7763
9110
  actions,
7764
9111
  badge,
9112
+ tag: tagOf,
7765
9113
  kind,
7766
9114
  size,
7767
9115
  ghost,
9116
+ draft: ghost && dr ? dr.url : '',
9117
+ onDraftEnd: draftEnd,
9118
+ pulse,
7768
9119
  picking: picking || textEdit || styleEdit,
7769
9120
  sel: sel || focus,
7770
9121
  labelOf: shortOf,
7771
9122
  reveal,
7772
- inset: ck || styleEdit || panel || styles || pane ? 296 : 0, // 右边的面板 276 宽,离边 10
9123
+ inset: ck || styleEdit || panel || pane || hist || phone ? 296 : 0, // 右边的面板 276 宽,离边 10
7773
9124
  render: (sc, z) => renderScreen(sc, z, false),
7774
9125
  flows,
7775
9126
  onSelect: setSel,
@@ -7779,30 +9130,41 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7779
9130
  const fill = kind === 'web'
7780
9131
  const [dw, dh] = deviceBox(kind, size)
7781
9132
  // 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
7782
- const aside = !fill && (ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? 296 : 0
9133
+ const sideOn = !!(ck || styleEdit || panel || pane || hist || phone) && box.w - 296 >= 240
9134
+ const aside = !fill && sideOn ? 296 : 0
9135
+ // 生成中:右侧栏窄(或开着面板)时状态条在上排浮块下面,手机这类给它让一条;宽的时候它在上排中间,不用让。网页不让(一让就重排)
9136
+ busyGap = busy && !fill && (box.w < 860 || sideOn)
7783
9137
  // 画笔标注开着:网页按开标注时的大小定住(侧栏拉窄了就整体缩小,不重排),画的才对得上
7784
9138
  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))
9139
+ 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
9140
  const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
9141
+ // 单屏时模型在写新的一屏:先看着它写(没在选、改字、调样式、标注的时候),写完换过去
9142
+ const watch = dr && !ver && !demo && !picking && !textEdit && !styleEdit && !mark && !lock
9143
+ if (watch) singleDraftR.current = dr.key || '*' // 还不知道是哪屏:名字到了再说
7787
9144
  body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
7788
9145
  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)))
9146
+ watch
9147
+ ? h(Device, { key: 'draft', kind, w: size[0], h: size[1], fill, label: '', os: meta.os },
9148
+ h(GhostDraw, { key: 'gd', t, kind, pulse }),
9149
+ h(DraftFrame, { key: 'df', url: dr.url, z: fill ? 1 : z, lang: t('lang'), onEnd: draftEnd }))
9150
+ : renderScreen(one, z, fill)))
7790
9151
  }
7791
9152
 
7792
9153
  let chip = null
7793
- const side = canvas || inMulti ? ' left' : ''
9154
+ const sideOpen = !!(ck || styleEdit || panel || pane || hist || phone) && box.w - 296 >= 240 // 右边开着面板:提示放在左边那块的中间
9155
+ const side = sideOpen ? ' side' : ''
7794
9156
  const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : styleEdit && !sty ? t(canvas ? 'styleAny' : 'styleOne') : ''
7795
9157
  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
9158
 
7798
9159
  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),
9160
+ h('div', { className: 'ddp-stage isl' + (busy ? ' bz' : ''), ref: stageRef },
9161
+ h('div', { className: 'ddp-body' + (cvBody ? '' : ' pad' + (ver || demo || hintTop || busyGap ? ' vb' : '') + (ease ? ' ease' : '')) }, body),
7802
9162
  busy ? h('div', { className: 'ddp-glow' }) : null,
7803
9163
  busy ? h('div', { className: 'ddp-ring' }) : null,
9164
+ busy ? busyBar(sideOpen) : null,
7804
9165
  // 右边开着面板时提示条居中在面板左边那块,别被面板盖住一半
7805
- hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
9166
+ // 提示只占一个位置:有新消息(toast)时它先让开,消息走了再回来
9167
+ hintText && !toast ? h('div', { className: 'ddp-hint' + (sideOpen ? ' side' : '') }, hintText) : null,
7806
9168
  ck ? h(CheckPanel, { t, ck, onFocus: focusIssue, onFix: fixIssues, onAgain: runCheck, onClose: closeCheck }) : null,
7807
9169
  styleEdit ? h(StylePanel, {
7808
9170
  t,
@@ -7815,11 +9177,40 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7815
9177
  onSave: saveStyle,
7816
9178
  onClose: () => setStyleEdit(false),
7817
9179
  }) : 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,
9180
+ panel ? h(LookPanel, {
9181
+ t,
9182
+ tab: lookTab,
9183
+ setTab: setLookTab,
9184
+ tokens,
9185
+ tv,
9186
+ onTv: (name, v) => setTv((c) => ({ ...c, [name]: v })),
9187
+ onTokAsk: (tok) => insert(formatToken(tok, stRef.current)),
9188
+ onTokMake: () => insert(t('tokensAskText') + '\n', 'insertedSend'),
9189
+ tw: tweaks,
9190
+ live: tw,
9191
+ onTw: setTweak,
9192
+ onTwMake: (errs) => insert(errs && errs.length ? t('twFixText', { errs: errs.join(';') }) + '\n' : t('twMakeText') + '\n', 'insertedSend'),
9193
+ write: !fileTarget ? { ok: false, tip: t('twNoWrite') } : ver ? { ok: false, tip: t('twVer') } : busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('lookSaveTip') },
9194
+ onReset: () => { setTw({}); setTv({}) },
9195
+ onAsk: () => insert(formatLook(t, tlist, tw, tokens, tv), 'insertedSend'),
9196
+ onSave: saveTweaks,
9197
+ onPickStyle: (x) => { setPanel(false); insert(formatStyle(t, x, true), 'insertedSend') },
9198
+ onRef: pickRef,
9199
+ onSaveStyle: saveMyStyle,
9200
+ onInspire: () => { setPanel(false); setLib('inspire') },
9201
+ brand: lookTab === 'brand' ? h(SystemsPanel, {
9202
+ t,
9203
+ bare: true,
9204
+ rev: (st && st.turns) || 0,
9205
+ hasDesign: !!url,
9206
+ onExtract: (src) => insert(t('sysExtractText', { src }) + '\n', 'insertedSend'),
9207
+ onApply: (x) => insert(t('sysApplyText', { name: x.name, dir: x.dir }) + '\n', 'insertedSend'),
9208
+ onSay: say,
9209
+ onClose: () => setPanel(false),
9210
+ }) : null,
9211
+ onClose: () => setPanel(false),
9212
+ }) : null,
7821
9213
  pane ? sidePane() : null,
7822
- styles ? h(StylesPanel, { t, onPick: (x) => { setStyles(false); insert(formatStyle(t, x, true), 'insertedSend') }, onClose: () => setStyles(false) }) : null,
7823
9214
  phone ? h(PhonePanel, {
7824
9215
  t,
7825
9216
  sid,
@@ -7833,19 +9224,23 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7833
9224
  mark && !markOff ? h(MarkLayer, { t, sending: markBusy, frames: markFrames, onCancel: () => { markRun.current++; setMark(false) }, onSend: sendMark, onCopy: canCopyImage() ? copyMark : null }) : null,
7834
9225
  demo ? h('div', { className: 'ddp-banner' },
7835
9226
  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,
9227
+ h('b', null, t('demoOn')),
9228
+ h('span', { className: 'nm' }, dropBrand((infoOf(demoKey) || {}).title || demoKey.replace(/\.html?$/i, ''))),
9229
+ demo.stack.length < 2 ? h('em', null, t('demoHow')) : null,
9230
+ h('button', { className: 'ddp-btn', ...tipA(t('demoBack')), disabled: demo.stack.length < 2, onClick: demoBack }, h(I.back)),
9231
+ h('button', { className: 'ddp-btn', ...tipA(t('demoRestart')), disabled: demo.stack.length < 2, onClick: demoRestart }, h(I.refresh)),
9232
+ h('button', { className: 'ddp-btn', ...tipA(t('demoExit'), '', 'Esc'), onClick: () => setDemo(null) }, h(I.close))) : null,
7840
9233
  ver ? h('div', { className: 'ddp-banner' },
7841
9234
  h(I.history),
7842
9235
  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')),
9236
+ h('button', { className: 'ddp-go sec' + (cmp ? ' on' : ''), onClick: () => setCmp(!cmp) }, t('compare')),
7844
9237
  h('button', { className: 'ddp-go pri', onClick: doRestore }, t('restore')),
7845
9238
  h('button', { className: 'ddp-btn', title: t('backNow'), onClick: () => { setVer(null); setCmp(false) } }, h(I.close))) : null,
7846
9239
  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))
9240
+ toast ? h('div', { className: 'ddp-toast' + (mark ? ' mk' : ver || demo ? ' low' : '') + (sideOpen && !mark ? ' side' : ''), role: 'status' }, h('span', null, toast.text),
9241
+ toast.act ? h('button', { onClick: () => { toast.act.run(); setToast('') } }, toast.act.label) : null) : null,
9242
+ ...islands,
9243
+ h(HoverTip, { key: 'tip' })))
7849
9244
  }
7850
9245
 
7851
9246
  // ── 标题栏按钮:打开预览,顺便在模型开了新页面时把右侧栏弹出来 ──────────
@@ -7894,7 +9289,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7894
9289
  ctx.effect(() => ctx.sidebarRightTabs.register({
7895
9290
  id: TAB_ID,
7896
9291
  kind: KIND,
7897
- title: () => t('title'),
9292
+ title: () => t('tabTitle'),
7898
9293
  keepMounted: true,
7899
9294
  guide: [{ id: 'open', order: 30, title: () => t('title'), description: () => t('guideDesc'), icon: GuideIcon }],
7900
9295
  }), 'dsh-design-studio: tab type')
@@ -7971,7 +9366,7 @@ ${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"
7971
9366
  // 注意:这里是客户端**服务名**(短的),不是包名
7972
9367
  exports.inject = INJECT
7973
9368
  // 测试用
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 }
9369
+ exports._test = { pulseStep, MarketDetail, dragKind, droppedOf, pickInfo, pickLabel, pickRanges, pickRule, chipOf, chipRanges, makeT, ZH, EN, formatCheck, formatTweaks, markHit, markPath, STYLES, formatStyle, accentUse, autoStyle, 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
9370
  return module.exports
7976
9371
  },
7977
9372
  })