dsh-design-studio 0.8.0 → 0.10.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/README.en.md +3 -2
- package/README.md +3 -2
- package/cordis.patch.yml +6 -4
- package/lib/browser.js +34 -0
- package/lib/client.js +1993 -126
- package/lib/core.js +26 -12
- package/lib/handoff.js +126 -11
- package/lib/index.js +17 -10
- package/lib/look.js +19 -4
- package/lib/picker.js +799 -66
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/skills/ui-design/SKILL.md +4 -3
- package/skills/ui-design/references/app.md +1 -1
- package/skills/ui-design/references/desktop.md +24 -0
- package/skills/ui-design/references/miniprogram.md +36 -0
package/lib/client.js
CHANGED
|
@@ -44,6 +44,26 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
guideDesc: '实时看页面,点选要改的地方',
|
|
45
45
|
empty: '还没有要预览的页面',
|
|
46
46
|
emptyHint: '也可以直接输入 HTML 文件路径或端口:',
|
|
47
|
+
emptyHintPick: '已有 HTML?点「浏览…」选一个,或直接拖进来;也能输入路径或端口:',
|
|
48
|
+
browse: '浏览…',
|
|
49
|
+
browseTip: '选一个本地 HTML 文件打开',
|
|
50
|
+
pathNone: '拿不到这个文件的路径,请直接输入它的路径打开',
|
|
51
|
+
pageDrop: '松开,打开这个页面',
|
|
52
|
+
maybeDrop: '松开,打开这个文件夹或 HTML',
|
|
53
|
+
pageDropSub: 'HTML 文件,或放着 HTML 的文件夹',
|
|
54
|
+
dropWeb: '网页版拿不到本地文件的路径',
|
|
55
|
+
dropWebSub: '请输入它在运行 DSH 的那台电脑上的路径打开',
|
|
56
|
+
dropWebSay: '网页版拿不到本地文件的路径:请输入它在运行 DSH 的那台电脑上的路径打开',
|
|
57
|
+
dropWhat: '这里只接 HTML、文件夹或图片',
|
|
58
|
+
dropWhatSub: 'HTML 或文件夹会打开预览,图片当参考图',
|
|
59
|
+
dropWhatWeb: '这里只接图片',
|
|
60
|
+
dropWhatWebSub: '图片当参考图;要打开 HTML,请输入它的路径',
|
|
61
|
+
pickDir: '选文件夹',
|
|
62
|
+
pickDirFail: '没打开选文件夹的窗口:{msg}',
|
|
63
|
+
hoMoved: '选的文件夹里已经有别的东西,代码会放进里面的 {name}(可以改)',
|
|
64
|
+
hoBeside: '代码不能放进设计目录(设计稿要留着对比),改放在旁边的 {name}(可以改)',
|
|
65
|
+
hoChecking: '正在看这个文件夹…',
|
|
66
|
+
hoCheckFail: '没看成这个文件夹({msg}),开始前请确认路径',
|
|
47
67
|
hintOpen: '预览刚写的 {name}',
|
|
48
68
|
headerHint: '刚写了 {name},点开能预览',
|
|
49
69
|
placeholder: '/路径/index.html 或 5173',
|
|
@@ -52,6 +72,24 @@ window.__ModuleLoader__.load({
|
|
|
52
72
|
pickTip: '点选元素或框选区域,发给模型(Esc 取消)',
|
|
53
73
|
pickAny: '点选任意一屏里的元素,或拖出一块区域 · Esc 取消',
|
|
54
74
|
pickOne: '点选一个元素,或拖出一块区域 · Esc 取消',
|
|
75
|
+
pickArea: '选中区域',
|
|
76
|
+
chipOne: '1 个元素',
|
|
77
|
+
chipMark: '标注图',
|
|
78
|
+
chipText: '改字',
|
|
79
|
+
chipStyle: '样式改动',
|
|
80
|
+
chipVariant: '再出两版',
|
|
81
|
+
chipDir: '选定方向',
|
|
82
|
+
chipCheck: '体检 · {n} 个问题',
|
|
83
|
+
chipCode: '转成代码',
|
|
84
|
+
chipRef: '参考图',
|
|
85
|
+
chipTweak: '滑块调整',
|
|
86
|
+
chipTwMake: '做调节滑块',
|
|
87
|
+
chipTwFix: '修调节滑块',
|
|
88
|
+
chipSysEx: '提取设计系统',
|
|
89
|
+
chipSysAp: '套用设计系统',
|
|
90
|
+
chipLook: '风格 · {name}',
|
|
91
|
+
chipTokens: '建设计变量',
|
|
92
|
+
pickElems: '{n} 个元素',
|
|
55
93
|
textAny: '点任意一屏里的文字直接改 · 回车保存 · Esc 退出',
|
|
56
94
|
refPick: '照参考图改风格…',
|
|
57
95
|
refChoose: '选一张',
|
|
@@ -85,6 +123,7 @@ window.__ModuleLoader__.load({
|
|
|
85
123
|
kindApp: 'App',
|
|
86
124
|
kindMini: '小程序',
|
|
87
125
|
kindTablet: '平板',
|
|
126
|
+
kindDesktop: '桌面软件',
|
|
88
127
|
kindPoster: '海报',
|
|
89
128
|
kindSlides: '幻灯片',
|
|
90
129
|
kindEmail: '邮件',
|
|
@@ -119,7 +158,8 @@ window.__ModuleLoader__.load({
|
|
|
119
158
|
checkFail: '没查成:{msg}',
|
|
120
159
|
checkNote: '只量得出排版上的硬伤,好不好看还得你看',
|
|
121
160
|
checkCount: '设计体检:上次量出 {n} 个问题,点开看',
|
|
122
|
-
checkAsk: '〔设计体检〕发现 {n}
|
|
161
|
+
checkAsk: '〔设计体检〕发现 {n} 个问题:',
|
|
162
|
+
checkTail: '请逐个修好:改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量。',
|
|
123
163
|
ckToolHead: '量了 {s} 屏,发现 {n} 个问题。逐个改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量:',
|
|
124
164
|
ckToolOk: '量了 {s} 屏({labels}),没发现问题。',
|
|
125
165
|
ckToolOff: '用户正在看旧版本、演示或多尺寸,右侧栏现在量不了;过一会儿再调,或照「自查」清单自己过一遍。',
|
|
@@ -141,6 +181,11 @@ window.__ModuleLoader__.load({
|
|
|
141
181
|
ck_jserr: '页面脚本报错',
|
|
142
182
|
ck_missing: '文件没载出来',
|
|
143
183
|
ck_capsule: '被小程序胶囊挡住',
|
|
184
|
+
ck_island: '被灵动岛挡住',
|
|
185
|
+
ck_homebar: '伸进底部横条区',
|
|
186
|
+
ck_tabbar: '标签栏项太多',
|
|
187
|
+
ck_navbar: '没有导航栏',
|
|
188
|
+
ck_winctl: '被窗口按钮挡住',
|
|
144
189
|
cd_hscroll: '伸出屏幕 {px}px',
|
|
145
190
|
cd_clipped: '被切掉 {px}px',
|
|
146
191
|
cd_overflow: '伸出框 {px}px',
|
|
@@ -158,6 +203,15 @@ window.__ModuleLoader__.load({
|
|
|
158
203
|
cd_jserr: '{msg}{at}',
|
|
159
204
|
cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
|
|
160
205
|
cd_capsule: '伸进右上角胶囊 {px}px(胶囊是微信画的、盖不住:顶栏右边从 x={x} 起空着)',
|
|
206
|
+
cd_island: '被灵动岛挡住 {px}px(灵动岛在 y 11–45:状态栏中间空着,内容放到状态栏下面)',
|
|
207
|
+
cd_homebar: '{bar} 里的字或图标伸进底部横条区 {px}px(底部留 34px:栏加 padding-bottom: 34px,预览里 env(safe-area-inset-bottom) 是 0)',
|
|
208
|
+
cd_tabbar: '标签栏有 {n} 项(最多 5 项)',
|
|
209
|
+
cd_navbar: '顶上 y {t}–{b}(导航栏)是空的:一级页放标题或品牌,二级页标题居中加返回',
|
|
210
|
+
cd_winctl: '伸进{osName}窗口按钮 {px}px(外框画的、盖在页面上面:{zone}空着)',
|
|
211
|
+
ckOs_mac: ' Mac ',
|
|
212
|
+
ckOs_win: ' Windows ',
|
|
213
|
+
ckZone_mac: '左上角 x 80、y 40 以内',
|
|
214
|
+
ckZone_win: '右上角 138×32',
|
|
161
215
|
ckEdge_top: '顶栏',
|
|
162
216
|
ckEdge_bottom: '底栏',
|
|
163
217
|
ckWhen_top: '没滚时',
|
|
@@ -202,7 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
202
256
|
pickShort: '就要这个',
|
|
203
257
|
pickText: '〔选定方向〕就用「{name}」({where})。先把同组的 {others} 挪进 drafts/ 子目录(别删),再把它改名成 {base}.html;它的设计变量搬进 tokens.css(没有就新建)、改用 tokens.css。然后接着做全套。\n',
|
|
204
258
|
pickKeep: '〔选定方向〕留原版「{name}」({where})。同组的 {others} 挪进 drafts/ 子目录(别删),其他不动。\n',
|
|
205
|
-
variantText: '给「{name}」再出两版不同方向的设计(布局或视觉风格明显不同),存成同目录下的 {b}-b.html 和 {b}-c.html
|
|
259
|
+
variantText: '给「{name}」再出两版不同方向的设计(布局或视觉风格明显不同),存成同目录下的 {b}-b.html 和 {b}-c.html(原版:{where}),会紧挨着原版排在画布上。原版和 tokens.css 别动;新版要换配色就在自己文件里覆盖变量。\n',
|
|
206
260
|
exThis: '导出这一屏',
|
|
207
261
|
demo: '演示:点页面里的按钮跳到别的屏',
|
|
208
262
|
demoOn: '演示中 · 点按钮跳转',
|
|
@@ -210,8 +264,50 @@ window.__ModuleLoader__.load({
|
|
|
210
264
|
demoRestart: '从头开始',
|
|
211
265
|
demoExit: '退出演示(Esc)',
|
|
212
266
|
textEdit: '改字:点页面里的文字直接改,回车保存(Esc 退出)',
|
|
267
|
+
mark: '画笔标注:在页面上画圈、箭头、写字,拼成一张图交给模型',
|
|
268
|
+
mkPen: '画笔',
|
|
269
|
+
mkHl: '荧光笔',
|
|
270
|
+
mkArrow: '箭头',
|
|
271
|
+
mkRect: '方框',
|
|
272
|
+
mkOval: '圆圈',
|
|
273
|
+
mkText: '文字',
|
|
274
|
+
mkErase: '橡皮',
|
|
275
|
+
mkColor: '颜色',
|
|
276
|
+
mkSize: '粗细',
|
|
277
|
+
mkUndo: '撤销',
|
|
278
|
+
mkRedo: '重做',
|
|
279
|
+
mkClear: '全部清掉',
|
|
280
|
+
mkHint: '在页面上画,画完点「交给模型」',
|
|
281
|
+
mkHow_pen: '按住拖动,随手画',
|
|
282
|
+
mkHow_hl: '按住拖过去,把要改的地方涂亮',
|
|
283
|
+
mkHow_arrow: '从起点按住,拖到要指的地方',
|
|
284
|
+
mkHow_rect: '按住拖出一个框,框住要改的地方',
|
|
285
|
+
mkHow_oval: '按住拖一下,圈出要改的地方',
|
|
286
|
+
mkHow_text: '在要说明的地方点一下,写字,回车写完',
|
|
287
|
+
mkHow_erase: '点一下笔画就擦掉,也能按住拖过去',
|
|
288
|
+
mkCoach: '直接在页面上画',
|
|
289
|
+
mkCoach1: '上面挑个工具:画笔、箭头、圆圈、文字…',
|
|
290
|
+
mkCoach2: '在页面上按住拖动,画出要改的地方',
|
|
291
|
+
mkCoach3: '画完点「交给模型」,再补一句要怎么改',
|
|
292
|
+
mkCancel: '取消',
|
|
293
|
+
mkSend: '交给模型',
|
|
294
|
+
mkCopy: '复制',
|
|
295
|
+
mkCopyTip: '把标注图复制到剪贴板,贴到别的地方用',
|
|
296
|
+
mkCopied: '标注图复制好了,贴到别处就行',
|
|
297
|
+
mkCopyFail: '没能复制到剪贴板,再点一次「复制」,或者「交给模型」',
|
|
298
|
+
mkSending: '正在拼图…',
|
|
299
|
+
mkTextPh: '写点什么,回车完成',
|
|
300
|
+
mkEmpty: '还没画东西',
|
|
301
|
+
mkFail: '标注图没存成:{msg}',
|
|
302
|
+
mkAsk: '〔标注〕{path}\n{notes}这是我在设计上画了标注的截图({where})。先用 read_image 看这张图,按图上的圈、箭头和文字改对应的地方;没标的地方别动。\n',
|
|
303
|
+
mkNotes: '图上写的字:{notes}\n',
|
|
304
|
+
mkQuote: '「{v}」',
|
|
305
|
+
mkIn: '标注图放进输入框了,补一句要怎么改再发',
|
|
306
|
+
mkMany: '画到的屏太多,只拼了前 {n} 屏,其余留成灰块',
|
|
307
|
+
mkMoved: '设计换了,这次标注没交出去',
|
|
308
|
+
mkNoInput: '没能放进输入框,点一下聊天输入框再点「交给模型」',
|
|
213
309
|
textSaved: '已改好,写回 {file}',
|
|
214
|
-
textAsk: '把页面上的「{before}」改成「{after}
|
|
310
|
+
textAsk: '把页面上的「{before}」改成「{after}」({where};只改这一处文字,别的不动)。\n',
|
|
215
311
|
styleEdit: '调样式:点页面里的元素,拖滑块改字号、颜色、圆角、间距(Esc 退出)',
|
|
216
312
|
styleAny: '点任意一屏里的元素调样式 · Esc 退出',
|
|
217
313
|
styleOne: '点页面里的元素调样式 · Esc 退出',
|
|
@@ -242,7 +338,7 @@ window.__ModuleLoader__.load({
|
|
|
242
338
|
styleSaved: '样式已写回 {file}',
|
|
243
339
|
styleMiss: '源码里没找到这个元素(可能是脚本生成的),改成交给模型',
|
|
244
340
|
styleN: '改了 {n} 项',
|
|
245
|
-
styleAskText: '〔调样式〕{name}({sel}):{list}。{rules}
|
|
341
|
+
styleAskText: '〔调样式〕{name}({sel}):{list}。{rules}把这些写进它的样式规则({where};能用设计变量就用设计变量),别写成行内 style。\n',
|
|
246
342
|
ruleFrom: '- {props}:现在由 {sel} 决定({at})',
|
|
247
343
|
ruleMaybe: '- {props}:大概由 {sel} 决定({at};它在 @layer / @container 里,没细算,改之前看一眼)',
|
|
248
344
|
ruleInline: '- {props}:现在写在元素的行内 style 上{over}',
|
|
@@ -252,10 +348,26 @@ window.__ModuleLoader__.load({
|
|
|
252
348
|
styleJoin: ';',
|
|
253
349
|
lang: 'zh',
|
|
254
350
|
stylesHead: '先挑一个风格开工',
|
|
255
|
-
stylesTip: '
|
|
351
|
+
stylesTip: '点一个看样板,满意了点「用这个风格」,整套配色、字体、圆角放进输入框;再写要做什么',
|
|
256
352
|
stylesOpen: '换个风格…',
|
|
257
353
|
stylesTitle: '换个风格',
|
|
258
|
-
stylesSwap: '
|
|
354
|
+
stylesSwap: '点一个看样板;「用这个风格」让模型把现在的设计换成它(先改 tokens.css,布局和文案不动)',
|
|
355
|
+
stylesAll: '全部风格',
|
|
356
|
+
stylePrev: '上一个(←)',
|
|
357
|
+
styleNext: '下一个(→)',
|
|
358
|
+
styleUse: '用这个风格',
|
|
359
|
+
samBrand: '小森',
|
|
360
|
+
samNav: '发现 课程 我的',
|
|
361
|
+
samHead: '把日子过得好一点',
|
|
362
|
+
samSub: '一句说明文字,看看正文和次要文字的样子。',
|
|
363
|
+
samGo: '开始使用',
|
|
364
|
+
samMore: '了解更多 →',
|
|
365
|
+
samItem: '周末手作课',
|
|
366
|
+
samMeta: '2 小时 · 6 人小班',
|
|
367
|
+
samPrice: '¥128',
|
|
368
|
+
samTag: '新',
|
|
369
|
+
samStat: '本周完成',
|
|
370
|
+
samStatMeta: '比上周多 12%',
|
|
259
371
|
styleText: '〔风格〕{name}——{desc}\n配色:{colors}\n字体:{fonts}\n形状:{shape}\n要点:{notes}\n{ask}',
|
|
260
372
|
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}({accUse}),链接和小字强调 {link}{bgImage}',
|
|
261
373
|
accBig: '图形、大字',
|
|
@@ -321,7 +433,8 @@ window.__ModuleLoader__.load({
|
|
|
321
433
|
twFix: '让模型修好',
|
|
322
434
|
twMakeText: '〔调节滑块〕给这套设计做 3–5 个调节滑块:挑最值得试的维度(比如留白、圆角、主色、卡片排法),照 ui-design 技能「调节滑块」一节写进设计目录的 tweaks.json,把样式接上,做完用 check_design 查一遍。',
|
|
323
435
|
twFixText: '〔调节滑块〕tweaks.json 有问题:{errs}。照 ui-design 技能「调节滑块」一节改好。',
|
|
324
|
-
twAskHead: '
|
|
436
|
+
twAskHead: '〔调节〕我在右侧调节滑块里调成了下面这样:',
|
|
437
|
+
twAskTail: '照这个改设计(改样式源文件,tweaks.json 里对应的 value 也改成新值)。',
|
|
325
438
|
twAskTip: '让模型照这几个值改设计(可以顺手调整相关的地方)',
|
|
326
439
|
twSaveTip: 'CSS 变量写进 tokens.css 的默认值,属性写到每屏的 <html> 上(改前改后各存一版)',
|
|
327
440
|
twNoWrite: '开发服务器的页面不能直接写回,交给模型改',
|
|
@@ -363,6 +476,26 @@ window.__ModuleLoader__.load({
|
|
|
363
476
|
guideDesc: 'Live page, pick what to change',
|
|
364
477
|
empty: 'Nothing to preview yet',
|
|
365
478
|
emptyHint: 'Or enter an HTML path or a port:',
|
|
479
|
+
emptyHintPick: 'Have HTML already? Click "Browse…" or drop it here, or enter a path or a port:',
|
|
480
|
+
browse: 'Browse…',
|
|
481
|
+
browseTip: 'Pick a local HTML file to open',
|
|
482
|
+
pathNone: "Couldn't get this file's path. Type its path to open it.",
|
|
483
|
+
pageDrop: 'Drop to open this page',
|
|
484
|
+
maybeDrop: 'Drop to open this folder or HTML file',
|
|
485
|
+
pageDropSub: 'An HTML file, or a folder with HTML files',
|
|
486
|
+
dropWeb: "The web version can't see local file paths",
|
|
487
|
+
dropWebSub: 'Type its path on the computer running DSH to open it',
|
|
488
|
+
dropWebSay: "The web version can't see local file paths. Type its path on the computer running DSH to open it.",
|
|
489
|
+
dropWhat: 'Only HTML, folders or images',
|
|
490
|
+
dropWhatSub: 'HTML and folders open in the preview; images become references',
|
|
491
|
+
dropWhatWeb: 'Only images here',
|
|
492
|
+
dropWhatWebSub: 'Images become references. To open HTML, type its path',
|
|
493
|
+
pickDir: 'Choose folder',
|
|
494
|
+
pickDirFail: "Couldn't open the folder picker: {msg}",
|
|
495
|
+
hoMoved: 'That folder already has other things in it, so the code goes into {name} inside it (you can change this)',
|
|
496
|
+
hoBeside: "Code can't go inside the design folder (the design stays for comparison), so it goes into {name} next to it (you can change this)",
|
|
497
|
+
hoChecking: 'Checking this folder…',
|
|
498
|
+
hoCheckFail: "Couldn't check this folder ({msg}). Check the path before you start",
|
|
366
499
|
hintOpen: 'Preview {name}',
|
|
367
500
|
headerHint: '{name} was just written. Click to preview it',
|
|
368
501
|
placeholder: '/path/index.html or 5173',
|
|
@@ -371,6 +504,24 @@ window.__ModuleLoader__.load({
|
|
|
371
504
|
pickTip: 'Click an element or drag a region to send it to the model (Esc to cancel)',
|
|
372
505
|
pickAny: 'Click an element in any screen, or drag a region · Esc to cancel',
|
|
373
506
|
pickOne: 'Click an element or drag a region · Esc to cancel',
|
|
507
|
+
pickArea: 'Selected region',
|
|
508
|
+
chipOne: '1 element',
|
|
509
|
+
chipMark: 'Markup',
|
|
510
|
+
chipText: 'Text edit',
|
|
511
|
+
chipStyle: 'Style changes',
|
|
512
|
+
chipVariant: 'Two more takes',
|
|
513
|
+
chipDir: 'Direction picked',
|
|
514
|
+
chipCheck: 'Check · {n} issues',
|
|
515
|
+
chipCode: 'Build as code',
|
|
516
|
+
chipRef: 'Reference image',
|
|
517
|
+
chipTweak: 'Tweaked values',
|
|
518
|
+
chipTwMake: 'Make tweaks',
|
|
519
|
+
chipTwFix: 'Fix tweaks',
|
|
520
|
+
chipSysEx: 'Extract design system',
|
|
521
|
+
chipSysAp: 'Apply design system',
|
|
522
|
+
chipLook: 'Style · {name}',
|
|
523
|
+
chipTokens: 'Create design tokens',
|
|
524
|
+
pickElems: '{n} elements',
|
|
374
525
|
textAny: 'Click text in any screen to edit · Enter to save · Esc to stop',
|
|
375
526
|
refPick: 'Restyle from a reference…',
|
|
376
527
|
refChoose: 'Choose one',
|
|
@@ -404,6 +555,7 @@ window.__ModuleLoader__.load({
|
|
|
404
555
|
kindApp: 'App',
|
|
405
556
|
kindMini: 'Mini program',
|
|
406
557
|
kindTablet: 'Tablet',
|
|
558
|
+
kindDesktop: 'Desktop app',
|
|
407
559
|
kindPoster: 'Poster',
|
|
408
560
|
kindSlides: 'Slides',
|
|
409
561
|
kindEmail: 'Email',
|
|
@@ -438,7 +590,8 @@ window.__ModuleLoader__.load({
|
|
|
438
590
|
checkFail: 'Check failed: {msg}',
|
|
439
591
|
checkNote: 'Only measurable layout problems; whether it looks good is still up to you',
|
|
440
592
|
checkCount: 'Design check: {n} problems last time. Click to see them',
|
|
441
|
-
checkAsk: '[Design check] Found {n} problems
|
|
593
|
+
checkAsk: '[Design check] Found {n} problems:',
|
|
594
|
+
checkTail: 'Please fix each one in the source files, touching only what is listed; for colors and type sizes, change the design tokens where possible.',
|
|
442
595
|
ckToolHead: 'Measured {s} screens and found {n} problems. Fix each one in the source files, touching only what is listed; for colors and type sizes, change the design tokens where possible:',
|
|
443
596
|
ckToolOk: 'Measured {s} screens ({labels}). No problems found.',
|
|
444
597
|
ckToolOff: 'The user is looking at an old version, the demo or the multi-size view, so the sidebar can\'t measure right now. Try again in a moment, or go through the self-check list yourself.',
|
|
@@ -460,6 +613,11 @@ window.__ModuleLoader__.load({
|
|
|
460
613
|
ck_jserr: 'Script error',
|
|
461
614
|
ck_missing: 'File failed to load',
|
|
462
615
|
ck_capsule: 'Under the mini program capsule',
|
|
616
|
+
ck_island: 'Under the Dynamic Island',
|
|
617
|
+
ck_homebar: 'In the home indicator area',
|
|
618
|
+
ck_tabbar: 'Too many tabs',
|
|
619
|
+
ck_navbar: 'No navigation bar',
|
|
620
|
+
ck_winctl: 'Under the window buttons',
|
|
463
621
|
cd_hscroll: 'sticks out {px}px',
|
|
464
622
|
cd_clipped: '{px}px cut off',
|
|
465
623
|
cd_overflow: 'sticks out {px}px',
|
|
@@ -477,6 +635,15 @@ window.__ModuleLoader__.load({
|
|
|
477
635
|
cd_jserr: '{msg}{at}',
|
|
478
636
|
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
479
637
|
cd_capsule: '{px}px under the top-right capsule (WeChat draws it on top; keep the top bar clear from x={x})',
|
|
638
|
+
cd_island: '{px}px under the Dynamic Island (it sits at y 11–45: keep the middle of the status bar empty and start content below the status bar)',
|
|
639
|
+
cd_homebar: 'text or icons in {bar} reach {px}px into the home indicator area (leave 34px: give the bar padding-bottom: 34px; env(safe-area-inset-bottom) is 0 in the preview)',
|
|
640
|
+
cd_tabbar: 'the tab bar has {n} items (5 at most)',
|
|
641
|
+
cd_navbar: 'nothing at y {t}–{b} (the navigation bar): top-level pages show a title or brand, other pages a centered title and a back button',
|
|
642
|
+
cd_winctl: '{px}px under the{osName}window buttons (the frame draws them on top: keep {zone} clear)',
|
|
643
|
+
ckOs_mac: ' Mac ',
|
|
644
|
+
ckOs_win: ' Windows ',
|
|
645
|
+
ckZone_mac: 'the top-left 80×40',
|
|
646
|
+
ckZone_win: 'the top-right 138×32',
|
|
480
647
|
ckEdge_top: 'top bar',
|
|
481
648
|
ckEdge_bottom: 'bottom bar',
|
|
482
649
|
ckWhen_top: 'before scrolling, ',
|
|
@@ -521,7 +688,7 @@ window.__ModuleLoader__.load({
|
|
|
521
688
|
pickShort: 'Use this',
|
|
522
689
|
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',
|
|
523
690
|
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',
|
|
524
|
-
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; 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
|
|
691
|
+
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',
|
|
525
692
|
exThis: 'Export this screen',
|
|
526
693
|
demo: 'Play: tap buttons in the page to move between screens',
|
|
527
694
|
demoOn: 'Playing · tap to navigate',
|
|
@@ -529,8 +696,50 @@ window.__ModuleLoader__.load({
|
|
|
529
696
|
demoRestart: 'Start over',
|
|
530
697
|
demoExit: 'Stop playing (Esc)',
|
|
531
698
|
textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
|
|
699
|
+
mark: 'Mark up: draw circles, arrows and notes on the page, then hand it to the model as one image',
|
|
700
|
+
mkPen: 'Pen',
|
|
701
|
+
mkHl: 'Highlighter',
|
|
702
|
+
mkArrow: 'Arrow',
|
|
703
|
+
mkRect: 'Rectangle',
|
|
704
|
+
mkOval: 'Circle',
|
|
705
|
+
mkText: 'Text',
|
|
706
|
+
mkErase: 'Eraser',
|
|
707
|
+
mkColor: 'Color',
|
|
708
|
+
mkSize: 'Size',
|
|
709
|
+
mkUndo: 'Undo',
|
|
710
|
+
mkRedo: 'Redo',
|
|
711
|
+
mkClear: 'Clear all',
|
|
712
|
+
mkHint: 'Draw on the page, then send it to the model',
|
|
713
|
+
mkHow_pen: 'Press and drag to draw freehand',
|
|
714
|
+
mkHow_hl: 'Drag over what needs changing to highlight it',
|
|
715
|
+
mkHow_arrow: 'Press at the start and drag to what you mean',
|
|
716
|
+
mkHow_rect: 'Drag to box what needs changing',
|
|
717
|
+
mkHow_oval: 'Drag to circle what needs changing',
|
|
718
|
+
mkHow_text: 'Click where the note goes, type, Enter to finish',
|
|
719
|
+
mkHow_erase: 'Click a stroke to erase it, or drag across',
|
|
720
|
+
mkCoach: 'Draw right on the page',
|
|
721
|
+
mkCoach1: 'Pick a tool above: pen, arrow, circle, text…',
|
|
722
|
+
mkCoach2: 'Press and drag on the page to mark what to change',
|
|
723
|
+
mkCoach3: 'Then click "Send to model" and add a line on what to do',
|
|
724
|
+
mkCancel: 'Cancel',
|
|
725
|
+
mkSend: 'Send to model',
|
|
726
|
+
mkCopy: 'Copy',
|
|
727
|
+
mkCopyTip: 'Copy the marked-up image to paste somewhere else',
|
|
728
|
+
mkCopied: 'Markup image copied. Paste it anywhere',
|
|
729
|
+
mkCopyFail: 'Could not copy to the clipboard. Click Copy again, or Send to model',
|
|
730
|
+
mkSending: 'Building the image…',
|
|
731
|
+
mkTextPh: 'Type a note, Enter to finish',
|
|
732
|
+
mkEmpty: 'Nothing drawn yet',
|
|
733
|
+
mkFail: 'Could not save the markup: {msg}',
|
|
734
|
+
mkAsk: '[Markup] {path}\n{notes}A screenshot of the design with my markup on it ({where}). Look at it with read_image first, then change what the circles, arrows and notes point at; leave unmarked parts alone.\n',
|
|
735
|
+
mkNotes: 'Notes written on it: {notes}\n',
|
|
736
|
+
mkQuote: '"{v}"',
|
|
737
|
+
mkIn: 'Markup added to the input. Say what to change, then send',
|
|
738
|
+
mkMany: 'Too many screens marked: captured the first {n}, the rest are gray',
|
|
739
|
+
mkMoved: 'The design changed, so the markup was not sent',
|
|
740
|
+
mkNoInput: 'Could not add it to the input. Click the chat input, then Send to model again',
|
|
532
741
|
textSaved: 'Saved to {file}',
|
|
533
|
-
textAsk: 'Change the text "{before}" to "{after}" on the page (only this text, nothing else).\n
|
|
742
|
+
textAsk: 'Change the text "{before}" to "{after}" on the page ({where}; only this text, nothing else).\n',
|
|
534
743
|
styleEdit: 'Style: click an element, then drag sliders for size, colors, radius and spacing (Esc to stop)',
|
|
535
744
|
styleAny: 'Click an element in any screen to style it · Esc to stop',
|
|
536
745
|
styleOne: 'Click an element in the page to style it · Esc to stop',
|
|
@@ -561,7 +770,7 @@ window.__ModuleLoader__.load({
|
|
|
561
770
|
styleSaved: 'Style saved to {file}',
|
|
562
771
|
styleMiss: 'Could not find this element in the source (maybe a script made it); asking the model instead',
|
|
563
772
|
styleN: '{n} changed',
|
|
564
|
-
styleAskText: '[Style] {name} ({sel}): {list}. {rules}Put it in its stylesheet rule, using design tokens where you can, not an inline style.\n
|
|
773
|
+
styleAskText: '[Style] {name} ({sel}): {list}. {rules}Put it in its stylesheet rule ({where}), using design tokens where you can, not an inline style.\n',
|
|
565
774
|
ruleFrom: '- {props}: currently set by {sel} ({at})',
|
|
566
775
|
ruleMaybe: '- {props}: probably set by {sel} ({at}; it sits in @layer / @container and was not fully resolved, check before editing)',
|
|
567
776
|
ruleInline: '- {props}: currently in the element\'s inline style{over}',
|
|
@@ -571,10 +780,26 @@ window.__ModuleLoader__.load({
|
|
|
571
780
|
styleJoin: '; ',
|
|
572
781
|
lang: 'en',
|
|
573
782
|
stylesHead: 'Start from a style',
|
|
574
|
-
stylesTip: 'Pick one to
|
|
783
|
+
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',
|
|
575
784
|
stylesOpen: 'Try another style…',
|
|
576
785
|
stylesTitle: 'Change the style',
|
|
577
|
-
stylesSwap: 'Pick one
|
|
786
|
+
stylesSwap: 'Pick one to see a sample; "Use this style" has the model restyle the current design (tokens.css first; layout and copy stay)',
|
|
787
|
+
stylesAll: 'All styles',
|
|
788
|
+
stylePrev: 'Previous (←)',
|
|
789
|
+
styleNext: 'Next (→)',
|
|
790
|
+
styleUse: 'Use this style',
|
|
791
|
+
samBrand: 'Bloom',
|
|
792
|
+
samNav: 'Explore Classes Me',
|
|
793
|
+
samHead: 'Make every day a little better',
|
|
794
|
+
samSub: 'One line of supporting text to show body and muted colors.',
|
|
795
|
+
samGo: 'Get started',
|
|
796
|
+
samMore: 'Learn more →',
|
|
797
|
+
samItem: 'Weekend workshop',
|
|
798
|
+
samMeta: '2 hrs · small group of 6',
|
|
799
|
+
samPrice: '$18',
|
|
800
|
+
samTag: 'New',
|
|
801
|
+
samStat: 'Done this week',
|
|
802
|
+
samStatMeta: '12% more than last week',
|
|
578
803
|
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
579
804
|
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} ({accUse}), links and small emphasis {link}{bgImage}',
|
|
580
805
|
accBig: 'graphics and large text only',
|
|
@@ -640,7 +865,8 @@ window.__ModuleLoader__.load({
|
|
|
640
865
|
twFix: 'Ask the model to fix it',
|
|
641
866
|
twMakeText: '[Tweaks] Make 3–5 tweaks for this design: pick the dimensions most worth trying (spacing, radius, brand color, card layout…), write them to tweaks.json in the design folder as the ui-design skill describes under "Tweaks", wire up the styles, then run check_design.',
|
|
642
867
|
twFixText: '[Tweaks] tweaks.json has problems: {errs}. Fix it as the ui-design skill describes under "Tweaks".',
|
|
643
|
-
twAskHead: '[Tweaks] I set the tweaks in the sidebar to these values
|
|
868
|
+
twAskHead: '[Tweaks] I set the tweaks in the sidebar to these values:',
|
|
869
|
+
twAskTail: 'Change the design to match (edit the style sources and update the matching values in tweaks.json).',
|
|
644
870
|
twAskTip: 'Let the model apply these values (and adjust related details)',
|
|
645
871
|
twSaveTip: 'CSS variables go into the defaults in tokens.css, attributes onto each screen\'s <html> (a version is kept before and after)',
|
|
646
872
|
twNoWrite: 'Dev-server pages cannot be written back. Ask the model instead',
|
|
@@ -685,7 +911,8 @@ window.__ModuleLoader__.load({
|
|
|
685
911
|
let s = ''
|
|
686
912
|
try { s = bound ? bound(key) : '' } catch { s = '' }
|
|
687
913
|
if (!s || s === key || s === `${NS}.${key}`) s = ZH[key] || key
|
|
688
|
-
|
|
914
|
+
// 一次替换完:填进去的内容里就算有「{x}」(比如目录名)也不会再被换掉
|
|
915
|
+
if (vars) s = s.replace(/\{(\w+)\}/g, (m, k) => (Object.prototype.hasOwnProperty.call(vars, k) ? String(vars[k]) : m))
|
|
689
916
|
return s
|
|
690
917
|
}
|
|
691
918
|
}
|
|
@@ -693,10 +920,26 @@ window.__ModuleLoader__.load({
|
|
|
693
920
|
// ── 样式(DSH 设计变量,深浅主题跟着走)──────────────────────────────────
|
|
694
921
|
|
|
695
922
|
const ACCENT = '#4d6bfe'
|
|
923
|
+
const PICK_SRC = 'dsh-design-studio' // 输入框里点选标签的来源名(样式里也用)
|
|
924
|
+
// 输入框里的各种标签:图标按来源名分(DSH 的标签上只带来源名),消息里和悬停卡片按 data-i
|
|
925
|
+
const CHIP_ICON = {
|
|
926
|
+
pick: 'M4 4l6.5 16 2.3-6.7L19.5 11z M13 13l6 6',
|
|
927
|
+
mark: 'M4 20l4.5-1L19 8.5a2.1 2.1 0 0 0-3-3L5.5 16z M14 7.5l3 3',
|
|
928
|
+
text: 'M5 7V5h14v2 M12 5v14 M9 19h6',
|
|
929
|
+
style: 'M4 7h9 M17 7h3 M4 17h3 M11 17h9 M13 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0 M7 17a2 2 0 1 0 4 0a2 2 0 1 0-4 0',
|
|
930
|
+
layers: 'M12 3l9 5-9 5-9-5z M3 13l9 5 9-5',
|
|
931
|
+
check: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0 M8 12.5l2.7 2.7L16 10',
|
|
932
|
+
code: 'M8 7l-5 5 5 5 M16 7l5 5-5 5',
|
|
933
|
+
image: 'M4 5h16v14H4z M4 16l5-5 4 4 2-2 5 5 M14.5 9a1 1 0 1 0 2 0a1 1 0 1 0-2 0',
|
|
934
|
+
look: 'M12 3a9 9 0 1 0 0 18c1 0 1.6-.8 1.6-1.6 0-1-1-1.4-1-2.4 0-.9.7-1.6 1.6-1.6H16a5 5 0 0 0 5-5c0-4-4-7.4-9-7.4z M7.5 12h.01 M9.5 7.8h.01 M14.5 7.8h.01',
|
|
935
|
+
}
|
|
936
|
+
const chipSrc = (i) => i === 'pick' ? PICK_SRC : `${PICK_SRC}-${i}`
|
|
937
|
+
const iconUrl = (d) => `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'><path d='${d}'/></svg>`)}")`
|
|
696
938
|
const EASE = 'cubic-bezier(.2,.8,.2,1)'
|
|
697
939
|
const CSS = `
|
|
698
940
|
.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}
|
|
699
941
|
.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}
|
|
942
|
+
.ddp-bar.editing>:not(form){display:none}
|
|
700
943
|
.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}
|
|
701
944
|
.ddp-addr:hover{border-color:var(--dsw-alias-border-l3)}
|
|
702
945
|
.ddp-addr span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -750,6 +993,11 @@ window.__ModuleLoader__.load({
|
|
|
750
993
|
.ddp-chrome span{flex:1;min-width:0;margin:0 60px 0 18px;height:20px;border-radius:6px;background:rgba(0,0,0,.05);color:#6b6b74;font:12px/20px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
751
994
|
.ddp-island{position:absolute;left:50%;width:118px;height:34px;margin-left:-59px;border-radius:20px;background:#000;z-index:4;pointer-events:none}
|
|
752
995
|
.ddp-cap{position:absolute;width:87px;height:32px;z-index:4;pointer-events:none;overflow:visible;fill:#000}
|
|
996
|
+
.ddp-dev.k-desktop{box-shadow:0 0 0 .5px rgba(0,0,0,.3),0 22px 60px rgba(0,0,0,.28)}
|
|
997
|
+
.ddp-dev.fill.k-desktop{box-shadow:none}
|
|
998
|
+
.ddp-win{position:absolute;z-index:4;pointer-events:none;overflow:visible}
|
|
999
|
+
.ddp-win .x{fill:none;stroke:#1f1f1f;stroke-width:1}
|
|
1000
|
+
.ddp-win.w .x{stroke:#fff}
|
|
753
1001
|
.ddp-cap .pl{fill:rgba(255,255,255,.6);stroke:rgba(0,0,0,.08);stroke-width:.5}
|
|
754
1002
|
.ddp-cap .sp{stroke:rgba(0,0,0,.15);stroke-width:.5}
|
|
755
1003
|
.ddp-cap .rg{fill:none;stroke:#000;stroke-width:2}
|
|
@@ -811,6 +1059,42 @@ window.__ModuleLoader__.load({
|
|
|
811
1059
|
@keyframes ddp-txt{from{background-position:100% 0}to{background-position:-160% 0}}
|
|
812
1060
|
@keyframes ddp-blink{50%{opacity:.25}}
|
|
813
1061
|
@keyframes ddp-in{from{opacity:0;translate:0 6px;scale:.98}}
|
|
1062
|
+
.ddp-mark{position:absolute;inset:0;z-index:6;outline:none;box-shadow:inset 0 0 0 2px rgba(77,107,254,.5)}
|
|
1063
|
+
.ddp-mark .cv{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair;touch-action:none}
|
|
1064
|
+
.ddp-mark .cv.tx{cursor:text}
|
|
1065
|
+
.ddp-mark .cv.er{cursor:cell}
|
|
1066
|
+
.ddp-mark textarea{position:absolute;z-index:2;margin:0;padding:0 2px;border:1px dashed rgba(77,107,254,.85);border-radius:3px;background:rgba(255,255,255,.1);font-family:${'"Kaiti SC","STKaiti","KaiTi","楷体",system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif'};font-weight:700;line-height:1.3;resize:none;overflow:hidden;white-space:pre;outline:none;text-shadow:0 0 2px rgba(255,255,255,.9),0 0 3px rgba(255,255,255,.9)}
|
|
1067
|
+
.ddp-mark textarea.lt{text-shadow:0 0 2px rgba(0,0,0,.7),0 0 3px rgba(0,0,0,.7)}
|
|
1068
|
+
.ddp-mark textarea::placeholder{color:inherit;opacity:.55}
|
|
1069
|
+
.ddp-mk-top,.ddp-mk-bot{position:absolute;left:8px;right:8px;z-index:3;display:flex;justify-content:center;pointer-events:none}
|
|
1070
|
+
.ddp-mk-top{top:10px}
|
|
1071
|
+
.ddp-mk-bot{bottom:12px}
|
|
1072
|
+
.ddp-mk-tools{pointer-events:auto;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:2px;max-width:100%;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1);
|
|
1073
|
+
box-shadow:0 8px 24px rgba(0,0,0,.18),0 0 0 .5px var(--dsw-alias-border-l3);animation:ddp-in .16s ${EASE}}
|
|
1074
|
+
.ddp-mk-tools .ddp-btn.sel{background:rgba(77,107,254,.14);color:${ACCENT}}
|
|
1075
|
+
.ddp-mk-color{position:relative;display:inline-flex}
|
|
1076
|
+
.ddp-mk-color>.ddp-btn i{width:15px;height:15px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(0,0,0,.18),0 0 0 1px rgba(255,255,255,.35)}
|
|
1077
|
+
.ddp-mk-pal{position:absolute;left:50%;top:34px;transform:translateX(-50%);z-index:2;display:grid;grid-template-columns:repeat(4,22px);gap:7px;padding:9px;border-radius:11px;background:var(--dsw-alias-bg-layer-1);
|
|
1078
|
+
box-shadow:0 12px 32px rgba(0,0,0,.24),0 0 0 .5px var(--dsw-alias-border-l3)}
|
|
1079
|
+
.ddp-mk-pal button{width:22px;height:22px;padding:0;border:0;border-radius:50%;cursor:pointer;box-shadow:inset 0 0 0 1px rgba(0,0,0,.18)}
|
|
1080
|
+
.ddp-mk-pal button.sel{outline:2px solid ${ACCENT};outline-offset:2px}
|
|
1081
|
+
.ddp-mk-sz{display:inline-flex}
|
|
1082
|
+
.ddp-mk-sz .ddp-btn{min-width:24px;padding:0 4px}
|
|
1083
|
+
.ddp-mk-sz i{border-radius:50%;background:currentColor}
|
|
1084
|
+
.ddp-mk-bar{pointer-events:auto;display:flex;align-items:center;gap:6px;max-width:100%;padding:4px 4px 4px 14px;border-radius:999px;background:rgba(24,24,27,.94);color:#f4f4f5;font-size:12px;
|
|
1085
|
+
box-shadow:0 8px 24px rgba(0,0,0,.25);animation:ddp-in .16s ${EASE}}
|
|
1086
|
+
.ddp-mk-bar span{flex:1;min-width:0;margin-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:rgba(244,244,245,.8)}
|
|
1087
|
+
.ddp-mk-bar span b{margin-right:6px;font-weight:600;color:#fff}
|
|
1088
|
+
.ddp-mk-bar button{flex:none;height:28px;padding:0 13px;border:0;border-radius:999px;background:rgba(255,255,255,.12);color:#f4f4f5;font:inherit;cursor:pointer}
|
|
1089
|
+
.ddp-mk-bar button:hover:not(:disabled){background:rgba(255,255,255,.2)}
|
|
1090
|
+
.ddp-mk-bar button.go{background:${ACCENT};color:#fff;font-weight:600}
|
|
1091
|
+
.ddp-mk-bar button.go:hover:not(:disabled){background:#3d5bf0}
|
|
1092
|
+
.ddp-mk-bar button.go.idle{background:rgba(77,107,254,.45);color:rgba(255,255,255,.75)}
|
|
1093
|
+
.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}
|
|
1094
|
+
.ddp-mk-coach b{display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#fff}
|
|
1095
|
+
.ddp-mk-coach ol{margin:0;padding:0 0 0 1.3em;color:rgba(244,244,245,.86)}
|
|
1096
|
+
.ddp-mk-coach li+li{margin-top:2px}
|
|
1097
|
+
.ddp-mk-bar button:disabled{opacity:.5;cursor:default}
|
|
814
1098
|
.ddp-skel{position:absolute;inset:0;z-index:2;padding:28px 24px;display:flex;flex-direction:column;gap:14px;background:var(--dsw-alias-bg-layer-1);transition:opacity .3s ease;pointer-events:none}
|
|
815
1099
|
.ddp-skel.gone{opacity:0}
|
|
816
1100
|
.ddp-skel b{display:block;border-radius:8px;background:var(--dsw-alias-bg-layer-2);position:relative;overflow:hidden}
|
|
@@ -819,8 +1103,8 @@ window.__ModuleLoader__.load({
|
|
|
819
1103
|
.ddp-wait{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px}
|
|
820
1104
|
.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}
|
|
821
1105
|
.ddp-wait p{margin:0;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.6}
|
|
822
|
-
.ddp-toast{position:absolute;left:
|
|
823
|
-
.ddp-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 24px;text-align:center;color:var(--dsw-alias-label-tertiary)}
|
|
1106
|
+
.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}
|
|
1107
|
+
.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)}
|
|
824
1108
|
.ddp-empty b{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500}
|
|
825
1109
|
.ddp-empty p{margin:6px 0 0;max-width:300px;line-height:1.6}
|
|
826
1110
|
.ddp-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px;text-align:left;counter-reset:s}
|
|
@@ -837,7 +1121,11 @@ window.__ModuleLoader__.load({
|
|
|
837
1121
|
@keyframes ddp-ants{to{stroke-dashoffset:-14}}
|
|
838
1122
|
@keyframes ddp-sel{0%,30%{opacity:0}42%,88%{opacity:1}100%{opacity:0}}
|
|
839
1123
|
@keyframes ddp-cur{0%,100%{transform:translate(132px,98px)}36%,90%{transform:translate(58px,76px)}}
|
|
840
|
-
.ddp-empty form{display:flex;gap:6px;width:100%;max-width:320px;margin-top:4px}
|
|
1124
|
+
.ddp-empty form{display:flex;flex-wrap:wrap;gap:6px;width:100%;max-width:320px;margin-top:4px}
|
|
1125
|
+
.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}
|
|
1126
|
+
.ddp-browse:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
1127
|
+
.ddp-drop .in.no{border-color:var(--dsw-alias-label-tertiary)}
|
|
1128
|
+
.ddp-drop .in.no svg{color:var(--dsw-alias-label-tertiary)}
|
|
841
1129
|
.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}
|
|
842
1130
|
.ddp-err{color:var(--dsw-alias-state-error-primary,#e5484d);font-size:11.5px}
|
|
843
1131
|
.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}
|
|
@@ -878,6 +1166,7 @@ window.__ModuleLoader__.load({
|
|
|
878
1166
|
.ddp{position:relative;outline:none}
|
|
879
1167
|
.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}
|
|
880
1168
|
.ddp-drop *{pointer-events:none}
|
|
1169
|
+
.ddp-drop.soft{pointer-events:none}
|
|
881
1170
|
.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}
|
|
882
1171
|
.ddp-drop .in svg{width:28px;height:28px;color:${ACCENT};margin-bottom:4px}
|
|
883
1172
|
.ddp-drop .in b{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500}
|
|
@@ -920,14 +1209,47 @@ window.__ModuleLoader__.load({
|
|
|
920
1209
|
.ddp-style .cnt{padding:0 14px 6px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
921
1210
|
.ddp-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px 8px}
|
|
922
1211
|
.ddp-sc{display:flex;flex-direction:column;gap:5px;min-width:0;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}
|
|
923
|
-
.ddp-sc .pv{position:relative;display:block;height:
|
|
924
|
-
.ddp-sc:hover .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
925
|
-
.ddp-sc:
|
|
926
|
-
.ddp-sc
|
|
927
|
-
.ddp-sc .pv .
|
|
928
|
-
.ddp-
|
|
929
|
-
.ddp-
|
|
930
|
-
.ddp-
|
|
1212
|
+
.ddp-sc .pv{position:relative;display:block;height:74px;border-radius:10px;overflow:hidden;container-type:inline-size;box-shadow:0 0 0 .5px rgba(127,127,127,.35);transition:transform .15s ${EASE},box-shadow .15s}
|
|
1213
|
+
.ddp-sc:hover .pv,.ddp-sc:focus-visible .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
1214
|
+
.ddp-sc:focus-visible{outline:none}
|
|
1215
|
+
.ddp-sc:hover,.ddp-sc:focus-visible{color:var(--dsw-alias-label-primary)}
|
|
1216
|
+
.ddp-sc .pv .ddp-sam{width:calc(100cqw / .32);min-height:calc(100% / .32);transform:scale(.32);transform-origin:0 0;pointer-events:none}
|
|
1217
|
+
.ddp-sam{display:flex;flex-direction:column;gap:7px;padding:12px 14px 14px;font-size:11px;line-height:1.45;text-align:left;overflow:hidden;box-sizing:border-box}
|
|
1218
|
+
.ddp-sam b{font-weight:600;color:inherit} /* 空白页的 .ddp-empty b 会把字染成界面色 */
|
|
1219
|
+
.ddp-sam .top{display:flex;align-items:center;gap:6px;margin-bottom:3px}
|
|
1220
|
+
.ddp-sam .top b{font-size:12px}
|
|
1221
|
+
.ddp-sam .lg{width:14px;height:14px;flex:none}
|
|
1222
|
+
.ddp-sam .nv{margin-left:auto;font-size:10px;white-space:nowrap}
|
|
1223
|
+
.ddp-sam .av{width:18px;height:18px;flex:none;border-radius:50%}
|
|
1224
|
+
.ddp-sam h5{margin:2px 0 0;font-size:21px;line-height:1.2}
|
|
1225
|
+
.ddp-sam p{margin:0}
|
|
1226
|
+
.ddp-sam .acts{display:flex;align-items:center;gap:12px;margin:2px 0 4px;white-space:nowrap}
|
|
1227
|
+
.ddp-sam .bt{display:inline-flex;align-items:center;height:26px;padding:0 13px;font-weight:600}
|
|
1228
|
+
.ddp-sam .cards{display:grid;grid-template-columns:1.25fr 1fr;gap:9px;align-items:stretch}
|
|
1229
|
+
.ddp-sam .cd{display:flex;flex-direction:column;gap:2px;min-width:0;padding:8px;font-size:10px;box-sizing:border-box}
|
|
1230
|
+
.ddp-sam .cd>*{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
1231
|
+
.ddp-sam .cd>b{font-size:11.5px}
|
|
1232
|
+
.ddp-sam .im{display:block;flex:none;height:46px;margin-bottom:4px}
|
|
1233
|
+
.ddp-sam .pr{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:3px}
|
|
1234
|
+
.ddp-sam .pr b{font-size:15px;font-weight:700}
|
|
1235
|
+
.ddp-sam .chip{padding:1px 7px;border-radius:999px;font-size:9.5px;font-weight:600}
|
|
1236
|
+
.ddp-sam .cd>.num{margin:2px 0 6px;font-size:28px;line-height:1.1}
|
|
1237
|
+
.ddp-sam .bar{position:relative;display:block;flex:none;height:5px;margin-bottom:5px;border-radius:3px}
|
|
1238
|
+
.ddp-sam .bar i{position:absolute;left:0;top:0;bottom:0;width:86%;border-radius:inherit}
|
|
1239
|
+
.ddp-samview{display:flex;flex-direction:column;gap:8px;text-align:left}
|
|
1240
|
+
.ddp-samview .all{align-self:flex-start;height:24px;margin-left:-4px;font-size:12px}
|
|
1241
|
+
.ddp-samview .all svg,.ddp-samview .row svg{width:14px;height:14px}
|
|
1242
|
+
.ddp-samview .ddp-sam{border-radius:12px;box-shadow:0 0 0 .5px rgba(127,127,127,.35),0 8px 24px rgba(0,0,0,.10);animation:ddp-in .2s ${EASE}}
|
|
1243
|
+
.ddp-samview .meta{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px}
|
|
1244
|
+
.ddp-samview .meta b{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
1245
|
+
.ddp-samview .meta p{flex-basis:100%;margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
1246
|
+
.ddp-samview .sw{display:flex;gap:3px;margin-left:auto}
|
|
1247
|
+
.ddp-samview .sw i{width:12px;height:12px;border-radius:50%;box-shadow:0 0 0 .5px rgba(127,127,127,.5)}
|
|
1248
|
+
.ddp-samview .row{display:flex;align-items:center;gap:2px}
|
|
1249
|
+
.ddp-samview .row .ddp-btn{height:26px;min-width:26px}
|
|
1250
|
+
.ddp-samview .row .nx svg{transform:scaleX(-1)}
|
|
1251
|
+
.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}
|
|
1252
|
+
.ddp-samview .row .ddp-go{margin-left:auto;height:28px;font-size:12px}
|
|
931
1253
|
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
932
1254
|
.ddp-tries{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:440px}
|
|
933
1255
|
.ddp-tries>span{flex-basis:100%;font-size:11px;line-height:16px}
|
|
@@ -986,6 +1308,7 @@ window.__ModuleLoader__.load({
|
|
|
986
1308
|
.ddp-banner .ddp-btn{height:24px;min-width:24px;border-radius:999px}
|
|
987
1309
|
.ddp-toast{display:flex;align-items:center;gap:10px}
|
|
988
1310
|
.ddp-toast.low{top:56px}
|
|
1311
|
+
.ddp-toast.mk{top:auto;bottom:62px}
|
|
989
1312
|
.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}
|
|
990
1313
|
.ddp-cmp{position:absolute;inset:0;overflow:hidden}
|
|
991
1314
|
.ddp-cmp>.ly{position:absolute;inset:0}
|
|
@@ -1030,7 +1353,7 @@ window.__ModuleLoader__.load({
|
|
|
1030
1353
|
.ddp-sysc .acts button{flex:1;height:26px;padding:0 6px;border:0;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:12px/26px inherit;cursor:pointer;white-space:nowrap}
|
|
1031
1354
|
.ddp-sysc .acts button:hover{color:var(--dsw-alias-label-primary)}
|
|
1032
1355
|
.ddp-sys form{display:flex;gap:6px}
|
|
1033
|
-
.ddp-sys form input,.ddp-ho input[type=text]{flex:1;min-width:0;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}
|
|
1356
|
+
.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}
|
|
1034
1357
|
.ddp-sys form input:focus,.ddp-ho input[type=text]:focus{border-color:${ACCENT}}
|
|
1035
1358
|
.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}
|
|
1036
1359
|
.ddp-tokens.ddp-sys footer form button:disabled{opacity:.45;cursor:default}
|
|
@@ -1038,6 +1361,15 @@ window.__ModuleLoader__.load({
|
|
|
1038
1361
|
.ddp-ho .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
|
|
1039
1362
|
.ddp-ho .row:first-child{border-top:0}
|
|
1040
1363
|
.ddp-ho label{display:block;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
1364
|
+
.ddp-ho .pathrow{display:flex;gap:6px;align-items:center}
|
|
1365
|
+
.ddp-ho .moved{margin:6px 0 0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
1366
|
+
.ddp-ho .moved.bad{color:var(--dsw-alias-state-error-primary,#d54941)}
|
|
1367
|
+
.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}
|
|
1368
|
+
.ddp-dirbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.5))}
|
|
1369
|
+
.ddp-dirbtn:disabled{opacity:.5;cursor:default}
|
|
1370
|
+
.ddp-dirbtn svg{width:15px;height:15px}
|
|
1371
|
+
.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}
|
|
1372
|
+
.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))}
|
|
1041
1373
|
.ddp-ho input[type=text]{width:100%;box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
|
|
1042
1374
|
.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}
|
|
1043
1375
|
.ddp-ho .opt:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
@@ -1051,13 +1383,42 @@ window.__ModuleLoader__.load({
|
|
|
1051
1383
|
.ddp-menu .o1,.ddp-menu .o2{display:none}
|
|
1052
1384
|
@container (max-width:700px){.ddp-bar>.o1{display:none}.ddp-bar>.ddp-more{display:block}.ddp-menu .o1{display:flex}}
|
|
1053
1385
|
@container (max-width:580px){.ddp-bar>.o2{display:none}.ddp-menu .o2{display:flex}}
|
|
1386
|
+
/* 标注底栏:右侧栏窄时「这支笔怎么用」折成两行,不被三个按钮挤成省略号 */
|
|
1387
|
+
@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}}
|
|
1388
|
+
/* 画标注时预览不动,画布右下角的缩放按钮被标注层盖着也点不到:先收起来,免得和底栏叠在一起 */
|
|
1389
|
+
.ddp-stage:has(>.ddp-mark) .ddp-zoom{display:none}
|
|
1390
|
+
/* 输入框里的标签:DSH 的引用标签换成我们的图标 + 底色;消息里我们自己画同样的样子 */
|
|
1391
|
+
[data-composer-chip^="${PICK_SRC}"]>span,.ddp-pickpill{border-radius:6px;padding:0 6px 0 5px;background:var(--dsw-alias-state-business-tertiary,color-mix(in srgb,${ACCENT} 13%,transparent));color:var(--dsw-alias-state-business-primary,${ACCENT});cursor:default}
|
|
1392
|
+
[data-composer-chip^="${PICK_SRC}"]>span>[aria-hidden]{width:12px;height:12px;align-self:center;font-size:0;background:currentColor}
|
|
1393
|
+
[data-composer-chip^="${PICK_SRC}"]>span>[aria-hidden],.ddp-pickpill::before,.ddp-pickcard b::before{-webkit-mask-position:center;-webkit-mask-size:contain;-webkit-mask-repeat:no-repeat;mask-position:center;mask-size:contain;mask-repeat:no-repeat}
|
|
1394
|
+
${Object.entries(CHIP_ICON).map(([k, d]) => `[data-composer-chip="${chipSrc(k)}"]>span>[aria-hidden],.ddp-pickpill[data-i="${k}"]::before,.ddp-pickcard[data-i="${k}"] b::before{-webkit-mask-image:${iconUrl(d)};mask-image:${iconUrl(d)}}`).join('\n')}
|
|
1395
|
+
.ddp-pickpill{display:inline;margin:0 2px;font-weight:500;white-space:inherit;overflow-wrap:anywhere;-webkit-box-decoration-break:clone;box-decoration-break:clone}
|
|
1396
|
+
.ddp-pickpill::before{content:'';display:inline-block;width:.86em;height:.86em;margin-right:4px;vertical-align:-.1em;background:currentColor}
|
|
1397
|
+
.ddp-pickcard{position:fixed;z-index:2147483000;max-width:min(340px,calc(100vw - 24px));padding:10px 12px;border-radius:12px;pointer-events:none;
|
|
1398
|
+
background:linear-gradient(var(--dsw-specific-menu,transparent),var(--dsw-specific-menu,transparent)),var(--dsw-specific-input-major,var(--dsw-alias-bg-base,#fff));
|
|
1399
|
+
color:var(--dsw-alias-label-primary,#18181b);font-size:13px;line-height:1.5;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1,rgba(0,0,0,.08));
|
|
1400
|
+
box-shadow:var(--dsw-elevation-prominent,0 0 0 1px rgba(0,0,0,.08),0 10px 28px rgba(0,0,0,.18));animation:ddp-in .12s ${EASE}}
|
|
1401
|
+
.ddp-pickcard b{display:flex;align-items:center;gap:6px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1402
|
+
.ddp-pickcard b::before{content:'';flex:none;width:14px;height:14px;background:${ACCENT}}
|
|
1403
|
+
.ddp-pickcard code{display:block;margin-top:3px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary,#52525b);word-break:break-all}
|
|
1404
|
+
.ddp-pickcard span{display:block;margin-top:2px;color:var(--dsw-alias-label-tertiary,#a1a1aa);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1405
|
+
.ddp-pickcard p{margin:3px 0 0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
|
|
1406
|
+
.ddp-pickcard img{display:block;width:100%;max-height:200px;margin:7px 0 2px;object-fit:contain;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}
|
|
1407
|
+
.ddp-pickcard>i{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
|
|
1408
|
+
.ddp-pickcard>i>i{width:16px;height:16px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(127,127,127,.35)}
|
|
1409
|
+
.ddp-pickcard.wide{width:min(320px,calc(100vw - 24px))}
|
|
1054
1410
|
`
|
|
1411
|
+
// 页面不刷新就换了新版插件时,旧版留下的样式要换成新的;卸载时一起拿掉
|
|
1055
1412
|
function ensureStyle() {
|
|
1056
|
-
if (typeof document === 'undefined'
|
|
1057
|
-
|
|
1058
|
-
el
|
|
1059
|
-
|
|
1060
|
-
|
|
1413
|
+
if (typeof document === 'undefined') return () => {}
|
|
1414
|
+
let el = document.getElementById('dsh-design-studio-style')
|
|
1415
|
+
if (!el) {
|
|
1416
|
+
el = document.createElement('style')
|
|
1417
|
+
el.id = 'dsh-design-studio-style'
|
|
1418
|
+
document.head.appendChild(el)
|
|
1419
|
+
}
|
|
1420
|
+
if (el.textContent !== CSS) el.textContent = CSS
|
|
1421
|
+
return () => { if (el.textContent === CSS) el.remove() }
|
|
1061
1422
|
}
|
|
1062
1423
|
|
|
1063
1424
|
// ── 图标 ─────────────────────────────────────────────────────────────────
|
|
@@ -1072,6 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1072
1433
|
app: () => svg([h('rect', { key: 1, x: 7, y: 2.5, width: 10, height: 19, rx: 2.4 }), h('path', { key: 2, d: 'M10.5 5h3' })]),
|
|
1073
1434
|
miniprogram: () => svg([h('rect', { key: 1, x: 6, y: 2.5, width: 12, height: 19, rx: 2.4 }), h('rect', { key: 2, x: 11.5, y: 5.2, width: 4.5, height: 2.4, rx: 1.2 })]),
|
|
1074
1435
|
tablet: () => svg([h('rect', { key: 1, x: 4.5, y: 3, width: 15, height: 18, rx: 2.2 }), h('path', { key: 2, d: 'M11 18h2' })]),
|
|
1436
|
+
desktop: () => svg([h('rect', { key: 1, x: 2.5, y: 4.5, width: 19, height: 15, rx: 2.6 }), h('path', { key: 2, d: 'M9 4.5v15' }), h('path', { key: 3, d: 'M4.8 7.2h.01M6.8 7.2h.01' })]),
|
|
1075
1437
|
poster: () => svg([h('rect', { key: 1, x: 5, y: 3, width: 14, height: 18, rx: 1.8 }), h('path', { key: 2, d: 'M5 16l4-4 3 3 2-2 5 5' }), h('circle', { key: 3, cx: 14.5, cy: 8, r: 1.6 })]),
|
|
1076
1438
|
slides: () => svg([h('rect', { key: 1, x: 2.5, y: 4, width: 19, height: 12, rx: 1.8 }), h('path', { key: 2, d: 'M12 16v3M8.5 20.5h7' }), h('path', { key: 3, d: 'M6.5 8h6M6.5 11h4' })]),
|
|
1077
1439
|
email: () => svg([h('rect', { key: 1, x: 3, y: 5, width: 18, height: 14, rx: 2.2 }), h('path', { key: 2, d: 'M3.5 6.5l8.5 6.5 8.5-6.5' })]),
|
|
@@ -1108,6 +1470,16 @@ window.__ModuleLoader__.load({
|
|
|
1108
1470
|
dial: () => svg([h('circle', { key: 1, cx: 12, cy: 13.5, r: 6.5 }), h('path', { key: 2, d: 'M12 13.5l3.2-3.2' }), h('path', { key: 3, d: 'M12 3.5v2M4.6 7.6l1.4 1.1M19.4 7.6L18 8.7' })]),
|
|
1109
1471
|
code: () => svg([h('path', { key: 1, d: 'M8.5 7.5L4 12l4.5 4.5M15.5 7.5L20 12l-4.5 4.5' }), h('path', { key: 2, d: 'M13.5 5l-3 14' })]),
|
|
1110
1472
|
library: () => svg([h('rect', { key: 1, x: 3.5, y: 4, width: 5, height: 16, rx: 1.2 }), h('rect', { key: 2, x: 10, y: 4, width: 5, height: 16, rx: 1.2 }), h('path', { key: 3, d: 'M16.6 5.4l3.7 13.9' })]),
|
|
1473
|
+
pen: () => svg([h('path', { key: 1, d: 'M4.5 19.5l.9-4.2L15.6 5.1a2 2 0 0 1 2.9 0l.4.4a2 2 0 0 1 0 2.9L8.7 18.6z' }), h('path', { key: 2, d: 'M13.8 6.9l3.3 3.3' })]),
|
|
1474
|
+
marker: () => svg([h('path', { key: 1, d: 'M14.5 4.5l5 5-7.5 7.5H8.5v-3.5z' }), h('path', { key: 2, d: 'M8.5 13.5L4 18h4.5' }), h('path', { key: 3, d: 'M13 20.5h7' })]),
|
|
1475
|
+
arrow: () => svg([h('path', { key: 1, d: 'M5 19L19 5' }), h('path', { key: 2, d: 'M10 5h9v9' })]),
|
|
1476
|
+
square: () => svg([h('rect', { key: 1, x: 4, y: 5, width: 16, height: 14, rx: 1.6 })]),
|
|
1477
|
+
circle: () => svg([h('ellipse', { key: 1, cx: 12, cy: 12, rx: 8.5, ry: 7.5 })]),
|
|
1478
|
+
eraser: () => svg([h('path', { key: 1, d: 'M8.6 19.5L4.4 15.3a1.5 1.5 0 0 1 0-2.1l8.8-8.8a1.5 1.5 0 0 1 2.1 0l4.2 4.2a1.5 1.5 0 0 1 0 2.1L11 19.5z' }), h('path', { key: 2, d: 'M8.8 9.2l6 6' }), h('path', { key: 3, d: 'M11 19.5h9' })]),
|
|
1479
|
+
undo: () => svg([h('path', { key: 1, d: 'M9 14L4.5 9.5 9 5' }), h('path', { key: 2, d: 'M4.5 9.5H15a5 5 0 0 1 0 10h-3' })]),
|
|
1480
|
+
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' })]),
|
|
1481
|
+
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' })]),
|
|
1482
|
+
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' })]),
|
|
1111
1483
|
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),
|
|
1112
1484
|
}
|
|
1113
1485
|
// 引导页上的大图标
|
|
@@ -1177,6 +1549,63 @@ window.__ModuleLoader__.load({
|
|
|
1177
1549
|
return data
|
|
1178
1550
|
}
|
|
1179
1551
|
|
|
1552
|
+
/** 打开一个本地文件 / 文件夹 / 端口(地址栏、「浏览…」、拖进来都走这里),打开后马上拉一次新状态 */
|
|
1553
|
+
async function openTarget(sid, target) {
|
|
1554
|
+
const s = await post('open', { session: sid, target })
|
|
1555
|
+
const p = poller(sid)
|
|
1556
|
+
p.stop()
|
|
1557
|
+
p.start()
|
|
1558
|
+
return s
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// DSH 桌面版给的两座桥:拿选中 / 拖进来的文件的真实路径;弹系统的选文件夹窗口。网页版没有,对应的按钮就不显示
|
|
1562
|
+
const hostPaths = () => { const b = globalThis.__DSH_HOST_PATHS__; return b && typeof b.pathFor === 'function' ? b : null }
|
|
1563
|
+
const dirPicker = () => { const b = globalThis.__DSH_DIRECTORY_PICKER__; return b && typeof b.pick === 'function' ? b : null }
|
|
1564
|
+
const pathOf = (file) => { try { const b = hostPaths(); return (b && file && b.pathFor(file)) || '' } catch { return '' } }
|
|
1565
|
+
const HTML_FILE = /\.html?$/i
|
|
1566
|
+
const isPageFile = (f) => !!f && (HTML_FILE.test(f.name || '') || /^text\/html$/i.test(f.type || ''))
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* 拖进来的是什么:'page' HTML 文件(桌面版能打开)、'maybe' 没类型的(多半是文件夹)、'web' 这两种但网页版拿不到路径、
|
|
1570
|
+
* 'image' 图片(当参考图)、'other' 别的文件。拖着的时候只看得到类型(文件夹、没后缀的文件类型是空的)
|
|
1571
|
+
*/
|
|
1572
|
+
function dragKind(dt) {
|
|
1573
|
+
const all = [...((dt && dt.items) || [])].filter(Boolean)
|
|
1574
|
+
const items = all.filter((x) => x.kind === 'file')
|
|
1575
|
+
if (!items.length) return 'image'
|
|
1576
|
+
// 从网页上拖下来的图:带着网址和 <img> 那段 HTML,文件那项常常没类型——是图,不是文件夹
|
|
1577
|
+
const strs = all.filter((x) => x.kind === 'string').map((x) => String(x.type).toLowerCase())
|
|
1578
|
+
if (strs.includes('text/uri-list') && strs.includes('text/html') && items.every((x) => !x.type || /^image\//i.test(x.type))) return 'image'
|
|
1579
|
+
// 拖着时看不到文件名:标了 HTML 的是页面;没类型的多半是文件夹(也可能是 .ts、Makefile 这种),蒙层话说中性一点
|
|
1580
|
+
if (items.some((x) => /^text\/html$/i.test(x.type))) return hostPaths() ? 'page' : 'web'
|
|
1581
|
+
if (items.some((x) => !x.type)) return hostPaths() ? 'maybe' : 'web'
|
|
1582
|
+
if (items.some((x) => /^image\//i.test(x.type))) return 'image'
|
|
1583
|
+
return 'other'
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** 松手时拖进来的东西 → { page: 要打开的文件 / 文件夹的 File, dir: 是不是文件夹, image: 图片 File } */
|
|
1587
|
+
function droppedOf(dt) {
|
|
1588
|
+
const out = { page: null, dir: false, image: null, any: false }
|
|
1589
|
+
const items = [...((dt && dt.items) || [])].filter((x) => x && x.kind === 'file')
|
|
1590
|
+
const list = items.length
|
|
1591
|
+
? items.map((x) => {
|
|
1592
|
+
let dir = false
|
|
1593
|
+
// 文件夹只有 entry 分得出来,得在 drop 里同步取
|
|
1594
|
+
try { const en = typeof x.webkitGetAsEntry === 'function' ? x.webkitGetAsEntry() : null; dir = !!(en && en.isDirectory) } catch { dir = false }
|
|
1595
|
+
let f = null
|
|
1596
|
+
try { f = x.getAsFile() } catch { f = null }
|
|
1597
|
+
return { f, dir }
|
|
1598
|
+
})
|
|
1599
|
+
: [...((dt && dt.files) || [])].map((f) => ({ f, dir: false }))
|
|
1600
|
+
for (const { f, dir } of list) {
|
|
1601
|
+
if (!f) continue
|
|
1602
|
+
out.any = true
|
|
1603
|
+
if (!out.page && (dir || isPageFile(f))) { out.page = f; out.dir = dir }
|
|
1604
|
+
else if (!out.image && !dir && /^image\//i.test(f.type || '')) out.image = f
|
|
1605
|
+
}
|
|
1606
|
+
return out
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1180
1609
|
// ── 选中内容 → 输入框里的一段字 ──────────────────────────────────────────
|
|
1181
1610
|
|
|
1182
1611
|
/** 页面地址 → 用户认得的位置:本地文件给路径,开发服务器给 localhost 地址 */
|
|
@@ -1267,6 +1696,296 @@ window.__ModuleLoader__.load({
|
|
|
1267
1696
|
return { text: lead + body, back }
|
|
1268
1697
|
}
|
|
1269
1698
|
|
|
1699
|
+
// ── 点选标签:DSH 0.2.1 起输入框里放一个短标签(像 @文件),悬停看详情;发出去模型照样拿到整段 ──────
|
|
1700
|
+
|
|
1701
|
+
const PICK_LINE = /^((文件|页面|位置|组件|HTML|样式|包含|屏宽):|- )/
|
|
1702
|
+
const baseName = (s) => String(s || '').replace(/[?#].*$/, '').split(/[\\/]/).filter(Boolean).pop() || ''
|
|
1703
|
+
// 标签复制 / 发出去都是整段,前后空一行:贴回输入框、和用户的话挨着都不会粘成一块
|
|
1704
|
+
const pickClip = (text) => `\n\n${text}\n\n`
|
|
1705
|
+
|
|
1706
|
+
/** 从插进去的那段字里读出标签要显示的:元素名、字、位置、大小、文件 */
|
|
1707
|
+
function pickInfo(text) {
|
|
1708
|
+
const lines = String(text || '').split('\n').map((l) => l.trim()).filter(Boolean)
|
|
1709
|
+
const head = lines[0] || ''
|
|
1710
|
+
const get = (k) => { const l = lines.find((x) => x.startsWith(k)); return l ? l.slice(k.length).trim() : '' }
|
|
1711
|
+
const file = baseName(get('文件:') || get('页面:'))
|
|
1712
|
+
let m = head.match(/^〔选中元素〕(.*?)(?:「(.*)」)?$/)
|
|
1713
|
+
if (m) return { kind: 'el', name: m[1], text: m[2] || '', sel: get('位置:'), size: (get('样式:').match(/^(\d+×\d+)/) || [])[1] || '', file }
|
|
1714
|
+
m = head.match(/^〔选中区域〕.*?(\d+×\d+)/)
|
|
1715
|
+
if (/^〔选中区域〕/.test(head)) return { kind: 'area', n: lines.filter((l) => l.startsWith('- ')).length, size: m ? m[1] : '', file, items: lines.filter((l) => l.startsWith('- ')).slice(0, 3).map((l) => l.slice(2)) }
|
|
1716
|
+
return null
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/** 标签上的字固定:「1 个元素」,区域写里面有几个元素(是哪个元素悬停看) */
|
|
1720
|
+
function pickLabel(info, t) {
|
|
1721
|
+
if (!info) return ''
|
|
1722
|
+
if (info.kind !== 'area') return t('chipOne')
|
|
1723
|
+
return info.n > 1 ? t('pickElems', { n: info.n }) : info.n === 1 ? t('chipOne') : t('pickArea')
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/** 点选标签的悬停卡片:元素上的字、元素名、大小、选择器、文件 */
|
|
1727
|
+
function pickItems(info, t) {
|
|
1728
|
+
if (info.kind === 'area') return [['b', t('pickArea')], ['code', [info.n ? t('pickElems', { n: info.n }) : '', info.size].filter(Boolean).join(' · ')], ...info.items.map((x) => ['span', x]), ['span', info.file]]
|
|
1729
|
+
return [['b', info.text || info.name], ['code', [info.name, info.size].filter(Boolean).join(' · ')],
|
|
1730
|
+
info.sel && info.sel !== info.name ? ['code', info.sel.length > 90 ? '…' + info.sel.slice(-90) : info.sel] : null, ['span', info.file]]
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// ── 别的整段(标注、调样式、风格、体检、转成代码…)也一样:输入框里一个短标签,发出去模型拿到整段 ──────
|
|
1734
|
+
// 按我们自己的模板认(中英文各一份),{x} 那里是一行里的任意字
|
|
1735
|
+
|
|
1736
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1737
|
+
// 「文件:…」以扩展名结尾、「页面:…」是地址(没有空格和中文):旧消息里紧贴在后面的用户的话不会被算进来
|
|
1738
|
+
const WHERE_RE = '(?:文件:[^\\n]*\\.[A-Za-z0-9]{1,8}|页面:[!-~]*)'
|
|
1739
|
+
/** 模板 → 正则源和各组的名字;sp:哪个 {x} 用什么正则(默认一行里任意字,{where} 是「文件:/页面:」那行) */
|
|
1740
|
+
function tplSrc(tpl, sp = {}) {
|
|
1741
|
+
const keys = []
|
|
1742
|
+
const src = String(tpl).replace(/\n+$/, '').split(/(\{\w+\})/).map((p) => {
|
|
1743
|
+
const m = p.match(/^\{(\w+)\}$/)
|
|
1744
|
+
if (!m) return reEsc(p)
|
|
1745
|
+
keys.push(m[1])
|
|
1746
|
+
return '(' + (sp[m[1]] || (m[1] === 'where' ? WHERE_RE : '[^\\n]*')) + ')'
|
|
1747
|
+
}).join('')
|
|
1748
|
+
return { src, keys }
|
|
1749
|
+
}
|
|
1750
|
+
/** 正则源里的捕获组都改成不捕获(拼进别的模板里,免得组的序号乱) */
|
|
1751
|
+
const noCap = (src) => src.replace(/(?<!\\)\((?!\?)/g, '(?:')
|
|
1752
|
+
const HEX = /#[0-9a-f]{6}\b|#[0-9a-f]{3}\b/gi
|
|
1753
|
+
const hexes = (s) => [...new Set(String(s || '').match(HEX) || [])]
|
|
1754
|
+
const fileOf = (v) => baseName(String(v.where || '').replace(/^(文件|页面):/, ''))
|
|
1755
|
+
|
|
1756
|
+
// 标注图、参考图的小图(悬停卡片上看一眼):只在这次打开 DSH 时记着,最多 24 张
|
|
1757
|
+
const chipThumbs = new Map()
|
|
1758
|
+
function keepThumb(path, src) {
|
|
1759
|
+
try {
|
|
1760
|
+
const w = src.naturalWidth || src.width
|
|
1761
|
+
const ht = src.naturalHeight || src.height
|
|
1762
|
+
const k = Math.min(1, 640 / w, 640 / ht)
|
|
1763
|
+
const c = document.createElement('canvas')
|
|
1764
|
+
c.width = Math.max(1, Math.round(w * k))
|
|
1765
|
+
c.height = Math.max(1, Math.round(ht * k))
|
|
1766
|
+
const g = c.getContext('2d')
|
|
1767
|
+
g.fillStyle = '#fff'
|
|
1768
|
+
g.fillRect(0, 0, c.width, c.height)
|
|
1769
|
+
g.drawImage(src, 0, 0, c.width, c.height)
|
|
1770
|
+
chipThumbs.set(path, { src: c.toDataURL('image/jpeg', 0.85), w: c.width, h: c.height })
|
|
1771
|
+
if (chipThumbs.size > 24) chipThumbs.delete(chipThumbs.keys().next().value)
|
|
1772
|
+
} catch { /* 没小图就只显示文件名 */ }
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
let chipKinds = null
|
|
1776
|
+
/** 每种整段:图标、认它的正则(alts)、标签上的字、悬停卡片上的几行([标签, 内容]) */
|
|
1777
|
+
function kinds() {
|
|
1778
|
+
if (chipKinds) return chipKinds
|
|
1779
|
+
const L = [ZH, EN]
|
|
1780
|
+
const both = (key, sp) => L.map((x) => tplSrc(x[key], sp))
|
|
1781
|
+
const lit = (keys) => '(?:' + keys.flatMap((k) => L.map((x) => reEsc(x[k].replace(/\n+$/, '')))).join('|') + ')'
|
|
1782
|
+
const quote = (t, s) => t('lang') === 'en' ? `"${s}"` : `「${s}」`
|
|
1783
|
+
chipKinds = [
|
|
1784
|
+
{ i: 'mark', label: (v, t) => t('chipMark'),
|
|
1785
|
+
alts: L.map((x) => tplSrc(x.mkAsk, { where: '[^\\n]*', notes: `(?:${noCap(tplSrc(x.mkNotes).src)}\\n)?` })),
|
|
1786
|
+
card: (v, t) => [['b', t('chipMark')], ['img', chipThumbs.get(v.path)], ['p', String(v.notes || '').trim().replace(/^[^::]*[::]\s*/, '')], ['span', v.where], ['span', baseName(v.path)]] },
|
|
1787
|
+
{ i: 'text', label: (v, t) => t('chipText'), alts: both('textAsk'),
|
|
1788
|
+
card: (v, t) => [['b', t('chipText')], ['p', `${quote(t, v.before)} → ${quote(t, v.after)}`], ['span', fileOf(v)]] },
|
|
1789
|
+
{ i: 'style', label: (v, t) => t('chipStyle'), alts: both('styleAskText', { rules: '(?:\\n- [^\\n]*)*\\n|' }),
|
|
1790
|
+
card: (v, t) => [['b', t('chipStyle')], ['code', v.sel && v.sel !== v.name ? `${v.name} · ${v.sel}` : v.name], ['p', v.list], ['span', fileOf(v)]] },
|
|
1791
|
+
{ i: 'layers', label: (v, t) => t('chipVariant'), alts: both('variantText'),
|
|
1792
|
+
card: (v, t) => [['b', t('chipVariant')], ['code', v.name], ['span', fileOf(v)]] },
|
|
1793
|
+
{ i: 'layers', label: (v, t) => t('chipDir'), alts: [...both('pickText'), ...both('pickKeep')],
|
|
1794
|
+
card: (v, t) => [['b', t('chipDir')], ['code', v.name], ['span', v.others], ['span', fileOf(v)]] },
|
|
1795
|
+
{ i: 'check', label: (v, t) => t('chipCheck', { n: v.n }),
|
|
1796
|
+
alts: L.map((x) => { const a = tplSrc(x.checkAsk, { n: '\\d+' }); const line = `(?:${noCap(tplSrc(x.ckScreen).src)}|\\d+\\. [^\\n]* — [^\\n]*)`; return { src: `${a.src}(?:\\n+${line})+\\n+${tplSrc(x.checkTail).src}`, keys: a.keys } }),
|
|
1797
|
+
card: (v, t, s) => { const it = s.split('\n').filter((l) => /^\d+\. /.test(l)); return [['b', t('chipCheck', { n: v.n })], ...it.slice(0, 4).map((l) => ['span', l.replace(/ — .*$/, '')]), it.length > 4 ? ['span', '…'] : null] } },
|
|
1798
|
+
{ i: 'code', label: (v, t) => t('chipCode'), alts: both('hoText', { only: '(?:,screens: [^\\n]*)?' }),
|
|
1799
|
+
card: (v, t) => [['b', t('chipCode')], ['code', v.fw], ['span', v.dest], ['span', v.scope]] },
|
|
1800
|
+
{ i: 'image', label: (v, t) => t('chipRef'), alts: L.map((x) => tplSrc(x.refText, { ask: lit(['refAsk', 'refAskNew']), w: '\\d+', h: '\\d+' })),
|
|
1801
|
+
card: (v, t) => [['b', t('chipRef')], ['img', chipThumbs.get(v.path)], ['sw', hexes(v.colors)], ['span', `${v.w}×${v.h} · ${v.tone}`], ['span', baseName(v.path)]] },
|
|
1802
|
+
{ i: 'style', label: (v, t) => t('chipTweak'), alts: L.map((x) => ({ src: `${tplSrc(x.twAskHead).src}(?:\\n- [^\\n]*)+\\n${tplSrc(x.twAskTail).src}`, keys: [] })),
|
|
1803
|
+
card: (v, t, s) => [['b', t('chipTweak')], ...s.split('\n').filter((l) => l.startsWith('- ')).slice(0, 5).map((l) => ['span', l.slice(2)])] },
|
|
1804
|
+
{ i: 'style', label: (v, t) => t('chipTwMake'), alts: both('twMakeText'), card: (v, t) => [['b', t('chipTwMake')], ['span', 'tweaks.json']] },
|
|
1805
|
+
{ i: 'style', label: (v, t) => t('chipTwFix'), alts: both('twFixText'), card: (v, t) => [['b', t('chipTwFix')], ['p', v.errs]] },
|
|
1806
|
+
{ i: 'look', label: (v, t) => t('chipSysEx'), alts: both('sysExtractText'), card: (v, t) => [['b', t('chipSysEx')], ['code', v.src]] },
|
|
1807
|
+
{ i: 'look', label: (v, t) => t('chipSysAp'), alts: both('sysApplyText'), card: (v, t) => [['b', t('chipSysAp')], ['code', v.name], ['span', v.dir]] },
|
|
1808
|
+
{ i: 'look', label: (v, t) => t('chipLook', { name: v.name }), alts: L.map((x) => tplSrc(x.styleText, { name: '[^\\n]*?', ask: lit(['styleStart', 'styleRestyle']) })),
|
|
1809
|
+
card: (v, t) => [['b', t('chipLook', { name: v.name })], ['p', v.desc], ['sw', hexes(v.colors)], ['span', v.fonts && v.fonts.split(/[;;]/)[0]]] },
|
|
1810
|
+
{ i: 'look', label: (v) => v.name, alts: [{ src: `〔设计变量〕([^\\n]*?): ([^\\n]*)((${WHERE_RE}))`, keys: ['name', 'value', 'where'] }],
|
|
1811
|
+
card: (v) => [['b', v.name], ['code', v.value], ['sw', hexes(v.value)], ['span', fileOf(v)]] },
|
|
1812
|
+
{ i: 'look', label: (v, t) => t('chipTokens'), alts: both('tokensAskText'), card: (v, t) => [['b', t('chipTokens')], ['span', 'tokens.css']] },
|
|
1813
|
+
]
|
|
1814
|
+
// 每种整段都以固定的一句(或固定的括号)收尾,后面还得是空行或结尾:旧版插成字的、用户的话紧跟着的都不认,照原样显示
|
|
1815
|
+
for (const k of chipKinds) for (const a of k.alts) a.re = new RegExp(`${a.src}(?=\\n\\n|$)`, 'y')
|
|
1816
|
+
return chipKinds
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// 我们的整段没有特别长的行;有(贴进来的大段代码之类)就整条消息都不认,免得正则在长行上来回试很久
|
|
1820
|
+
const longLine = (s) => s.split('\n').some((l) => l.length > 4000)
|
|
1821
|
+
|
|
1822
|
+
/** text 的 pos 处是不是我们的一整段:是就给出种类、各个 {x} 和结尾位置 */
|
|
1823
|
+
function chipAt(text, pos) {
|
|
1824
|
+
for (const k of kinds()) {
|
|
1825
|
+
for (const a of k.alts) {
|
|
1826
|
+
a.re.lastIndex = pos
|
|
1827
|
+
const m = a.re.exec(text)
|
|
1828
|
+
if (!m || !m[0]) continue
|
|
1829
|
+
const end = pos + m[0].length
|
|
1830
|
+
const v = {}
|
|
1831
|
+
a.keys.forEach((key, j) => { if (m[j + 1] != null && !(key in v)) v[key] = m[j + 1] })
|
|
1832
|
+
return { k, v, end }
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return null
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/** 一个标签存的整段 → 图标、标签上的字、悬停卡片;不是我们的给 null */
|
|
1839
|
+
function chipOf(text, t) {
|
|
1840
|
+
const s = String(text || '').trim()
|
|
1841
|
+
const p = pickInfo(s)
|
|
1842
|
+
if (p) return { i: 'pick', label: pickLabel(p, t), items: () => pickItems(p, t) }
|
|
1843
|
+
if (longLine(s)) return null
|
|
1844
|
+
const c = chipAt(s, 0)
|
|
1845
|
+
if (!c || c.end !== s.length) return null
|
|
1846
|
+
return { i: c.k.i, label: c.k.label(c.v, t), items: () => c.k.card(c.v, t, s) }
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/** 消息里的点选块(开头一行〔选中元素〕/〔选中区域〕,后面跟着细节行) */
|
|
1850
|
+
function pickRanges(text) {
|
|
1851
|
+
const out = []
|
|
1852
|
+
const re = /(^|\n)〔(?:选中元素|选中区域)〕/g
|
|
1853
|
+
let m
|
|
1854
|
+
while ((m = re.exec(text))) {
|
|
1855
|
+
const start = m.index + m[1].length
|
|
1856
|
+
let end = text.indexOf('\n', start)
|
|
1857
|
+
if (end < 0) end = text.length
|
|
1858
|
+
const n1 = text.indexOf('\n', end + 1)
|
|
1859
|
+
if (end >= text.length || !/^(文件|页面):/.test(text.slice(end + 1, n1 < 0 ? text.length : n1).trim())) { re.lastIndex = start + 1; continue }
|
|
1860
|
+
while (end < text.length) {
|
|
1861
|
+
const nx = text.indexOf('\n', end + 1)
|
|
1862
|
+
const line = text.slice(end + 1, nx < 0 ? text.length : nx).trim()
|
|
1863
|
+
if (!PICK_LINE.test(line)) break
|
|
1864
|
+
end = nx < 0 ? text.length : nx
|
|
1865
|
+
}
|
|
1866
|
+
const block = text.slice(start, end)
|
|
1867
|
+
const ok = block.startsWith('〔选中元素〕') ? /\n位置:/.test(block) && /\nHTML:</.test(block) && /\n样式:/.test(block) : /^〔选中区域〕页面上 \([-\d.]+, [-\d.]+\) 起 [\d.]+×[\d.]+ 的一块\n/.test(block)
|
|
1868
|
+
if (!ok) { re.lastIndex = start + 1; continue }
|
|
1869
|
+
out.push({ start, end })
|
|
1870
|
+
re.lastIndex = Math.max(end, start + 1)
|
|
1871
|
+
}
|
|
1872
|
+
return out
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/** 消息里我们的整段都在哪(点选块 + 别的整段),按位置排好 */
|
|
1876
|
+
let rangeMemo = { text: null, out: [] }
|
|
1877
|
+
function chipRanges(text) {
|
|
1878
|
+
if (rangeMemo.text === text) return rangeMemo.out
|
|
1879
|
+
const out = pickRanges(text)
|
|
1880
|
+
for (let pos = longLine(text) ? text.length : 0; pos < text.length;) {
|
|
1881
|
+
if ((pos === 0 || text[pos - 1] === '\n') && !out.some((r) => pos >= r.start && pos < r.end)) {
|
|
1882
|
+
const c = chipAt(text, pos)
|
|
1883
|
+
if (c) { out.push({ start: pos, end: c.end }); pos = c.end; continue }
|
|
1884
|
+
}
|
|
1885
|
+
const nx = text.indexOf('\n', pos)
|
|
1886
|
+
if (nx < 0) break
|
|
1887
|
+
pos = nx + 1
|
|
1888
|
+
}
|
|
1889
|
+
out.sort((x, y) => x.start - y.start)
|
|
1890
|
+
rangeMemo = { text, out }
|
|
1891
|
+
return out
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** 发出去的消息里:我们的整段显示成标签,前后的换行显示成一个空格(和用户的话排在一行) */
|
|
1895
|
+
const pickRule = {
|
|
1896
|
+
id: 'ddp-pick',
|
|
1897
|
+
order: -20,
|
|
1898
|
+
match(text, from) {
|
|
1899
|
+
for (const r of chipRanges(text)) {
|
|
1900
|
+
const pre = text.slice(0, r.start).match(/\n+$/)
|
|
1901
|
+
const post = text.slice(r.end).match(/^\n+/)
|
|
1902
|
+
const cands = [
|
|
1903
|
+
pre && r.start - pre[0].length > 0 ? { id: 'text', start: r.start - pre[0].length, end: r.start, data: { text: pre[0], display: ' ' } } : null,
|
|
1904
|
+
{ id: 'ddp-pick', start: r.start, end: r.end, data: { text: text.slice(r.start, r.end) } },
|
|
1905
|
+
post && r.end + post[0].length < text.length ? { id: 'text', start: r.end, end: r.end + post[0].length, data: { text: post[0], display: ' ' } } : null,
|
|
1906
|
+
]
|
|
1907
|
+
for (const c of cands) if (c && c.start >= from) return c
|
|
1908
|
+
}
|
|
1909
|
+
return undefined
|
|
1910
|
+
},
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** 悬停卡片:输入框里的标签和消息里的标签共用一个 */
|
|
1914
|
+
function pickCard(t) {
|
|
1915
|
+
if (typeof document === 'undefined') return () => {}
|
|
1916
|
+
let card = null
|
|
1917
|
+
let on = null
|
|
1918
|
+
const hide = () => { on = null; if (card) card.style.display = 'none' }
|
|
1919
|
+
// 输入框里的标签:从编辑器里读出这个标签存的那段字
|
|
1920
|
+
const textOf = (el) => {
|
|
1921
|
+
if (el.dataset && el.dataset.pick) return el.dataset.pick
|
|
1922
|
+
try {
|
|
1923
|
+
const root = el.closest('[data-lexical-editor]')
|
|
1924
|
+
const ed = root && root.__lexicalEditor
|
|
1925
|
+
const k = Object.keys(el).find((x) => x.startsWith('__lexicalKey_'))
|
|
1926
|
+
const node = ed && k ? ed.getEditorState()._nodeMap.get(el[k]) : null
|
|
1927
|
+
return node && typeof node.__ref === 'string' ? node.__ref : ''
|
|
1928
|
+
} catch { return '' }
|
|
1929
|
+
}
|
|
1930
|
+
const show = (el) => {
|
|
1931
|
+
const c = chipOf(textOf(el), t)
|
|
1932
|
+
if (!c) return
|
|
1933
|
+
if (!card) {
|
|
1934
|
+
card = document.createElement('div')
|
|
1935
|
+
card.className = 'ddp-pickcard'
|
|
1936
|
+
document.body.appendChild(card)
|
|
1937
|
+
}
|
|
1938
|
+
on = el
|
|
1939
|
+
for (const x of [el, ...el.querySelectorAll('[title]')]) x.removeAttribute('title') // DSH 自带的悬停提示只有名字,别和卡片叠在一起
|
|
1940
|
+
card.replaceChildren()
|
|
1941
|
+
card.dataset.i = c.i
|
|
1942
|
+
let wide = false
|
|
1943
|
+
for (const it of c.items()) {
|
|
1944
|
+
const [tag, v] = it || []
|
|
1945
|
+
if (!v || (Array.isArray(v) && !v.length)) continue
|
|
1946
|
+
let n
|
|
1947
|
+
if (tag === 'img') {
|
|
1948
|
+
n = document.createElement('img')
|
|
1949
|
+
n.src = v.src
|
|
1950
|
+
n.alt = ''
|
|
1951
|
+
n.style.aspectRatio = `${v.w} / ${v.h}` // 图还没解码时卡片高度也对
|
|
1952
|
+
wide = true
|
|
1953
|
+
} else if (tag === 'sw') {
|
|
1954
|
+
n = document.createElement('i')
|
|
1955
|
+
for (const hex of v.slice(0, 10)) { const d = document.createElement('i'); d.style.background = hex; n.appendChild(d) }
|
|
1956
|
+
} else {
|
|
1957
|
+
n = document.createElement(tag)
|
|
1958
|
+
n.textContent = v
|
|
1959
|
+
}
|
|
1960
|
+
card.appendChild(n)
|
|
1961
|
+
}
|
|
1962
|
+
card.classList.toggle('wide', wide)
|
|
1963
|
+
card.style.display = 'block'
|
|
1964
|
+
const r = el.getBoundingClientRect()
|
|
1965
|
+
const w = card.offsetWidth
|
|
1966
|
+
const ch = card.offsetHeight
|
|
1967
|
+
card.style.left = Math.max(12, Math.min(innerWidth - w - 12, r.left)) + 'px'
|
|
1968
|
+
card.style.top = (r.top - ch - 8 >= 8 ? r.top - ch - 8 : r.bottom + 8) + 'px'
|
|
1969
|
+
}
|
|
1970
|
+
const sel = `[data-composer-chip^="${PICK_SRC}"], .ddp-pickpill`
|
|
1971
|
+
const over = (e) => {
|
|
1972
|
+
const el = e.target && e.target.closest ? e.target.closest(sel) : null
|
|
1973
|
+
if (el === on) return
|
|
1974
|
+
if (el) show(el)
|
|
1975
|
+
else hide()
|
|
1976
|
+
}
|
|
1977
|
+
document.addEventListener('mouseover', over, true)
|
|
1978
|
+
document.addEventListener('scroll', hide, true)
|
|
1979
|
+
return () => {
|
|
1980
|
+
document.removeEventListener('mouseover', over, true)
|
|
1981
|
+
document.removeEventListener('scroll', hide, true)
|
|
1982
|
+
if (card) card.remove()
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// 输入框里插标签:apply 里拿到 DSH 的服务后填上(旧版 DSH 没有就一直是 null,照旧插字)
|
|
1987
|
+
let chips = null
|
|
1988
|
+
|
|
1270
1989
|
// DSH 的输入框(只认一个;分屏开着多个时不碰)
|
|
1271
1990
|
function composerEl() {
|
|
1272
1991
|
try {
|
|
@@ -1324,7 +2043,7 @@ window.__ModuleLoader__.load({
|
|
|
1324
2043
|
|
|
1325
2044
|
/** 点了一个变量 → 输入框里的一段字 */
|
|
1326
2045
|
function formatToken(tok, st) {
|
|
1327
|
-
return `〔设计变量〕${tok.name}: ${tok.value}
|
|
2046
|
+
return `〔设计变量〕${tok.name}: ${String(tok.value).replace(/\s*\n\s*/g, ' ')}(${whereOf(tok.href, st)})\n`
|
|
1328
2047
|
}
|
|
1329
2048
|
|
|
1330
2049
|
// ── 风格卡:开局挑一个风格(或给已有的设计换风格),整套配色、字体、圆角、阴影、要点放进输入框 ──────
|
|
@@ -1334,6 +2053,7 @@ window.__ModuleLoader__.load({
|
|
|
1334
2053
|
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1335
2054
|
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1336
2055
|
const MONO = '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace'
|
|
2056
|
+
const GROTESK = '"Helvetica Neue", Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif'
|
|
1337
2057
|
const STYLES = [
|
|
1338
2058
|
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1339
2059
|
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
@@ -1411,8 +2131,75 @@ window.__ModuleLoader__.load({
|
|
|
1411
2131
|
head: SANS, w: 800, radius: 12, shadow: '0 4px 12px rgba(207,46,14,.12)',
|
|
1412
2132
|
nzh: '价格用大号粗体数字(¥ 小一号),原价灰色划线;「限时」「包邮」这类小标签用色块;优惠券用金黄色、带锯齿边;倒计时醒目;信息多但分区清楚,主按钮通栏',
|
|
1413
2133
|
nen: 'Prices in big bold numerals (smaller currency sign), old price struck through in gray; small colored tags like "Limited" and "Free shipping"; gold coupons with notched edges; a prominent countdown; dense but clearly sectioned, full-width main button' },
|
|
2134
|
+
{ id: 'japandi', zh: '日系原木', en: 'Japandi', dzh: '米色原木、低饱和、小字大留白', den: 'Oat and wood tones, muted, small type and lots of space',
|
|
2135
|
+
c: { bg: '#F2EEE6', surface: '#FAF8F3', text: '#2F2B26', muted: '#6B645A', line: '#DDD5C8', primary: '#4F4234', accent: '#8C9A6E', on: '#FAF8F3', link: '#6B4F33' },
|
|
2136
|
+
head: SANS, w: 500, radius: 6, shadow: '0 2px 8px rgba(79,66,52,.06)',
|
|
2137
|
+
nzh: '像杂货店和家居品牌:字号偏小、字重不重,留白很多;颜色只有米、原木棕、灰绿,饱和度都低;照片柔光、自然材质(木、麻、陶);细线分隔,几乎不用阴影;主按钮深棕实底',
|
|
2138
|
+
nen: 'Like a lifestyle or homeware brand: smallish, light type and lots of space; only oat, wood brown and sage, all low saturation; soft-light photos of natural materials (wood, linen, ceramic); hairline dividers, almost no shadow; solid dark-brown main button' },
|
|
2139
|
+
{ id: 'swiss', zh: '瑞士网格', en: 'Swiss grid', dzh: '白底黑字红色块、严格网格', den: 'White, black type, red blocks, strict grid',
|
|
2140
|
+
c: { bg: '#FFFFFF', surface: '#F1F1F1', text: '#0A0A0A', muted: '#545454', line: '#0A0A0A', primary: '#D9001B', accent: '#0A0A0A', on: '#FFFFFF', link: '#C2001A' },
|
|
2141
|
+
head: GROTESK, w: 800, radius: 0, shadow: 'none',
|
|
2142
|
+
nzh: '12 栏严格网格、全部左对齐(不居中);标题是超大粗无衬线、字距收紧(-0.03em);红色大色块只用一两处;用数字编号(01 02 03)和粗黑线分区;没有圆角、阴影和渐变',
|
|
2143
|
+
nen: 'A strict 12-column grid, everything left-aligned (nothing centered); huge, bold, tightly tracked sans headings (-0.03em); one or two big red blocks; numbered sections (01 02 03) and heavy black rules; no radius, shadow or gradient' },
|
|
2144
|
+
{ id: 'brutal', zh: '粗野主义', en: 'Brutalist', dzh: '亮黄底、黑粗框、硬阴影、等宽字', den: 'Bright yellow, thick black borders, hard shadows, monospace',
|
|
2145
|
+
c: { bg: '#FFEF5C', surface: '#FFFFFF', text: '#000000', muted: '#3B3B3B', line: '#000000', primary: '#000000', accent: '#FF5C39', on: '#FFEF5C', link: '#1F3FD1' },
|
|
2146
|
+
head: MONO, w: 800, radius: 0, shadow: '5px 5px 0 #000000',
|
|
2147
|
+
nzh: '卡片和按钮 3px 黑粗边框 + 大偏移的硬阴影(不模糊);标题等宽字、很大;亮黄、白、橙红高饱和撞色;元素可以故意错位、叠放;不用渐变、圆角和柔和阴影;链接带下划线',
|
|
2148
|
+
nen: '3px black borders and big unblurred offset shadows on cards and buttons; large monospace headings; loud yellow, white and red-orange blocks; deliberate offsets and overlaps are fine; no gradients, radius or soft shadows; underlined links' },
|
|
2149
|
+
{ id: 'cream', zh: '奶油风', en: 'Cream', dzh: '奶白燕麦焦糖、大圆角、柔光', den: 'Milk, oat and caramel, big radii, soft light',
|
|
2150
|
+
c: { bg: '#FAF5EE', surface: '#FFFDF9', text: '#3A2F28', muted: '#76675C', line: '#EEE3D5', primary: '#8C5E3C', accent: '#E7C9A6', on: '#FFFFFF', link: '#8A5A36' },
|
|
2151
|
+
head: ROUND, w: 700, radius: 22, shadow: '0 10px 30px rgba(140,94,60,.10)',
|
|
2152
|
+
nzh: '奶白、燕麦、焦糖三个色;圆角大,多用圆形和胶囊形;阴影又大又淡、带一点暖色;字是深咖啡色不用纯黑;照片和插画柔光、低对比;整体软、暖、安静',
|
|
2153
|
+
nen: 'Milk, oat and caramel; big radii, lots of circles and pills; large, faint, warm-tinted shadows; dark coffee text, never pure black; soft, low-contrast photos and illustrations; soft, warm and calm overall' },
|
|
2154
|
+
{ id: 'sport', zh: '运动能量', en: 'Sport', dzh: '黑底荧光绿、粗斜体、大数字', den: 'Black with volt green, heavy italics, big numbers',
|
|
2155
|
+
c: { bg: '#0E0F11', surface: '#1A1C20', text: '#F4F5F6', muted: '#A1A5AD', line: '#2B2E35', primary: '#C8FF2E', accent: '#FF5A1F', on: '#0E0F11', link: '#C8FF2E' },
|
|
2156
|
+
head: SANS, w: 900, radius: 6, shadow: 'none',
|
|
2157
|
+
nzh: '标题超粗斜体、全大写、字距收紧;荧光绿只给主按钮、进度和关键数字;配速、心率、卡路里这类数字做得很大(等宽数字);可以用斜切的色块(transform: skewX(-8deg));照片高对比、有动感;橙色只做警示和次要强调',
|
|
2158
|
+
nen: 'Heavy italic, all-caps, tightly tracked headings; volt green only on the main button, progress and key numbers; big tabular numbers for pace, heart rate and calories; slanted blocks (transform: skewX(-8deg)) are fine; high-contrast action photos; orange only for alerts and secondary emphasis' },
|
|
1414
2159
|
]
|
|
1415
2160
|
|
|
2161
|
+
/** 样板上各风格的招牌细节(卡片、按钮、标题、配图块),没写的按 STYLES 里的圆角阴影来 */
|
|
2162
|
+
const LOOK = {
|
|
2163
|
+
minimal: { card: { boxShadow: '0 1px 2px rgba(24,24,27,.06), inset 0 0 0 1px #E4E4E7' }, img: 'linear-gradient(135deg, #E9EEF8, #C7D6F5)' },
|
|
2164
|
+
warm: { img: 'linear-gradient(135deg, #E7B08A, #A9542A)' },
|
|
2165
|
+
editorial: { card: { background: 'transparent', boxShadow: 'inset 0 3px 0 #111111', borderRadius: 0 }, head: { letterSpacing: '-0.02em' }, img: '#C8231B' },
|
|
2166
|
+
dark: { head: { letterSpacing: '.04em' }, img: 'linear-gradient(135deg, #2A2620, #C9A86A)' },
|
|
2167
|
+
playful: { card: { border: '2px solid #1D1B3A' }, btn: { border: '2px solid #1D1B3A', boxShadow: '3px 3px 0 #1D1B3A' }, img: '#1F9E92' },
|
|
2168
|
+
fresh: { card: { boxShadow: '0 6px 18px rgba(47,125,79,.10), inset 0 0 0 1px #DCE6DA' }, img: 'linear-gradient(135deg, #CFE6C9, #7DB08B)' },
|
|
2169
|
+
tech: { card: { boxShadow: '0 1px 3px rgba(15,23,42,.08), inset 0 0 0 1px #E2E8F0' }, img: 'linear-gradient(135deg, #DCE6FF, #2457F5)' },
|
|
2170
|
+
glass: { card: { background: 'rgba(255,255,255,.55)', backdropFilter: 'blur(14px)', boxShadow: '0 10px 30px rgba(16,21,28,.10), inset 0 0 0 1px rgba(255,255,255,.85)' }, img: 'linear-gradient(135deg, #FFD9C7, #C9E2FF)' },
|
|
2171
|
+
soft: { btn: { boxShadow: '4px 4px 10px #C8CED6, -4px -4px 10px #FFFFFF' }, img: 'linear-gradient(135deg, #DDE2EA, #F4F6F9)' },
|
|
2172
|
+
guochao: { card: { boxShadow: 'inset 0 0 0 1px #C8952E, 0 6px 18px rgba(140,30,20,.12)' } },
|
|
2173
|
+
ink: { head: { letterSpacing: '.12em' }, img: 'radial-gradient(ellipse at 35% 65%, rgba(30,30,28,.55), transparent 62%), #ECE7DD' },
|
|
2174
|
+
cyber: { card: { boxShadow: 'inset 0 0 0 1px #2E2B52' }, head: { textTransform: 'uppercase', letterSpacing: '.04em' } },
|
|
2175
|
+
cute: { btn: { boxShadow: '0 4px 0 #E0628E' }, img: 'linear-gradient(135deg, #FFD3E2, #FFC93C)' },
|
|
2176
|
+
retro: { card: { border: '2px solid #2E1F14' }, btn: { boxShadow: '0 3px 0 #2E1F14' }, img: 'linear-gradient(180deg, #B5471D 0 33%, #E0A526 33% 66%, #2F5D50 66%)' },
|
|
2177
|
+
sale: { img: 'linear-gradient(135deg, #FF8A3D, #CF2E0E)' },
|
|
2178
|
+
japandi: { card: { boxShadow: 'inset 0 0 0 1px #DDD5C8' }, img: 'linear-gradient(160deg, #DCD1BF, #B6A58C)' },
|
|
2179
|
+
swiss: { card: { boxShadow: 'none' }, head: { letterSpacing: '-0.03em' }, img: '#D9001B' },
|
|
2180
|
+
brutal: { card: { border: '3px solid #000000' }, btn: { border: '3px solid #000000', boxShadow: '3px 3px 0 #000000' }, img: '#FF5C39' },
|
|
2181
|
+
cream: { img: 'linear-gradient(135deg, #F3E3CF, #E7C9A6)' },
|
|
2182
|
+
sport: { head: { fontStyle: 'italic', textTransform: 'uppercase', letterSpacing: '-0.01em' }, btn: { transform: 'skewX(-8deg)' }, img: 'linear-gradient(115deg, #C8FF2E 0 42%, #2B2E35 42%)' },
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/** 一个风格在样板上的样子:卡片、主按钮、标题字、配图块、小标签 */
|
|
2186
|
+
function styleLook(x) {
|
|
2187
|
+
const c = x.c
|
|
2188
|
+
const o = LOOK[x.id] || {}
|
|
2189
|
+
// 小标签是 9.5px 的小字,要 4.5:1:点缀色在卡片和底色上都够就描边写点缀色,色块上正文色够就做色块,都不行用主色
|
|
2190
|
+
const chip = Math.min(hexRatio(c.accent, c.surface), hexRatio(c.accent, c.bg)) >= 4.5 ? { color: c.accent, boxShadow: `inset 0 0 0 1px ${c.accent}` }
|
|
2191
|
+
: hexRatio(c.text, c.accent) >= 4.5 ? { background: c.accent, color: c.text } : { background: c.primary, color: c.on }
|
|
2192
|
+
return {
|
|
2193
|
+
card: { background: c.surface, borderRadius: x.radius, boxShadow: x.shadow === 'none' ? `inset 0 0 0 1px ${c.line}` : x.shadow, ...o.card },
|
|
2194
|
+
btn: { background: c.primary, color: c.on, borderRadius: Math.min(x.radius, 14), ...o.btn },
|
|
2195
|
+
head: { fontFamily: x.head, fontWeight: x.w, ...o.head },
|
|
2196
|
+
img: o.img || `linear-gradient(135deg, ${c.primary}, ${c.accent})`,
|
|
2197
|
+
// 进度条底:分隔线是黑的(杂志、瑞士、粗野)就换成淡一点的正文色,不然和黑色主色分不开
|
|
2198
|
+
track: c.line === c.text || c.line === c.primary ? `color-mix(in srgb, ${c.text} 14%, ${c.surface})` : c.line,
|
|
2199
|
+
chip,
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1416
2203
|
/** 两个 #RRGGBB 的对比度(WCAG) */
|
|
1417
2204
|
function hexRatio(a, b) {
|
|
1418
2205
|
const l = (hex) => { const v = hex.slice(1).match(/../g).map((x) => parseInt(x, 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2] }
|
|
@@ -1538,6 +2325,8 @@ window.__ModuleLoader__.load({
|
|
|
1538
2325
|
otherText: String(it.otherText || '').slice(0, 20),
|
|
1539
2326
|
edgeName: edge ? t('ckEdge_' + edge) : '',
|
|
1540
2327
|
when: edge ? t('ckWhen_' + edge) : '',
|
|
2328
|
+
osName: it.type === 'winctl' ? t('ckOs_' + (it.os === 'win' ? 'win' : 'mac')) : '',
|
|
2329
|
+
zone: it.type === 'winctl' ? t('ckZone_' + (it.os === 'win' ? 'win' : 'mac')) : '',
|
|
1541
2330
|
what: it.type === 'missing' ? t('ckWhat_' + (['css', 'script', 'font'].includes(it.what) ? it.what : 'other')) : '',
|
|
1542
2331
|
at: it.type === 'jserr' && it.src ? ` (${it.src}${it.line ? ':' + it.line : ''})` : '',
|
|
1543
2332
|
})
|
|
@@ -1568,6 +2357,7 @@ window.__ModuleLoader__.load({
|
|
|
1568
2357
|
detail: checkDetail(t, it),
|
|
1569
2358
|
})))
|
|
1570
2359
|
}
|
|
2360
|
+
if (head === 'checkAsk') lines.push('', t('checkTail'))
|
|
1571
2361
|
return lines.join('\n') + '\n'
|
|
1572
2362
|
}
|
|
1573
2363
|
|
|
@@ -1597,9 +2387,9 @@ window.__ModuleLoader__.load({
|
|
|
1597
2387
|
|
|
1598
2388
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
1599
2389
|
|
|
1600
|
-
const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
2390
|
+
const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, desktop: 2, web: 2, poster: 3, slides: 2, email: 3 }
|
|
1601
2391
|
const CANVAS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1602
|
-
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, web: 1 }
|
|
2392
|
+
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, desktop: 1, web: 1 }
|
|
1603
2393
|
|
|
1604
2394
|
function loadImg(blob) {
|
|
1605
2395
|
return new Promise((resolve, reject) => {
|
|
@@ -1624,7 +2414,7 @@ window.__ModuleLoader__.load({
|
|
|
1624
2414
|
}
|
|
1625
2415
|
|
|
1626
2416
|
/** 在 (x, y) 画一个带外框的屏:w × ht 是屏幕本身;img 按宽铺进去,高出来的裁掉 */
|
|
1627
|
-
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite) {
|
|
2417
|
+
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite, os) {
|
|
1628
2418
|
const f = frameOf(kind, w, ht)
|
|
1629
2419
|
const pad = f.pad || 0
|
|
1630
2420
|
const top = f.top || 0
|
|
@@ -1684,17 +2474,58 @@ window.__ModuleLoader__.load({
|
|
|
1684
2474
|
}
|
|
1685
2475
|
// 胶囊颜色用页面截图那一刻自己量的(和画布上一致);老页面没报就看截图
|
|
1686
2476
|
if (f.capsule) drawCapsule(ctx, x + pad + w - CAP.right - CAP.w, y + pad + CAP.top, typeof capWhite === 'boolean' ? capWhite : !!img && capDarkOf(img, w))
|
|
2477
|
+
if (f.win) {
|
|
2478
|
+
rrect(ctx, x + 0.25, y + 0.25, w - 0.5, ht - 0.5, f.r)
|
|
2479
|
+
ctx.strokeStyle = 'rgba(0,0,0,.28)'
|
|
2480
|
+
ctx.lineWidth = 0.5
|
|
2481
|
+
ctx.stroke()
|
|
2482
|
+
drawWinCtl(ctx, x, y, w, os, typeof capWhite === 'boolean' ? capWhite : os === 'win' && !!img && areaDarkOf(img, w, w - WINBTN.w * 3, 0, WINBTN.w * 3, WINBTN.h))
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** 窗口按钮画到导出的图上(和画布上 WinCtl 一样) */
|
|
2487
|
+
function drawWinCtl(ctx, x, y, w, os, white) {
|
|
2488
|
+
ctx.save()
|
|
2489
|
+
if (os === 'win') {
|
|
2490
|
+
const W = WINBTN.w, H = WINBTN.h, x0 = x + w - W * 3
|
|
2491
|
+
ctx.strokeStyle = white ? '#fff' : '#1f1f1f'
|
|
2492
|
+
ctx.lineWidth = 1
|
|
2493
|
+
ctx.beginPath()
|
|
2494
|
+
ctx.moveTo(x0 + W / 2 - 5, y + H / 2 + 0.5); ctx.lineTo(x0 + W / 2 + 5, y + H / 2 + 0.5)
|
|
2495
|
+
ctx.moveTo(x0 + W * 2.5 - 5, y + H / 2 - 5); ctx.lineTo(x0 + W * 2.5 + 5, y + H / 2 + 5)
|
|
2496
|
+
ctx.moveTo(x0 + W * 2.5 + 5, y + H / 2 - 5); ctx.lineTo(x0 + W * 2.5 - 5, y + H / 2 + 5)
|
|
2497
|
+
ctx.stroke()
|
|
2498
|
+
rrect(ctx, x0 + W * 1.5 - 4.5, y + H / 2 - 4.5, 9, 9, 1)
|
|
2499
|
+
ctx.stroke()
|
|
2500
|
+
} else {
|
|
2501
|
+
const { x: lx, y: ly, d, gap } = LIGHTS
|
|
2502
|
+
;['#FF5F57', '#FEBC2E', '#28C840'].forEach((c, i) => {
|
|
2503
|
+
ctx.beginPath()
|
|
2504
|
+
ctx.arc(x + lx + d / 2 + i * (d + gap), y + ly + d / 2, d / 2 - 0.25, 0, Math.PI * 2)
|
|
2505
|
+
ctx.fillStyle = c
|
|
2506
|
+
ctx.fill()
|
|
2507
|
+
ctx.strokeStyle = 'rgba(0,0,0,.12)'
|
|
2508
|
+
ctx.lineWidth = 0.5
|
|
2509
|
+
ctx.stroke()
|
|
2510
|
+
})
|
|
2511
|
+
}
|
|
2512
|
+
ctx.restore()
|
|
1687
2513
|
}
|
|
1688
2514
|
|
|
1689
2515
|
/** 截图里胶囊那块是不是深色(导出时没有页面可问,看图) */
|
|
1690
2516
|
function capDarkOf(img, w) {
|
|
2517
|
+
return areaDarkOf(img, w, w - CAP.right - CAP.w, CAP.top, CAP.w, CAP.h)
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/** 截图里 (sx, sy, sw, sh) 那块(屏幕坐标)是不是深色 */
|
|
2521
|
+
function areaDarkOf(img, w, sx, sy, sw, sh) {
|
|
1691
2522
|
try {
|
|
1692
2523
|
const k = img.width / w
|
|
1693
2524
|
const c = document.createElement('canvas')
|
|
1694
|
-
c.width = Math.max(1, Math.round(
|
|
1695
|
-
c.height = Math.max(1, Math.round(
|
|
2525
|
+
c.width = Math.max(1, Math.round(sw * k))
|
|
2526
|
+
c.height = Math.max(1, Math.round(sh * k))
|
|
1696
2527
|
const g = c.getContext('2d')
|
|
1697
|
-
g.drawImage(img,
|
|
2528
|
+
g.drawImage(img, sx * k, sy * k, sw * k, sh * k, 0, 0, c.width, c.height)
|
|
1698
2529
|
const d = g.getImageData(0, 0, c.width, c.height).data
|
|
1699
2530
|
let sum = 0
|
|
1700
2531
|
for (let i = 0; i < d.length; i += 4) sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]
|
|
@@ -1730,7 +2561,7 @@ window.__ModuleLoader__.load({
|
|
|
1730
2561
|
}
|
|
1731
2562
|
|
|
1732
2563
|
/** 一屏带外框(四周留出阴影的地方,背景透明) */
|
|
1733
|
-
async function composeOne(kind, size, img, label, top) {
|
|
2564
|
+
async function composeOne(kind, size, img, label, top, os) {
|
|
1734
2565
|
const [w, ht] = size
|
|
1735
2566
|
const [W, H] = frameSize(kind, w, ht)
|
|
1736
2567
|
const m = 56
|
|
@@ -1740,7 +2571,7 @@ window.__ModuleLoader__.load({
|
|
|
1740
2571
|
c.height = Math.round((H + m * 2) * S)
|
|
1741
2572
|
const ctx = c.getContext('2d')
|
|
1742
2573
|
ctx.scale(S, S)
|
|
1743
|
-
drawDevice(ctx, kind, img, m, m, w, ht, label, top)
|
|
2574
|
+
drawDevice(ctx, kind, img, m, m, w, ht, label, top, os)
|
|
1744
2575
|
return canvasBlob(c)
|
|
1745
2576
|
}
|
|
1746
2577
|
|
|
@@ -1874,7 +2705,7 @@ window.__ModuleLoader__.load({
|
|
|
1874
2705
|
big.getContext('2d').drawImage(img, 0, 0, big.width, big.height)
|
|
1875
2706
|
blob = await canvasBlob(big)
|
|
1876
2707
|
}
|
|
1877
|
-
return { w, h: ht, palette, data: await blobB64(blob) }
|
|
2708
|
+
return { w, h: ht, palette, img, data: await blobB64(blob) }
|
|
1878
2709
|
}
|
|
1879
2710
|
|
|
1880
2711
|
function lumaOf(color) {
|
|
@@ -1894,7 +2725,7 @@ window.__ModuleLoader__.load({
|
|
|
1894
2725
|
* 整套展示图:几屏带外框排成网格,上面是品牌名和一行说明,铺一层底色 + 点缀色的光晕。
|
|
1895
2726
|
* shots: [{ img, label }]
|
|
1896
2727
|
*/
|
|
1897
|
-
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark }) {
|
|
2728
|
+
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark, os }) {
|
|
1898
2729
|
const [w, ht] = size
|
|
1899
2730
|
const [DW, DH] = frameSize(kind, w, ht)
|
|
1900
2731
|
const u = Math.max(1, Math.min(2, DW / 430))
|
|
@@ -1955,7 +2786,7 @@ window.__ModuleLoader__.load({
|
|
|
1955
2786
|
const off = ((cols - inRow) * (DW + gap)) / 2 // 最后一行不满就居中
|
|
1956
2787
|
const x = pad + off + col * (DW + gap)
|
|
1957
2788
|
const yy = y + row * (DH + labH + gap * 0.6)
|
|
1958
|
-
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top)
|
|
2789
|
+
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top, os)
|
|
1959
2790
|
ctx.fillStyle = muted
|
|
1960
2791
|
ctx.font = `500 ${16 * u}px ${CANVAS_FONT}`
|
|
1961
2792
|
ctx.textAlign = 'center'
|
|
@@ -2008,6 +2839,11 @@ window.__ModuleLoader__.load({
|
|
|
2008
2839
|
})
|
|
2009
2840
|
}
|
|
2010
2841
|
|
|
2842
|
+
/** 这里能不能往剪贴板写图(http 远程打开、老浏览器不行;不行就不给「复制」按钮) */
|
|
2843
|
+
function canCopyImage() {
|
|
2844
|
+
try { return !!(window.isSecureContext && navigator.clipboard && typeof navigator.clipboard.write === 'function' && typeof ClipboardItem === 'function') } catch { return false }
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2011
2847
|
/** 图片放进剪贴板(DSH 窗口没焦点之类的拿不到就算了) */
|
|
2012
2848
|
async function copyImage(blob) {
|
|
2013
2849
|
try {
|
|
@@ -2032,17 +2868,18 @@ window.__ModuleLoader__.load({
|
|
|
2032
2868
|
|
|
2033
2869
|
// ── 设计类型:画幅和外框 ─────────────────────────────────────────────────
|
|
2034
2870
|
|
|
2035
|
-
const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
|
|
2036
|
-
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
2871
|
+
const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'desktop', 'poster', 'slides', 'email']
|
|
2872
|
+
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', desktop: 'kindDesktop', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
2037
2873
|
const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
|
|
2038
2874
|
const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
|
|
2039
|
-
const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
2040
|
-
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule
|
|
2875
|
+
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] }
|
|
2876
|
+
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule 小程序右上角的胶囊、win 桌面软件的窗口按钮
|
|
2041
2877
|
const FRAME = {
|
|
2042
2878
|
web: { pad: 0, top: 34, r: 10, ir: 0 },
|
|
2043
2879
|
app: { pad: 11, top: 0, r: 55, ir: 44, island: 11 },
|
|
2044
2880
|
miniprogram: { pad: 11, top: 0, r: 55, ir: 44, island: 11, capsule: 1 },
|
|
2045
2881
|
tablet: { pad: 14, top: 0, r: 34, ir: 21 },
|
|
2882
|
+
desktop: { pad: 0, top: 0, r: 12, ir: 12, win: 1 },
|
|
2046
2883
|
poster: { pad: 0, top: 0, r: 6, ir: 6 },
|
|
2047
2884
|
slides: { pad: 0, top: 0, r: 8, ir: 8 },
|
|
2048
2885
|
email: { pad: 0, top: 0, r: 10, ir: 10 },
|
|
@@ -2055,15 +2892,19 @@ window.__ModuleLoader__.load({
|
|
|
2055
2892
|
}
|
|
2056
2893
|
// 小程序胶囊:离屏幕顶 48(状态栏 44 + 4)、离右边 7、87×32(和页面体检 picker.js 的 CAP 一样)
|
|
2057
2894
|
const CAP = { top: 48, right: 7, w: 87, h: 32 }
|
|
2058
|
-
|
|
2895
|
+
// 桌面软件的窗口按钮(和 picker.js 的一样):Mac 红绿灯在左上 x 20、y 20 起,三个 12px 圆点隔 8;Windows 最小化 / 最大化 / 关闭在右上角,各 46×32
|
|
2896
|
+
const LIGHTS = { x: 20, y: 20, d: 12, gap: 8 }
|
|
2897
|
+
const WINBTN = { w: 46, h: 32 }
|
|
2898
|
+
const META_ALIAS = { phone: 'app', mobile: 'app', ios: 'app', android: 'app', mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram', pad: 'tablet', ipad: 'tablet', pc: 'desktop', electron: 'desktop', tauri: 'desktop', mac: 'desktop', macos: 'desktop', windows: 'desktop', win: 'desktop', 桌面: 'desktop', 桌面软件: 'desktop', social: 'poster', image: 'poster', slide: 'slides', deck: 'slides', ppt: 'slides', mail: 'email' }
|
|
2059
2899
|
|
|
2060
|
-
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" → { kind, size } */
|
|
2900
|
+
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" / "desktop windows" → { kind, size, os }(os:win,不写是 Mac) */
|
|
2061
2901
|
function parseMeta(content) {
|
|
2062
|
-
const out = { kind: '', size: '' }
|
|
2902
|
+
const out = { kind: '', size: '', os: '' }
|
|
2063
2903
|
for (const w of String(content || '').toLowerCase().split(/[\s,;]+/)) {
|
|
2064
2904
|
if (!out.kind && (KIND_LIST.includes(w) || META_ALIAS[w])) out.kind = KIND_LIST.includes(w) ? w : META_ALIAS[w]
|
|
2065
2905
|
const m = /^(\d{2,5})[x×*](\d{2,5})$/.exec(w)
|
|
2066
2906
|
if (!out.size && m) out.size = `${m[1]}x${m[2]}`
|
|
2907
|
+
if (!out.os && /^(windows|win|win10|win11)$/.test(w)) out.os = 'win'
|
|
2067
2908
|
}
|
|
2068
2909
|
return out
|
|
2069
2910
|
}
|
|
@@ -2133,6 +2974,7 @@ window.__ModuleLoader__.load({
|
|
|
2133
2974
|
app: [['szSmall', 'app', [375, 667]], ['szStd', 'app', [393, 852]], ['szLarge', 'app', [430, 932]]],
|
|
2134
2975
|
miniprogram: [['szSmall', 'miniprogram', [375, 667]], ['szStd', 'miniprogram', [375, 812]], ['szLarge', 'miniprogram', [414, 896]]],
|
|
2135
2976
|
tablet: [['szSmall', 'tablet', [744, 1133]], ['szStd', 'tablet', [820, 1180]], ['szLarge', 'tablet', [1032, 1376]]],
|
|
2977
|
+
desktop: [['szSmall', 'desktop', [1024, 680]], ['szStd', 'desktop', [1280, 800]], ['szLarge', 'desktop', [1440, 900]]],
|
|
2136
2978
|
}
|
|
2137
2979
|
|
|
2138
2980
|
/** 大小不一的几块排成一行(顶端对齐),算出整块大小和放得下的缩放 */
|
|
@@ -2473,34 +3315,46 @@ window.__ModuleLoader__.load({
|
|
|
2473
3315
|
h('b', { style: { height: 14, width: '54%' } }))
|
|
2474
3316
|
}
|
|
2475
3317
|
|
|
2476
|
-
function AddressForm({ t, sid, initial, onDone, autoFocus, className }) {
|
|
3318
|
+
function AddressForm({ t, sid, initial, onDone, autoFocus, className, onBrowse }) {
|
|
2477
3319
|
const [value, setValue] = useState(initial || '')
|
|
2478
3320
|
const [err, setErr] = useState('')
|
|
2479
3321
|
const submit = async (e) => {
|
|
2480
3322
|
if (e) e.preventDefault()
|
|
2481
3323
|
if (!value.trim()) return onDone && onDone()
|
|
2482
3324
|
try {
|
|
2483
|
-
await
|
|
2484
|
-
const p = poller(sid)
|
|
2485
|
-
p.stop()
|
|
2486
|
-
p.start()
|
|
3325
|
+
await openTarget(sid, value.trim())
|
|
2487
3326
|
setErr('')
|
|
2488
3327
|
onDone && onDone()
|
|
2489
3328
|
} catch (x) {
|
|
2490
3329
|
setErr(t('openFailed', { msg: x.message }))
|
|
2491
3330
|
}
|
|
2492
3331
|
}
|
|
2493
|
-
|
|
3332
|
+
// 工具栏里:焦点离开整个地址栏(输入框和「浏览…」都算在内)才收起;弹出选文件窗口时整个窗口失焦,不收
|
|
3333
|
+
const blur = (e) => {
|
|
3334
|
+
if (!onDone || className) return
|
|
3335
|
+
const form = e.currentTarget
|
|
3336
|
+
if (form.contains(e.relatedTarget)) return
|
|
3337
|
+
setTimeout(() => { if (document.hasFocus() && !form.contains(document.activeElement)) onDone() }, 150)
|
|
3338
|
+
}
|
|
3339
|
+
const esc = (e) => { if (e.key === 'Escape' && !(e.nativeEvent && e.nativeEvent.isComposing) && e.keyCode !== 229 && onDone) onDone() }
|
|
3340
|
+
return h('form', { onSubmit: submit, onBlur: blur, onKeyDown: esc, className, style: className ? undefined : { display: 'contents' } },
|
|
2494
3341
|
h('input', {
|
|
2495
3342
|
className: 'ddp-input',
|
|
2496
3343
|
value,
|
|
2497
3344
|
autoFocus,
|
|
2498
3345
|
spellCheck: false,
|
|
2499
3346
|
placeholder: t('placeholder'),
|
|
2500
|
-
onChange: (e) => setValue(e.target.value),
|
|
2501
|
-
onKeyDown: (e) => { if (e.key === 'Escape') onDone && onDone() },
|
|
2502
|
-
onBlur: () => { if (!err && onDone && !className) setTimeout(onDone, 150) },
|
|
3347
|
+
onChange: (e) => { setValue(e.target.value); if (err) setErr('') },
|
|
2503
3348
|
}),
|
|
3349
|
+
// 「浏览…」:桌面版弹系统的选文件窗口;按下时不抢输入框的焦点(工具栏里失焦会收起地址栏)
|
|
3350
|
+
onBrowse ? h('button', {
|
|
3351
|
+
type: 'button',
|
|
3352
|
+
className: className ? 'ddp-browse' : 'ddp-btn',
|
|
3353
|
+
title: t('browseTip'),
|
|
3354
|
+
'aria-label': className ? undefined : t('browseTip'),
|
|
3355
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
3356
|
+
onClick: onBrowse,
|
|
3357
|
+
}, className ? t('browse') : h(I.folder)) : null,
|
|
2504
3358
|
className ? h('button', { className: 'ddp-go', type: 'submit' }, t('go')) : null,
|
|
2505
3359
|
err ? h('div', { className: 'ddp-err', style: className ? { width: '100%' } : { position: 'absolute', top: 40, left: 12 } }, err) : null)
|
|
2506
3360
|
}
|
|
@@ -2526,7 +3380,7 @@ window.__ModuleLoader__.load({
|
|
|
2526
3380
|
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
|
|
2527
3381
|
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
|
|
2528
3382
|
*/
|
|
2529
|
-
function Device({ kind, w, h: ht, fill, label, top, children }) {
|
|
3383
|
+
function Device({ kind, w, h: ht, fill, label, top, os, children }) {
|
|
2530
3384
|
const f = fill ? { pad: 0, top: 0, r: 0, ir: 0 } : frameOf(kind, w, ht)
|
|
2531
3385
|
return h('div', {
|
|
2532
3386
|
className: `ddp-dev k-${kind}` + (fill ? ' fill' : ''),
|
|
@@ -2535,7 +3389,22 @@ window.__ModuleLoader__.load({
|
|
|
2535
3389
|
h('div', { className: 'ddp-chrome', style: { display: f.top ? 'flex' : 'none', height: f.top } }, h('i'), h('i'), h('i'), h('span', null, label || '')),
|
|
2536
3390
|
h('div', { className: 'ddp-vp', style: fill ? { inset: 0 } : { left: f.pad, top: f.pad + f.top, width: w, height: ht, borderRadius: f.ir } }, children),
|
|
2537
3391
|
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } }),
|
|
2538
|
-
f.capsule ? h(Capsule, { white: !!top, style: { top: (f.pad || 0) + CAP.top, right: (f.pad || 0) + CAP.right } }) : null
|
|
3392
|
+
f.capsule ? h(Capsule, { white: !!top, style: { top: (f.pad || 0) + CAP.top, right: (f.pad || 0) + CAP.right } }) : null,
|
|
3393
|
+
f.win ? h(WinCtl, { os, white: !!top }) : null)
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
/** 桌面软件的窗口按钮:Mac 左上角红绿灯,Windows 右上角最小化 / 最大化 / 关闭(顶栏深色时画白的) */
|
|
3397
|
+
function WinCtl({ os, white }) {
|
|
3398
|
+
if (os === 'win') {
|
|
3399
|
+
const W = WINBTN.w, H = WINBTN.h
|
|
3400
|
+
return h('svg', { className: 'ddp-win' + (white ? ' w' : ''), style: { top: 0, right: 0, width: W * 3, height: H }, viewBox: `0 0 ${W * 3} ${H}`, 'aria-hidden': true },
|
|
3401
|
+
h('path', { className: 'x', d: `M${W / 2 - 5} ${H / 2 + 0.5}h10` }),
|
|
3402
|
+
h('rect', { className: 'x', x: W * 1.5 - 4.5, y: H / 2 - 4.5, width: 9, height: 9, rx: 1 }),
|
|
3403
|
+
h('path', { className: 'x', d: `M${W * 2.5 - 5} ${H / 2 - 5}l10 10M${W * 2.5 + 5} ${H / 2 - 5}l-10 10` }))
|
|
3404
|
+
}
|
|
3405
|
+
const { x, y, d, gap } = LIGHTS
|
|
3406
|
+
return h('svg', { className: 'ddp-win', style: { left: x, top: y, width: d * 3 + gap * 2, height: d }, viewBox: `0 0 ${d * 3 + gap * 2} ${d}`, 'aria-hidden': true },
|
|
3407
|
+
['#FF5F57', '#FEBC2E', '#28C840'].map((c, i) => h('circle', { key: i, cx: d / 2 + i * (d + gap), cy: d / 2, r: d / 2 - 0.25, fill: c, stroke: 'rgba(0,0,0,.12)', strokeWidth: 0.5 })))
|
|
2539
3408
|
}
|
|
2540
3409
|
|
|
2541
3410
|
function Capsule({ white, style }) {
|
|
@@ -2609,6 +3478,9 @@ window.__ModuleLoader__.load({
|
|
|
2609
3478
|
}),
|
|
2610
3479
|
checkFocus: (i) => { if (curRef.current) postTo(curRef.current, { type: 'checkFocus', i }) },
|
|
2611
3480
|
checkClear: () => { if (curRef.current) postTo(curRef.current, { type: 'checkClear' }) },
|
|
3481
|
+
unpin: () => { if (curRef.current) postTo(curRef.current, { type: 'unpin' }) }, // 去掉「已选」虚线框
|
|
3482
|
+
// 画笔标注:这一屏的页面(iframe)现在摆在哪,拼图时按它的位置放截图
|
|
3483
|
+
frame: () => wins.current.get(curRef.current) || null,
|
|
2612
3484
|
}
|
|
2613
3485
|
}
|
|
2614
3486
|
// 登记到右侧栏:导出图片时按屏找到它
|
|
@@ -2760,6 +3632,11 @@ window.__ModuleLoader__.load({
|
|
|
2760
3632
|
case 'links':
|
|
2761
3633
|
if (p.onLinks && Array.isArray(d.links)) p.onLinks(d.links)
|
|
2762
3634
|
break
|
|
3635
|
+
case 'dragIn':
|
|
3636
|
+
case 'dragOut':
|
|
3637
|
+
case 'dropIn':
|
|
3638
|
+
if (p.onPageDrag) p.onPageDrag(d)
|
|
3639
|
+
break
|
|
2763
3640
|
case 'top':
|
|
2764
3641
|
// 每帧记着自己的;只有正显示的那帧算数(预载的换上来时再用,旧帧晚到的不管)
|
|
2765
3642
|
L.tops.set(id, !!d.dark)
|
|
@@ -2922,7 +3799,7 @@ window.__ModuleLoader__.load({
|
|
|
2922
3799
|
}
|
|
2923
3800
|
|
|
2924
3801
|
/** 类型菜单:网页 / App / 平板 / 海报 / 幻灯片 / 邮件 */
|
|
2925
|
-
function KindMenu({ t, kind, open, setOpen, onPick }) {
|
|
3802
|
+
function KindMenu({ t, kind, open, setOpen, onPick, disabled }) {
|
|
2926
3803
|
const ref = useRef(null)
|
|
2927
3804
|
useEffect(() => {
|
|
2928
3805
|
if (!open) return undefined
|
|
@@ -2936,7 +3813,7 @@ window.__ModuleLoader__.load({
|
|
|
2936
3813
|
}
|
|
2937
3814
|
}, [open])
|
|
2938
3815
|
return h('div', { className: 'ddp-kind', ref },
|
|
2939
|
-
h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('kind'), onClick: () => setOpen(!open) },
|
|
3816
|
+
h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('kind'), disabled: !!disabled, onClick: () => setOpen(!open) },
|
|
2940
3817
|
h(I[kind]), h('span', null, t(KIND_KEY[kind])), h(I.caret)),
|
|
2941
3818
|
open ? h('div', { className: 'ddp-menu', role: 'menu' },
|
|
2942
3819
|
h('div', { className: 'hd' }, t('kind')),
|
|
@@ -2944,6 +3821,540 @@ window.__ModuleLoader__.load({
|
|
|
2944
3821
|
h(I[k]), h('span', null, t(KIND_KEY[k])), h('em', null, KIND_SIZE[k].join('×'))))) : null)
|
|
2945
3822
|
}
|
|
2946
3823
|
|
|
3824
|
+
// ── 画笔标注:一层透明画布盖在预览上,画的东西按台面上的坐标(CSS 像素)一笔一笔记着,能撤销、能擦 ──
|
|
3825
|
+
// 一笔:pen / hl { p: [[x, y]…] } · arrow / rect / oval { a: [x, y], b: [x, y] } · text { x, y, f, v };都带颜色 c,线宽 w
|
|
3826
|
+
const MARK_TOOLS = [['pen', 'pen', 'mkPen', 'p'], ['hl', 'marker', 'mkHl', 'h'], ['arrow', 'arrow', 'mkArrow', 'a'], ['rect', 'square', 'mkRect', 'r'], ['oval', 'circle', 'mkOval', 'o'], ['text', 'text', 'mkText', 't'], ['erase', 'eraser', 'mkErase', 'e']]
|
|
3827
|
+
const MARK_COLORS = ['#ef4444', '#f97316', '#facc15', '#22c55e', '#3b82f6', '#a855f7', '#111827', '#ffffff']
|
|
3828
|
+
const MARK_WIDTH = [2, 3, 5]
|
|
3829
|
+
const MARK_FONT = [16, 20, 28]
|
|
3830
|
+
// 手绘风:字用楷体(像手写),没有就退回系统字
|
|
3831
|
+
const MARK_FAMILY = '"Kaiti SC","STKaiti","KaiTi","楷体",system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif'
|
|
3832
|
+
const MARK_MAX = 300 // 最多这么多笔(再多就是在乱画了)
|
|
3833
|
+
const MOD = /Mac|iPhone|iPad/i.test((typeof navigator !== 'undefined' && navigator.platform) || '') ? '⌘' : 'Ctrl+'
|
|
3834
|
+
|
|
3835
|
+
let markMeasure = null
|
|
3836
|
+
/** 一行字画出来多宽 */
|
|
3837
|
+
function markTextWidth(text, f) {
|
|
3838
|
+
if (!markMeasure) markMeasure = document.createElement('canvas').getContext('2d')
|
|
3839
|
+
markMeasure.font = `700 ${f}px ${MARK_FAMILY}`
|
|
3840
|
+
return markMeasure.measureText(text).width
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
/** 浅色(白、黄):字的描边换成深色 */
|
|
3844
|
+
function markLight(hex) {
|
|
3845
|
+
const n = parseInt(String(hex).slice(1), 16)
|
|
3846
|
+
return ((n >> 16 & 255) * 0.299 + (n >> 8 & 255) * 0.587 + (n & 255) * 0.114) > 170
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
/** 一笔占的地方(含线宽):拼图时裁剪、橡皮擦字都按它 */
|
|
3850
|
+
function markBox(s) {
|
|
3851
|
+
if (s.t === 'text') {
|
|
3852
|
+
const lines = s.v.split('\n')
|
|
3853
|
+
const w = Math.max(...lines.map((l) => markTextWidth(l, s.f)))
|
|
3854
|
+
return { l: s.x, t: s.y, r: s.x + w, b: s.y + lines.length * s.f * 1.3 }
|
|
3855
|
+
}
|
|
3856
|
+
const pts = s.p || [s.a, s.b]
|
|
3857
|
+
const pad = s.w / 2 + (s.t === 'arrow' ? Math.max(14, s.w * 4) : s.p ? 0 : 4 + Math.max(Math.abs(s.b[0] - s.a[0]), Math.abs(s.b[1] - s.a[1])) * 0.06)
|
|
3858
|
+
const xs = pts.map((q) => q[0])
|
|
3859
|
+
const ys = pts.map((q) => q[1])
|
|
3860
|
+
return { l: Math.min(...xs) - pad, t: Math.min(...ys) - pad, r: Math.max(...xs) + pad, b: Math.max(...ys) + pad }
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
/** 这一笔的随机数(-0.5…0.5):按笔上记的种子 sd 来,重画、拼图时一模一样 */
|
|
3864
|
+
function markRand(seed) {
|
|
3865
|
+
let v = (seed | 0) % 2147483647
|
|
3866
|
+
if (v <= 0) v += 2147483646
|
|
3867
|
+
return () => (v = (v * 16807) % 2147483647) / 2147483647 - 0.5
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
/** 手绘的一条线:两头和中间都抖一点,弯一点(P:CSS 坐标 → 画布坐标;bend:这条线弯多少,描第二遍时一样) */
|
|
3871
|
+
function roughLine(ctx, P, a, b, rnd, amp, bend) {
|
|
3872
|
+
const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1
|
|
3873
|
+
const nx = -(b[1] - a[1]) / len
|
|
3874
|
+
const ny = (b[0] - a[0]) / len
|
|
3875
|
+
const j = Math.min(len * 0.015, 2.2) * amp
|
|
3876
|
+
const bow = Math.min(len * 0.05, 9) * amp
|
|
3877
|
+
const pt = (t, off) => [a[0] + (b[0] - a[0]) * t + nx * off + rnd() * j, a[1] + (b[1] - a[1]) * t + ny * off + rnd() * j]
|
|
3878
|
+
const k1 = (bend ? bend[0] : rnd()) * bow * 2
|
|
3879
|
+
const k2 = (bend ? bend[1] : rnd()) * bow * 2
|
|
3880
|
+
const p0 = pt(0, 0)
|
|
3881
|
+
const c1 = pt(0.4, k1)
|
|
3882
|
+
const c2 = pt(0.75, k2)
|
|
3883
|
+
const p1 = pt(1, 0)
|
|
3884
|
+
ctx.moveTo(...P(p0))
|
|
3885
|
+
ctx.bezierCurveTo(...P(c1), ...P(c2), ...P(p1))
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
/** 手绘的圈:从一个随便的角度起笔,绕一圈多一点,半径微微起伏;点和点之间连成顺滑的曲线 */
|
|
3889
|
+
function roughLoop(ctx, P, cx, cy, rx, ry, rnd, amp) {
|
|
3890
|
+
const start = -Math.PI / 2 - 0.6 + rnd() * 1.2
|
|
3891
|
+
const over = 0.45 + rnd() * 0.35
|
|
3892
|
+
const n = 44
|
|
3893
|
+
const wob = [rnd(), rnd()]
|
|
3894
|
+
const pts = []
|
|
3895
|
+
for (let i = 0; i <= n; i++) {
|
|
3896
|
+
const t = i / n
|
|
3897
|
+
const ang = start + t * (Math.PI * 2 + over)
|
|
3898
|
+
const m = 1 - 0.04 * amp + (wob[0] * Math.sin(t * 6.3) + wob[1] * Math.sin(t * 12.6 + 1) * 0.6) * 0.1 * amp + rnd() * 0.02 * amp + t * 0.09 * amp
|
|
3899
|
+
pts.push(P([cx + Math.cos(ang) * rx * m, cy + Math.sin(ang) * ry * m]))
|
|
3900
|
+
}
|
|
3901
|
+
ctx.moveTo(...pts[0])
|
|
3902
|
+
for (let i = 1; i < pts.length - 1; i++) ctx.quadraticCurveTo(...pts[i], (pts[i][0] + pts[i + 1][0]) / 2, (pts[i][1] + pts[i + 1][1]) / 2)
|
|
3903
|
+
ctx.lineTo(...pts[pts.length - 1])
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
/** 一笔画到画布上(k:CSS 像素 → 画布像素;ox / oy:拼图时裁掉的左上角) */
|
|
3907
|
+
function drawMark(ctx, s, k = 1, ox = 0, oy = 0) {
|
|
3908
|
+
const X = (v) => (v - ox) * k
|
|
3909
|
+
const Y = (v) => (v - oy) * k
|
|
3910
|
+
ctx.save()
|
|
3911
|
+
ctx.lineCap = 'round'
|
|
3912
|
+
ctx.lineJoin = 'round'
|
|
3913
|
+
ctx.strokeStyle = s.c
|
|
3914
|
+
ctx.fillStyle = s.c
|
|
3915
|
+
ctx.lineWidth = s.w * k
|
|
3916
|
+
if (s.t === 'pen' || s.t === 'hl') {
|
|
3917
|
+
// 荧光笔:半透明,压在字上还看得见字
|
|
3918
|
+
if (s.t === 'hl') ctx.globalAlpha = 0.38
|
|
3919
|
+
const p = s.p
|
|
3920
|
+
ctx.beginPath()
|
|
3921
|
+
ctx.moveTo(X(p[0][0]), Y(p[0][1]))
|
|
3922
|
+
if (p.length === 1) ctx.lineTo(X(p[0][0]) + 0.01, Y(p[0][1]))
|
|
3923
|
+
// 相邻两点的中点连二次曲线:手画的线顺一点
|
|
3924
|
+
for (let i = 1; i < p.length - 1; i++) ctx.quadraticCurveTo(X(p[i][0]), Y(p[i][1]), X((p[i][0] + p[i + 1][0]) / 2), Y((p[i][1] + p[i + 1][1]) / 2))
|
|
3925
|
+
if (p.length > 1) ctx.lineTo(X(p[p.length - 1][0]), Y(p[p.length - 1][1]))
|
|
3926
|
+
ctx.stroke()
|
|
3927
|
+
} else if (s.t === 'arrow' || s.t === 'rect' || s.t === 'oval') {
|
|
3928
|
+
const P = (q) => [X(q[0]), Y(q[1])]
|
|
3929
|
+
const [x0, y0] = s.a
|
|
3930
|
+
const [x1, y1] = s.b
|
|
3931
|
+
const br = markRand(s.sd || 1)
|
|
3932
|
+
const bends = Array.from({ length: 4 }, () => [br(), br()])
|
|
3933
|
+
for (const pass of [0, 1]) {
|
|
3934
|
+
const rnd = markRand((s.sd || 1) + pass * 7919)
|
|
3935
|
+
ctx.beginPath()
|
|
3936
|
+
if (s.t === 'arrow') {
|
|
3937
|
+
// 杆子微微弯,箭头是两笔撇出去的(不填实心)
|
|
3938
|
+
const ang = Math.atan2(y1 - y0, x1 - x0)
|
|
3939
|
+
const head = Math.min(Math.max(16, s.w * 5), Math.hypot(x1 - x0, y1 - y0) * 0.6)
|
|
3940
|
+
roughLine(ctx, P, s.a, s.b, rnd, 1, bends[0])
|
|
3941
|
+
const sp = 0.5 + bends[3][0] * 0.12
|
|
3942
|
+
roughLine(ctx, P, s.b, [x1 - head * Math.cos(ang - sp), y1 - head * Math.sin(ang - sp)], rnd, 0.6, bends[1])
|
|
3943
|
+
roughLine(ctx, P, s.b, [x1 - head * Math.cos(ang + sp), y1 - head * Math.sin(ang + sp)], rnd, 0.6, bends[2])
|
|
3944
|
+
} else if (s.t === 'rect') {
|
|
3945
|
+
const l = Math.min(x0, x1)
|
|
3946
|
+
const r = Math.max(x0, x1)
|
|
3947
|
+
const tp = Math.min(y0, y1)
|
|
3948
|
+
const bt = Math.max(y0, y1)
|
|
3949
|
+
// 四条边各画一笔,拐角处稍微出头
|
|
3950
|
+
const o = Math.min(6, Math.min(r - l, bt - tp) * 0.06)
|
|
3951
|
+
roughLine(ctx, P, [l - o, tp], [r + o * 0.4, tp], rnd, 1, bends[0])
|
|
3952
|
+
roughLine(ctx, P, [r, tp - o * 0.4], [r, bt + o], rnd, 1, bends[1])
|
|
3953
|
+
roughLine(ctx, P, [r + o, bt], [l - o * 0.4, bt], rnd, 1, bends[2])
|
|
3954
|
+
roughLine(ctx, P, [l, bt + o * 0.4], [l, tp - o], rnd, 1, bends[3])
|
|
3955
|
+
} else {
|
|
3956
|
+
roughLoop(ctx, P, (x0 + x1) / 2, (y0 + y1) / 2, Math.abs(x1 - x0) / 2, Math.abs(y1 - y0) / 2, rnd, 1)
|
|
3957
|
+
}
|
|
3958
|
+
ctx.lineWidth = s.w * (pass ? 0.5 : 0.85) * k
|
|
3959
|
+
ctx.globalAlpha = pass ? 0.75 : 1
|
|
3960
|
+
ctx.stroke()
|
|
3961
|
+
}
|
|
3962
|
+
} else if (s.t === 'text') {
|
|
3963
|
+
// 手写的字:楷体,下面一道手画的线
|
|
3964
|
+
ctx.font = `700 ${s.f * k}px ${MARK_FAMILY}`
|
|
3965
|
+
ctx.textBaseline = 'middle'
|
|
3966
|
+
const lh = s.f * 1.3
|
|
3967
|
+
const rnd = markRand(s.sd || 1)
|
|
3968
|
+
const P = (q) => [X(q[0]), Y(q[1])]
|
|
3969
|
+
s.v.split('\n').forEach((line, i) => {
|
|
3970
|
+
const yy = s.y + lh * i + lh / 2
|
|
3971
|
+
// 描一圈反色的边:白、黄这类浅色描深色,深色描浅色(红字写在红按钮上也看得见)
|
|
3972
|
+
ctx.save()
|
|
3973
|
+
ctx.strokeStyle = markLight(s.c) ? 'rgba(0,0,0,.65)' : 'rgba(255,255,255,.85)'
|
|
3974
|
+
ctx.lineWidth = Math.max(2, s.f / 7) * k
|
|
3975
|
+
ctx.strokeText(line, X(s.x), Y(yy))
|
|
3976
|
+
ctx.restore()
|
|
3977
|
+
ctx.fillText(line, X(s.x), Y(yy))
|
|
3978
|
+
const w = markTextWidth(line, s.f)
|
|
3979
|
+
if (!w) return
|
|
3980
|
+
ctx.beginPath()
|
|
3981
|
+
roughLine(ctx, P, [s.x - 2, yy + s.f * 0.62], [s.x + w + 3, yy + s.f * 0.56], rnd, 0.8)
|
|
3982
|
+
ctx.lineWidth = Math.max(1.2, s.f / 12) * k
|
|
3983
|
+
ctx.stroke()
|
|
3984
|
+
})
|
|
3985
|
+
}
|
|
3986
|
+
ctx.restore()
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
/** 点到线段的距离 */
|
|
3990
|
+
function segDist(px, py, a, b) {
|
|
3991
|
+
const dx = b[0] - a[0]
|
|
3992
|
+
const dy = b[1] - a[1]
|
|
3993
|
+
const len = dx * dx + dy * dy
|
|
3994
|
+
const u = len ? Math.max(0, Math.min(1, ((px - a[0]) * dx + (py - a[1]) * dy) / len)) : 0
|
|
3995
|
+
return Math.hypot(px - a[0] - u * dx, py - a[1] - u * dy)
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
/** 手绘的圈、框、箭头实际画在哪:照 drawMark 同样画一遍,路径记成小段折线(橡皮按它判断,抖出去的线也擦得到) */
|
|
3999
|
+
function markPath(s) {
|
|
4000
|
+
const segs = []
|
|
4001
|
+
let cur = null
|
|
4002
|
+
const add = (q) => { if (cur) segs.push([cur, q]); cur = q }
|
|
4003
|
+
const ctx = {
|
|
4004
|
+
save() {}, restore() {}, stroke() {}, fill() {}, fillText() {}, strokeText() {},
|
|
4005
|
+
beginPath() { cur = null },
|
|
4006
|
+
moveTo(x, y) { cur = [x, y] },
|
|
4007
|
+
lineTo(x, y) { add([x, y]) },
|
|
4008
|
+
quadraticCurveTo(cx, cy, x, y) {
|
|
4009
|
+
const p0 = cur || [x, y]
|
|
4010
|
+
for (let i = 1; i <= 6; i++) { const t = i / 6; const u = 1 - t; add([u * u * p0[0] + 2 * u * t * cx + t * t * x, u * u * p0[1] + 2 * u * t * cy + t * t * y]) }
|
|
4011
|
+
},
|
|
4012
|
+
bezierCurveTo(ax, ay, bx, by, x, y) {
|
|
4013
|
+
const p0 = cur || [x, y]
|
|
4014
|
+
for (let i = 1; i <= 12; i++) { const t = i / 12; const u = 1 - t; add([u * u * u * p0[0] + 3 * u * u * t * ax + 3 * u * t * t * bx + t * t * t * x, u * u * u * p0[1] + 3 * u * u * t * ay + 3 * u * t * t * by + t * t * t * y]) }
|
|
4015
|
+
},
|
|
4016
|
+
}
|
|
4017
|
+
drawMark(ctx, s)
|
|
4018
|
+
return segs
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
/** 橡皮擦到了这一笔没有(r:橡皮半径) */
|
|
4022
|
+
function markHit(s, x, y, r) {
|
|
4023
|
+
if (s.t === 'text') {
|
|
4024
|
+
const b = markBox(s)
|
|
4025
|
+
return x >= b.l - r && x <= b.r + r && y >= b.t - r && y <= b.b + r
|
|
4026
|
+
}
|
|
4027
|
+
const near = r + s.w / 2
|
|
4028
|
+
if (s.p) {
|
|
4029
|
+
if (s.p.length === 1) return Math.hypot(x - s.p[0][0], y - s.p[0][1]) <= near
|
|
4030
|
+
for (let i = 1; i < s.p.length; i++) if (segDist(x, y, s.p[i - 1], s.p[i]) <= near) return true
|
|
4031
|
+
return false
|
|
4032
|
+
}
|
|
4033
|
+
// 只认真正画出来的那条手绘线,不再按看不见的标准形状补判(不然会擦掉旁边的圈)
|
|
4034
|
+
const segs = markPath(s)
|
|
4035
|
+
if (segs.length) return segs.some(([a, b]) => segDist(x, y, a, b) <= near)
|
|
4036
|
+
return Math.hypot(x - s.a[0], y - s.a[1]) <= near
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
/** 按住 Shift:方框 / 圆圈画成正的,箭头吸到 45° 的倍数 */
|
|
4040
|
+
function markSnap(tool, a, b) {
|
|
4041
|
+
const dx = b[0] - a[0]
|
|
4042
|
+
const dy = b[1] - a[1]
|
|
4043
|
+
if (tool === 'arrow') {
|
|
4044
|
+
const ang = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4)
|
|
4045
|
+
const len = Math.hypot(dx, dy)
|
|
4046
|
+
return [a[0] + Math.cos(ang) * len, a[1] + Math.sin(ang) * len]
|
|
4047
|
+
}
|
|
4048
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy))
|
|
4049
|
+
return [a[0] + (dx < 0 ? -d : d), a[1] + (dy < 0 ? -d : d)]
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
/**
|
|
4053
|
+
* 一笔跟着它画在的那一屏走:画的时候记下那一屏的位置(an),侧栏拉宽拉窄、开面板、画布自己缩放后,按那一屏现在的位置和大小挪过去。
|
|
4054
|
+
* frames:台面上现在每一屏 { key, l, t, w }(台面坐标)
|
|
4055
|
+
*/
|
|
4056
|
+
function markPlace(s, frames) {
|
|
4057
|
+
const fit = (an) => {
|
|
4058
|
+
const now = an && frames.find((x) => x.key === an.key)
|
|
4059
|
+
if (!now) return null
|
|
4060
|
+
const k = now.w / an.w
|
|
4061
|
+
if (Math.abs(k - 1) < 1e-3 && Math.abs(now.l - an.l) < 0.25 && Math.abs(now.t - an.t) < 0.25) return null
|
|
4062
|
+
return { k, P: (q) => [now.l + (q[0] - an.l) * k, now.t + (q[1] - an.t) * k] }
|
|
4063
|
+
}
|
|
4064
|
+
const A = fit(s.an)
|
|
4065
|
+
if (s.t === 'text') { if (!A) return s; const [x, y] = A.P([s.x, s.y]); return { ...s, x, y, f: s.f * A.k } }
|
|
4066
|
+
if (s.p && s.ans) {
|
|
4067
|
+
// 跨了几屏的自由线:每个点跟着自己那一屏([x, y, 第几个锚点])
|
|
4068
|
+
const F = s.ans.map(fit)
|
|
4069
|
+
return F.some(Boolean) ? { ...s, p: s.p.map((q) => (F[q[2]] ? [...F[q[2]].P(q), q[2]] : q)) } : s
|
|
4070
|
+
}
|
|
4071
|
+
if (s.p) return A ? { ...s, p: s.p.map(A.P) } : s
|
|
4072
|
+
// 箭头、方框、圆圈:两头各跟各的屏(从这一屏指到那一屏的箭头,画布重新排了也还指着那儿)
|
|
4073
|
+
const B = s.bn ? fit(s.bn) : A
|
|
4074
|
+
if (!A && !B) return s
|
|
4075
|
+
return { ...s, a: A ? A.P(s.a) : s.a, b: B ? B.P(s.b) : s.b }
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
/** 盖在台面上的画布 + 上面一条工具栏 + 下面「取消 / 交给模型」 */
|
|
4079
|
+
function MarkLayer({ t, sending, frames, onCancel, onSend, onCopy }) {
|
|
4080
|
+
const wrap = useRef(null)
|
|
4081
|
+
const cv = useRef(null)
|
|
4082
|
+
const area = useRef(null)
|
|
4083
|
+
const [tool, setTool] = useState('pen')
|
|
4084
|
+
const [color, setColor] = useState(MARK_COLORS[0])
|
|
4085
|
+
const [size, setSize] = useState(1)
|
|
4086
|
+
const [pal, setPal] = useState(false) // 颜色板开着
|
|
4087
|
+
const [hist, setHist] = useState({ list: [[]], i: 0 }) // 每画一笔存一份整张的,撤销 / 重做就是前后挪
|
|
4088
|
+
const [txt, setTxt] = useState(null) // 正在写的字:{ x, y, v }
|
|
4089
|
+
// 头几次打开时页面中间教一下怎么用(画了第一笔就收起)
|
|
4090
|
+
const [coach] = useState(() => {
|
|
4091
|
+
try { const n = Number(localStorage.getItem('ddp-mk-coach')) || 0; if (n >= 3) return false; localStorage.setItem('ddp-mk-coach', String(n + 1)) } catch { /* 存不了就每次都教 */ }
|
|
4092
|
+
return true
|
|
4093
|
+
})
|
|
4094
|
+
const shapes = hist.list[hist.i]
|
|
4095
|
+
const cur = useRef(shapes)
|
|
4096
|
+
cur.current = shapes
|
|
4097
|
+
const opt = useRef({})
|
|
4098
|
+
opt.current = { tool, color, size }
|
|
4099
|
+
const txtRef = useRef(txt)
|
|
4100
|
+
txtRef.current = txt
|
|
4101
|
+
const live = useRef(null) // 正在画的那一笔;橡皮:{ erase: 还剩下的, hit }
|
|
4102
|
+
const raf = useRef(0)
|
|
4103
|
+
const dims = useRef({ w: 0, h: 0, k: 1 })
|
|
4104
|
+
const fr = useRef(frames)
|
|
4105
|
+
fr.current = frames
|
|
4106
|
+
const [, setTick] = useState(0) // 屏挪了:正写着的字框跟着挪
|
|
4107
|
+
const where = () => { try { return fr.current() } catch { return [] } }
|
|
4108
|
+
const placed = (list, f = where()) => list.map((s) => markPlace(s, f))
|
|
4109
|
+
|
|
4110
|
+
const paint = () => {
|
|
4111
|
+
raf.current = 0
|
|
4112
|
+
const c = cv.current
|
|
4113
|
+
if (!c) return
|
|
4114
|
+
const ctx = c.getContext('2d')
|
|
4115
|
+
ctx.clearRect(0, 0, c.width, c.height)
|
|
4116
|
+
const lv = live.current
|
|
4117
|
+
for (const s of placed(lv && lv.erase ? lv.erase : cur.current)) drawMark(ctx, s, dims.current.k)
|
|
4118
|
+
if (lv && !lv.erase) drawMark(ctx, lv, dims.current.k)
|
|
4119
|
+
}
|
|
4120
|
+
const repaint = () => { if (!raf.current) raf.current = requestAnimationFrame(paint) }
|
|
4121
|
+
useEffect(() => () => cancelAnimationFrame(raf.current), [])
|
|
4122
|
+
useEffect(() => { paint() }, [shapes])
|
|
4123
|
+
// 底下的屏挪了(开关面板、画布自己缩放、模型加了一屏):跟着重画
|
|
4124
|
+
useEffect(() => {
|
|
4125
|
+
let sig = ''
|
|
4126
|
+
const tm = setInterval(() => {
|
|
4127
|
+
const now = where().map((x) => `${x.key}:${Math.round(x.l)},${Math.round(x.t)},${Math.round(x.w)}`).join('|')
|
|
4128
|
+
if (now !== sig) { sig = now; repaint(); if (txtRef.current) setTick((n) => n + 1) }
|
|
4129
|
+
}, 250)
|
|
4130
|
+
return () => clearInterval(tm)
|
|
4131
|
+
}, [])
|
|
4132
|
+
|
|
4133
|
+
// 画布跟着台面一样大(右侧栏拉宽拉窄),按屏幕像素密度画,线不糊
|
|
4134
|
+
useEffect(() => {
|
|
4135
|
+
const el = wrap.current
|
|
4136
|
+
if (!el) return undefined
|
|
4137
|
+
const fit = () => {
|
|
4138
|
+
const r = el.getBoundingClientRect()
|
|
4139
|
+
const k = Math.min(2, Math.max(1, window.devicePixelRatio || 1))
|
|
4140
|
+
dims.current = { w: r.width, h: r.height, k }
|
|
4141
|
+
if (cv.current) {
|
|
4142
|
+
cv.current.width = Math.max(1, Math.round(r.width * k))
|
|
4143
|
+
cv.current.height = Math.max(1, Math.round(r.height * k))
|
|
4144
|
+
}
|
|
4145
|
+
paint()
|
|
4146
|
+
}
|
|
4147
|
+
fit()
|
|
4148
|
+
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(fit) : null
|
|
4149
|
+
if (ro) ro.observe(el)
|
|
4150
|
+
try { el.focus({ preventScroll: true }) } catch { /* 点一下画布也行 */ }
|
|
4151
|
+
return () => { if (ro) ro.disconnect() }
|
|
4152
|
+
}, [])
|
|
4153
|
+
|
|
4154
|
+
const commit = (next) => setHist((hs) => {
|
|
4155
|
+
const list = hs.list.slice(Math.max(0, hs.i - 79), hs.i + 1)
|
|
4156
|
+
list.push(next)
|
|
4157
|
+
return { list, i: list.length - 1 }
|
|
4158
|
+
})
|
|
4159
|
+
const undo = () => { if (!live.current) setHist((hs) => (hs.i > 0 ? { ...hs, i: hs.i - 1 } : hs)) }
|
|
4160
|
+
const redo = () => { if (!live.current) setHist((hs) => (hs.i < hs.list.length - 1 ? { ...hs, i: hs.i + 1 } : hs)) }
|
|
4161
|
+
// 清掉:正写着的字也不要了
|
|
4162
|
+
const clear = () => {
|
|
4163
|
+
if (txtRef.current) { txtRef.current = null; setTxt(null) }
|
|
4164
|
+
if (cur.current.length) commit([])
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// 写字:回车 / 点别处算写完;Esc 不要了。颜色、大小按写完那一刻的
|
|
4168
|
+
const textOf = (x) => {
|
|
4169
|
+
const v = String(x.v || '').replace(/\s+$/, '').slice(0, 2000)
|
|
4170
|
+
return v.trim() ? { t: 'text', c: opt.current.color, w: 0, f: MARK_FONT[opt.current.size], x: x.x, y: x.y, v, an: x.an, sd: 1 + Math.floor(Math.random() * 2e9) } : null
|
|
4171
|
+
}
|
|
4172
|
+
// refocus:回车 / Esc / 点画布写完的,键盘交回画布;点到别处(比如聊天输入框)失焦的,别把焦点抢回来
|
|
4173
|
+
const endText = (keep, refocus) => {
|
|
4174
|
+
const x = txtRef.current
|
|
4175
|
+
if (!x) return null
|
|
4176
|
+
txtRef.current = null
|
|
4177
|
+
setTxt(null)
|
|
4178
|
+
const s = keep ? textOf(x) : null
|
|
4179
|
+
if (s && cur.current.length < MARK_MAX) commit([...cur.current, s])
|
|
4180
|
+
if (refocus) { try { wrap.current.focus({ preventScroll: true }) } catch { /* 已经关了 */ } }
|
|
4181
|
+
return s
|
|
4182
|
+
}
|
|
4183
|
+
useEffect(() => {
|
|
4184
|
+
if (!txt || !area.current || document.activeElement === area.current) return undefined
|
|
4185
|
+
const tm = setTimeout(() => { try { area.current.focus({ preventScroll: true }) } catch { /* 关了 */ } }, 0)
|
|
4186
|
+
return () => clearTimeout(tm)
|
|
4187
|
+
}, [!!txt])
|
|
4188
|
+
|
|
4189
|
+
const at = (e) => {
|
|
4190
|
+
const r = wrap.current.getBoundingClientRect()
|
|
4191
|
+
return [e.clientX - r.left, e.clientY - r.top]
|
|
4192
|
+
}
|
|
4193
|
+
const eraseAt = (x, y) => {
|
|
4194
|
+
const lv = live.current
|
|
4195
|
+
const f = where()
|
|
4196
|
+
const left = lv.erase.filter((s) => !markHit(markPlace(s, f), x, y, 6))
|
|
4197
|
+
if (left.length !== lv.erase.length) { lv.erase = left; lv.hit = true; repaint() }
|
|
4198
|
+
}
|
|
4199
|
+
// 画的途中屏要是挪了:按落笔完那一刻那一屏的位置记(这一笔是照着现在看到的画的)
|
|
4200
|
+
const reAnchor = (an) => {
|
|
4201
|
+
const now = an && where().find((q) => q.key === an.key)
|
|
4202
|
+
return now ? { key: now.key, l: now.l, t: now.t, w: now.w } : an
|
|
4203
|
+
}
|
|
4204
|
+
// 自由线:看每个点落在哪屏;只在一屏里的照旧整条跟那一屏,跨屏的每个点记自己那屏(落在屏外的点跟最近的前后点)
|
|
4205
|
+
const penAnchors = (lv) => {
|
|
4206
|
+
const fz = where()
|
|
4207
|
+
const ans = []
|
|
4208
|
+
let last = null
|
|
4209
|
+
const idx = lv.p.map(([x, y]) => {
|
|
4210
|
+
const hit = last && x >= last.l && x <= last.l + last.w && y >= last.t && y <= last.t + last.h ? last
|
|
4211
|
+
: fz.find((q) => x >= q.l && x <= q.l + q.w && y >= q.t && y <= q.t + q.h)
|
|
4212
|
+
if (!hit) return -1
|
|
4213
|
+
last = hit
|
|
4214
|
+
let i = ans.findIndex((a) => a.key === hit.key)
|
|
4215
|
+
if (i < 0) { i = ans.length; ans.push({ key: hit.key, l: hit.l, t: hit.t, w: hit.w }) }
|
|
4216
|
+
return i
|
|
4217
|
+
})
|
|
4218
|
+
if (ans.length < 2) return { ...lv, an: ans[0] || reAnchor(lv.an) }
|
|
4219
|
+
for (let i = 1; i < idx.length; i++) if (idx[i] < 0) idx[i] = idx[i - 1]
|
|
4220
|
+
for (let i = idx.length - 2; i >= 0; i--) if (idx[i] < 0) idx[i] = idx[i + 1]
|
|
4221
|
+
return { ...lv, an: ans[0], ans, p: lv.p.map((q, i) => [q[0], q[1], idx[i]]) }
|
|
4222
|
+
}
|
|
4223
|
+
// 这一点落在哪一屏上(落在屏外就不跟着谁)
|
|
4224
|
+
const anchorAt = (x, y) => {
|
|
4225
|
+
const hit = where().find((q) => x >= q.l && x <= q.l + q.w && y >= q.t && y <= q.t + q.h)
|
|
4226
|
+
return hit ? { key: hit.key, l: hit.l, t: hit.t, w: hit.w } : null
|
|
4227
|
+
}
|
|
4228
|
+
const onDown = (e) => {
|
|
4229
|
+
if (e.button !== 0 || sending || live.current) return
|
|
4230
|
+
e.preventDefault()
|
|
4231
|
+
setPal(false)
|
|
4232
|
+
if (!wrap.current.contains(document.activeElement)) { try { wrap.current.focus({ preventScroll: true }) } catch { /* 键盘快捷键用不了而已 */ } }
|
|
4233
|
+
// 正写着字:这一下只算写完
|
|
4234
|
+
if (txtRef.current) { endText(true, true); return }
|
|
4235
|
+
const [x, y] = at(e)
|
|
4236
|
+
const o = opt.current
|
|
4237
|
+
if (o.tool === 'text') {
|
|
4238
|
+
if (cur.current.length < MARK_MAX) setTxt({ x, y: y - MARK_FONT[o.size] * 0.65, v: '', an: anchorAt(x, y) })
|
|
4239
|
+
return
|
|
4240
|
+
}
|
|
4241
|
+
if (o.tool !== 'erase' && cur.current.length >= MARK_MAX) return
|
|
4242
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch { /* 拖出去就断了也没事 */ }
|
|
4243
|
+
if (o.tool === 'erase') {
|
|
4244
|
+
live.current = { erase: cur.current.slice(), hit: false }
|
|
4245
|
+
eraseAt(x, y)
|
|
4246
|
+
return
|
|
4247
|
+
}
|
|
4248
|
+
const w = MARK_WIDTH[o.size] * (o.tool === 'hl' ? 4 : 1)
|
|
4249
|
+
const an = anchorAt(x, y)
|
|
4250
|
+
live.current = o.tool === 'pen' || o.tool === 'hl' ? { t: o.tool, c: o.color, w, p: [[x, y]], an } : { t: o.tool, c: o.color, w, a: [x, y], b: [x, y], an, sd: 1 + Math.floor(Math.random() * 2e9) }
|
|
4251
|
+
repaint()
|
|
4252
|
+
}
|
|
4253
|
+
const onMove = (e) => {
|
|
4254
|
+
const lv = live.current
|
|
4255
|
+
if (!lv) return
|
|
4256
|
+
const [x, y] = at(e)
|
|
4257
|
+
if (lv.erase) { eraseAt(x, y); return }
|
|
4258
|
+
if (lv.p) {
|
|
4259
|
+
const q = lv.p[lv.p.length - 1]
|
|
4260
|
+
if (Math.hypot(x - q[0], y - q[1]) >= 1.5 && lv.p.length < 5000) lv.p.push([x, y])
|
|
4261
|
+
} else lv.b = e.shiftKey ? markSnap(lv.t, lv.a, [x, y]) : [x, y]
|
|
4262
|
+
repaint()
|
|
4263
|
+
}
|
|
4264
|
+
const onUp = () => {
|
|
4265
|
+
const lv = live.current
|
|
4266
|
+
if (!lv) return
|
|
4267
|
+
live.current = null
|
|
4268
|
+
if (lv.erase) { if (lv.hit) commit(lv.erase); else paint(); return }
|
|
4269
|
+
// 方框、箭头只点了一下没拖:不算
|
|
4270
|
+
if (lv.p) commit([...cur.current, penAnchors(lv)])
|
|
4271
|
+
else if (Math.hypot(lv.b[0] - lv.a[0], lv.b[1] - lv.a[1]) >= 4) {
|
|
4272
|
+
commit([...cur.current, { ...lv, an: reAnchor(lv.an), bn: anchorAt(lv.b[0], lv.b[1]) }])
|
|
4273
|
+
} else paint()
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
const onKey = (e) => {
|
|
4277
|
+
if (e.target === area.current) {
|
|
4278
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); endText(false, true) }
|
|
4279
|
+
else if (e.key === 'Enter' && !e.shiftKey && !(e.nativeEvent && e.nativeEvent.isComposing) && e.keyCode !== 229) { e.preventDefault(); e.stopPropagation(); endText(true, true) }
|
|
4280
|
+
return
|
|
4281
|
+
}
|
|
4282
|
+
if (e.target && e.target.tagName === 'BUTTON' && (e.key === 'Enter' || e.key === ' ')) return
|
|
4283
|
+
const mod = e.metaKey || e.ctrlKey
|
|
4284
|
+
const k = String(e.key).toLowerCase()
|
|
4285
|
+
let done = true
|
|
4286
|
+
if (e.key === 'Escape') { if (pal) setPal(false); else if (!sending) onCancel() }
|
|
4287
|
+
else if (mod && k === 'z') { if (e.shiftKey) redo(); else undo() }
|
|
4288
|
+
else if (mod && k === 'y') redo()
|
|
4289
|
+
else if (mod && k === 'c' && onCopy && !e.shiftKey && !e.altKey) copy()
|
|
4290
|
+
else if (!mod && !e.altKey && MARK_TOOLS.some((x) => x[3] === k)) { setTool(MARK_TOOLS.find((x) => x[3] === k)[0]); setPal(false) }
|
|
4291
|
+
else done = false
|
|
4292
|
+
if (done) { e.preventDefault(); e.stopPropagation() }
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
// 交出去的是按屏现在的位置摆好的;正写着的字也算上
|
|
4296
|
+
const done = () => {
|
|
4297
|
+
const s = txtRef.current ? endText(true, true) : null
|
|
4298
|
+
return placed(s && cur.current.length < MARK_MAX ? [...cur.current, s] : cur.current)
|
|
4299
|
+
}
|
|
4300
|
+
const send = () => { if (!sending) onSend(done()) }
|
|
4301
|
+
// 复制:拼成同一张图放进剪贴板,贴到别的地方用(issue #6;不支持的环境不给这个按钮)
|
|
4302
|
+
const copy = () => { if (!sending && onCopy) onCopy(done()) }
|
|
4303
|
+
|
|
4304
|
+
// 工具栏上按下去别抢走焦点:正写着的字不会被打断,快捷键也还管用
|
|
4305
|
+
const keep = (e) => { if (e.target !== area.current) e.preventDefault() }
|
|
4306
|
+
const btn = (key, icon, title, on, run, off) => h('button', { key, type: 'button', className: 'ddp-btn' + (on ? ' sel' : ''), title, 'aria-label': title, 'aria-pressed': on || undefined, disabled: !!off, onClick: run }, h(I[icon]))
|
|
4307
|
+
const f = MARK_FONT[size]
|
|
4308
|
+
let box = null
|
|
4309
|
+
if (txt) {
|
|
4310
|
+
// 字框和写完后的字一样跟着那一屏走
|
|
4311
|
+
const p = markPlace({ t: 'text', x: txt.x, y: txt.y, f, an: txt.an }, where())
|
|
4312
|
+
const pf = p.f
|
|
4313
|
+
const lines = txt.v.split('\n')
|
|
4314
|
+
const w = Math.max(markTextWidth(txt.v ? '' : t('mkTextPh'), pf), ...lines.map((l) => markTextWidth(l, pf))) + pf
|
|
4315
|
+
box = { left: p.x - 3, top: p.y - 1, width: Math.max(pf * 3, Math.min(w, dims.current.w - p.x - 6)), height: lines.length * pf * 1.3 + 2, fontSize: pf, color }
|
|
4316
|
+
}
|
|
4317
|
+
return h('div', { className: 'ddp-mark', ref: wrap, tabIndex: -1, onKeyDown: onKey },
|
|
4318
|
+
h('canvas', { ref: cv, className: 'cv' + (tool === 'text' ? ' tx' : tool === 'erase' ? ' er' : ''), onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp }),
|
|
4319
|
+
txt ? h('textarea', {
|
|
4320
|
+
ref: area,
|
|
4321
|
+
className: markLight(color) ? 'lt' : '',
|
|
4322
|
+
value: txt.v,
|
|
4323
|
+
placeholder: t('mkTextPh'),
|
|
4324
|
+
spellCheck: false,
|
|
4325
|
+
rows: 1,
|
|
4326
|
+
style: box,
|
|
4327
|
+
onChange: (e) => setTxt({ ...txt, v: e.target.value }),
|
|
4328
|
+
onBlur: () => endText(true),
|
|
4329
|
+
}) : null,
|
|
4330
|
+
h('div', { className: 'ddp-mk-top' },
|
|
4331
|
+
h('div', { className: 'ddp-mk-tools', onMouseDown: keep },
|
|
4332
|
+
...MARK_TOOLS.map(([id, icon, label, key]) => btn(id, icon, `${t(label)} (${key.toUpperCase()})`, tool === id, () => { setTool(id); setPal(false) })),
|
|
4333
|
+
h('span', { className: 'ddp-sep' }),
|
|
4334
|
+
h('div', { className: 'ddp-mk-color' },
|
|
4335
|
+
h('button', { type: 'button', className: 'ddp-btn' + (pal ? ' sel' : ''), title: t('mkColor'), 'aria-label': t('mkColor'), 'aria-expanded': pal, onClick: () => setPal(!pal) }, h('i', { style: { background: color } })),
|
|
4336
|
+
pal ? h('div', { className: 'ddp-mk-pal' }, MARK_COLORS.map((c) => h('button', {
|
|
4337
|
+
key: c, type: 'button', className: c === color ? 'sel' : '', style: { background: c }, title: c, 'aria-label': c, onClick: () => { setColor(c); setPal(false) },
|
|
4338
|
+
}))) : null),
|
|
4339
|
+
h('div', { className: 'ddp-mk-sz', role: 'group', 'aria-label': t('mkSize') },
|
|
4340
|
+
MARK_WIDTH.map((_, i) => h('button', { key: i, type: 'button', className: 'ddp-btn' + (i === size ? ' sel' : ''), title: t('mkSize'), 'aria-label': `${t('mkSize')} ${i + 1}`, 'aria-pressed': i === size, onClick: () => setSize(i) },
|
|
4341
|
+
h('i', { style: { width: 4 + i * 3 + 'px', height: 4 + i * 3 + 'px' } })))),
|
|
4342
|
+
h('span', { className: 'ddp-sep' }),
|
|
4343
|
+
btn('undo', 'undo', `${t('mkUndo')} (${MOD}Z)`, false, undo, hist.i === 0),
|
|
4344
|
+
btn('redo', 'redo', `${t('mkRedo')} (${MOD === '⌘' ? '⇧⌘Z' : 'Ctrl+Y'})`, false, redo, hist.i >= hist.list.length - 1),
|
|
4345
|
+
btn('clear', 'trash', t('mkClear'), false, clear, !shapes.length && !txt))),
|
|
4346
|
+
coach && !shapes.length && !txt ? h('div', { className: 'ddp-mk-coach', 'aria-hidden': true },
|
|
4347
|
+
h('b', null, t('mkCoach')),
|
|
4348
|
+
h('ol', null, h('li', null, t('mkCoach1')), h('li', null, t('mkCoach2')), h('li', null, t('mkCoach3')))) : null,
|
|
4349
|
+
h('div', { className: 'ddp-mk-bot' },
|
|
4350
|
+
h('div', { className: 'ddp-mk-bar', onMouseDown: keep },
|
|
4351
|
+
// 现在这个工具怎么用(只认图标的人也知道该怎么画)
|
|
4352
|
+
sending ? h('span', null, t('mkSending')) : h('span', { title: t('mkHow_' + tool) }, h('b', null, t(MARK_TOOLS.find((x) => x[0] === tool)[2])), t('mkHow_' + tool)),
|
|
4353
|
+
h('button', { type: 'button', disabled: sending, onClick: onCancel }, t('mkCancel')),
|
|
4354
|
+
onCopy ? h('button', { type: 'button', className: 'cp', title: `${t('mkCopyTip')} (${MOD}C)`, disabled: sending, onClick: copy }, t('mkCopy')) : null,
|
|
4355
|
+
h('button', { type: 'button', className: 'go' + (shapes.length || (txt && txt.v.trim()) ? '' : ' idle'), disabled: sending, onClick: send }, t('mkSend')))))
|
|
4356
|
+
}
|
|
4357
|
+
|
|
2947
4358
|
/** 设计规范面板:按组列出变量,点一个放进输入框 */
|
|
2948
4359
|
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onSystems, onClose }) {
|
|
2949
4360
|
const groups = {}
|
|
@@ -3059,17 +4470,71 @@ window.__ModuleLoader__.load({
|
|
|
3059
4470
|
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
3060
4471
|
}
|
|
3061
4472
|
|
|
3062
|
-
/**
|
|
4473
|
+
/** 风格样板:用这个风格画的一小屏(顶栏、标题、按钮、商品卡、数据卡);卡片上缩小着放,点开是原大 */
|
|
4474
|
+
function StyleSample({ t, x }) {
|
|
4475
|
+
const c = x.c
|
|
4476
|
+
const L = styleLook(x)
|
|
4477
|
+
const muted = { color: c.muted }
|
|
4478
|
+
return h('div', { className: 'ddp-sam', style: { background: c.bg, backgroundImage: x.bgImage || 'none', color: c.text, fontFamily: SANS } },
|
|
4479
|
+
h('div', { className: 'top' },
|
|
4480
|
+
h('i', { className: 'lg', style: { background: c.primary, borderRadius: Math.min(x.radius, 5) } }),
|
|
4481
|
+
h('b', { style: L.head }, t('samBrand')),
|
|
4482
|
+
h('span', { className: 'nv', style: muted }, t('samNav')),
|
|
4483
|
+
h('i', { className: 'av', style: { background: L.img } })),
|
|
4484
|
+
h('h5', { style: L.head }, t('samHead')),
|
|
4485
|
+
h('p', { style: muted }, t('samSub')),
|
|
4486
|
+
h('div', { className: 'acts' }, h('span', { className: 'bt', style: L.btn }, t('samGo')), h('span', { style: { color: c.link } }, t('samMore'))),
|
|
4487
|
+
h('div', { className: 'cards' },
|
|
4488
|
+
h('div', { className: 'cd', style: L.card },
|
|
4489
|
+
h('i', { className: 'im', style: { background: L.img, borderRadius: Math.max(0, x.radius - 6) } }),
|
|
4490
|
+
h('b', null, t('samItem')),
|
|
4491
|
+
h('span', { style: muted }, t('samMeta')),
|
|
4492
|
+
h('div', { className: 'pr' }, h('b', { style: { fontFamily: x.head } }, t('samPrice')), h('span', { className: 'chip', style: L.chip }, t('samTag')))),
|
|
4493
|
+
h('div', { className: 'cd', style: L.card },
|
|
4494
|
+
h('span', { style: muted }, t('samStat')),
|
|
4495
|
+
h('b', { className: 'num', style: L.head }, '86%'),
|
|
4496
|
+
h('i', { className: 'bar', style: { background: L.track } }, h('i', { style: { background: c.primary } })),
|
|
4497
|
+
h('span', { style: muted }, t('samStatMeta')))))
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
/** 一排风格卡:每张是缩小的样板;点一张看原大样板(左右翻),「用这个风格」才交给 onPick */
|
|
3063
4501
|
function StyleCards({ t, onPick }) {
|
|
3064
4502
|
const en = t('lang') === 'en'
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
4503
|
+
const [on, setOn] = useState(-1) // 正在看样板的那个;-1 是看全部
|
|
4504
|
+
const was = useRef(-1)
|
|
4505
|
+
const box = useRef(null)
|
|
4506
|
+
useEffect(() => {
|
|
4507
|
+
// 进样板:焦点给「用这个风格」;回全部:焦点回到刚才那张卡
|
|
4508
|
+
const el = box.current
|
|
4509
|
+
if (!el) return
|
|
4510
|
+
const f = on >= 0 ? el.querySelector('.ddp-go') : was.current >= 0 ? el.querySelectorAll('.ddp-sc')[was.current] : null
|
|
4511
|
+
if (f) f.focus({ preventScroll: true })
|
|
4512
|
+
// 列表和样板高矮不一样,外面滚动的位置会变:把样板 / 刚才那张卡滚进来
|
|
4513
|
+
if (was.current >= 0 || on >= 0) (on >= 0 ? el : f || el).scrollIntoView({ block: 'nearest' })
|
|
4514
|
+
if (on >= 0) was.current = on
|
|
4515
|
+
}, [on])
|
|
4516
|
+
const n = STYLES.length
|
|
4517
|
+
if (on >= 0) {
|
|
4518
|
+
const x = STYLES[on]
|
|
4519
|
+
const go = (d) => setOn((on + d + n) % n)
|
|
4520
|
+
const onKey = (e) => {
|
|
4521
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); go(e.key === 'ArrowLeft' ? -1 : 1) } else if (e.key === 'Escape') { e.stopPropagation(); setOn(-1) }
|
|
4522
|
+
}
|
|
4523
|
+
return h('div', { className: 'ddp-samview', ref: box, onKeyDown: onKey },
|
|
4524
|
+
h('button', { className: 'ddp-btn all', onClick: () => setOn(-1) }, h(I.back), h('span', null, t('stylesAll'))),
|
|
4525
|
+
h(StyleSample, { key: x.id, t, x }),
|
|
4526
|
+
h('div', { className: 'meta' },
|
|
4527
|
+
h('b', null, en ? x.en : x.zh),
|
|
4528
|
+
h('span', { className: 'sw' }, ...['bg', 'surface', 'text', 'primary', 'accent'].map((k) => h('i', { key: k, title: x.c[k], style: { background: x.c[k] } }))),
|
|
4529
|
+
h('p', null, en ? x.den : x.dzh)),
|
|
4530
|
+
h('div', { className: 'row' },
|
|
4531
|
+
h('button', { className: 'ddp-btn', title: t('stylePrev'), onClick: () => go(-1) }, h(I.back)),
|
|
4532
|
+
h('em', null, `${on + 1} / ${n}`),
|
|
4533
|
+
h('button', { className: 'ddp-btn nx', title: t('styleNext'), onClick: () => go(1) }, h(I.back)),
|
|
4534
|
+
h('button', { className: 'ddp-go', onClick: () => onPick(x) }, t('styleUse'))))
|
|
4535
|
+
}
|
|
4536
|
+
return h('div', { className: 'ddp-styles', ref: box }, ...STYLES.map((x, i) => h('button', { key: x.id, className: 'ddp-sc', title: en ? x.den : x.dzh, onClick: () => setOn(i) },
|
|
4537
|
+
h('span', { className: 'pv', 'aria-hidden': 'true' }, h(StyleSample, { t, x })),
|
|
3073
4538
|
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
3074
4539
|
}
|
|
3075
4540
|
|
|
@@ -3103,7 +4568,7 @@ window.__ModuleLoader__.load({
|
|
|
3103
4568
|
/** 拖过的滑块 → 交给模型的一段字 */
|
|
3104
4569
|
function formatTweaks(t, list, live) {
|
|
3105
4570
|
const rows = list.filter((k) => hasOwn(live, k.id)).map((k) => `- ${k.label}:${tweakShown(k, k.value)} → ${tweakShown(k, live[k.id])}(${k.var || k.attr})`)
|
|
3106
|
-
return t('twAskHead') + '\n' + rows.join('\n') + '\n'
|
|
4571
|
+
return t('twAskHead') + '\n' + rows.join('\n') + '\n' + t('twAskTail') + '\n'
|
|
3107
4572
|
}
|
|
3108
4573
|
|
|
3109
4574
|
/** 调节滑块面板:模型给这套设计做的几个维度(tweaks.json),拖的时候所有屏一起变,定下来写回文件或交给模型 */
|
|
@@ -3151,6 +4616,26 @@ window.__ModuleLoader__.load({
|
|
|
3151
4616
|
}
|
|
3152
4617
|
|
|
3153
4618
|
/** 品牌设计系统库:从用户的网站 / 项目提取的几套,默认那套以后做新设计都照它来。rev 变了(模型干完一轮)重新列 */
|
|
4619
|
+
/** 输入框旁的「选文件夹」:桌面版弹系统的选文件夹窗口,选了填进去;网页版没有这个按钮 */
|
|
4620
|
+
function DirButton({ t, onPicked, onSay }) {
|
|
4621
|
+
const dp = dirPicker()
|
|
4622
|
+
const [busy, setBusy] = useState(false)
|
|
4623
|
+
if (!dp) return null
|
|
4624
|
+
const pick = async () => {
|
|
4625
|
+
if (busy) return
|
|
4626
|
+
setBusy(true)
|
|
4627
|
+
try {
|
|
4628
|
+
const p = await dp.pick()
|
|
4629
|
+
if (p) onPicked(p)
|
|
4630
|
+
} catch (e) {
|
|
4631
|
+
if (onSay) onSay(t('pickDirFail', { msg: e && e.message ? e.message : String(e) }))
|
|
4632
|
+
} finally {
|
|
4633
|
+
setBusy(false)
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
return h('button', { type: 'button', className: 'ddp-dirbtn', title: t('pickDir'), 'aria-label': t('pickDir'), disabled: busy, onClick: pick }, h(I.folder))
|
|
4637
|
+
}
|
|
4638
|
+
|
|
3154
4639
|
function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose }) {
|
|
3155
4640
|
const [data, setData] = useState(null)
|
|
3156
4641
|
const [err, setErr] = useState('')
|
|
@@ -3186,6 +4671,7 @@ window.__ModuleLoader__.load({
|
|
|
3186
4671
|
h('footer', null,
|
|
3187
4672
|
h('form', { onSubmit: go },
|
|
3188
4673
|
h('input', { value: src, placeholder: t('sysPh'), 'aria-label': t('sysPh'), onChange: (e) => setSrc(e.target.value) }),
|
|
4674
|
+
h(DirButton, { t, onPicked: setSrc, onSay }),
|
|
3189
4675
|
h('button', { type: 'submit', disabled: !src.trim() }, t('sysExtract')))))
|
|
3190
4676
|
}
|
|
3191
4677
|
|
|
@@ -3193,25 +4679,56 @@ window.__ModuleLoader__.load({
|
|
|
3193
4679
|
const FW_LABEL = { react: 'React + Vite', vue: 'Vue 3 + Vite', next: 'Next.js' }
|
|
3194
4680
|
|
|
3195
4681
|
/** 转成代码:用什么做、放哪、转几屏;点了把一段话放进输入框,模型调 handoff_design 照交接包做,每屏做完 compare_design */
|
|
3196
|
-
function HandoffPanel({ t, sid, root, fileTarget, count, cur, onGo, onClose }) {
|
|
4682
|
+
function HandoffPanel({ t, sid, root, fileTarget, count, cur, onGo, onSay, onClose }) {
|
|
3197
4683
|
const [info, setInfo] = useState(null)
|
|
3198
4684
|
const [fw, setFw] = useState('')
|
|
3199
4685
|
const [dest, setDest] = useState('')
|
|
3200
4686
|
const [one, setOne] = useState(false)
|
|
4687
|
+
const [moved, setMoved] = useState(null) // 选的文件夹不能直接放:{ dest, why }('nested' 在里面新建、'beside' 放设计目录旁边)
|
|
4688
|
+
const [checking, setChecking] = useState('') // 'busy' 正在问服务器这个文件夹;别的字是没问成的原因
|
|
4689
|
+
// 现在是哪套设计、第几次选:选文件夹的窗口开着时换了设计、或者又选了一次 / 手改了路径,旧的结果作废
|
|
4690
|
+
// hand:用户自己点过框架(换设计前一直听用户的,不再按选的文件夹自动换)
|
|
4691
|
+
const live = useRef({ root, seq: 0, hand: false })
|
|
4692
|
+
live.current.root = root
|
|
3201
4693
|
// 换了会话或设计目录就重新问:先清掉旧的(别把上一套设计的目标发出去)
|
|
3202
4694
|
useEffect(() => {
|
|
3203
4695
|
let alive = true
|
|
4696
|
+
live.current.seq++
|
|
4697
|
+
live.current.hand = false
|
|
3204
4698
|
setInfo(null)
|
|
4699
|
+
setFw('') // 上一套设计的框架(比如已经没有的「跟项目一样」)别留着
|
|
3205
4700
|
setDest('')
|
|
4701
|
+
setMoved(null)
|
|
4702
|
+
setChecking('')
|
|
3206
4703
|
if (!root) return () => { alive = false }
|
|
3207
4704
|
getJson(`project?session=${encodeURIComponent(sid)}`).then((d) => {
|
|
3208
4705
|
if (!alive) return
|
|
3209
4706
|
setInfo(d)
|
|
3210
|
-
setFw(d.
|
|
4707
|
+
setFw(d.fw || 'react')
|
|
3211
4708
|
setDest(d.root === root ? d.dest || '' : '')
|
|
3212
|
-
}, () => { if (alive) setInfo({}) })
|
|
4709
|
+
}, () => { if (alive) { setInfo({}); setFw('react') } })
|
|
3213
4710
|
return () => { alive = false }
|
|
3214
4711
|
}, [sid, root])
|
|
4712
|
+
// 选了文件夹:问一下它是不是项目——是就放进去、跟着它的框架;不是而且有东西,就在里面新建一个目录(下面说一声,能改)
|
|
4713
|
+
const pickDest = (dir) => {
|
|
4714
|
+
if (live.current.root !== root) return
|
|
4715
|
+
const my = ++live.current.seq
|
|
4716
|
+
setDest(dir)
|
|
4717
|
+
setMoved(null)
|
|
4718
|
+
setChecking('busy') // 问清楚之前「开始」点不了:别把没调整过的目录发出去
|
|
4719
|
+
getJson(`project?session=${encodeURIComponent(sid)}&dest=${encodeURIComponent(dir)}`).then((d) => {
|
|
4720
|
+
if (my !== live.current.seq) return
|
|
4721
|
+
if (!d || d.root !== root || typeof d.dest !== 'string') return setChecking(t('hoCheckFail', { msg: '?' }))
|
|
4722
|
+
setChecking('')
|
|
4723
|
+
setInfo(d)
|
|
4724
|
+
setDest(d.dest)
|
|
4725
|
+
setMoved(d.why ? { dest: d.dest, why: d.why } : null)
|
|
4726
|
+
// 框架按服务器给的默认(是项目就跟它一样,纯 HTML 站点就纯 HTML);用户自己点过就听用户的,除非点的「跟项目一样」没了
|
|
4727
|
+
const same = !!(d.project && d.project.pkg && d.project.framework)
|
|
4728
|
+
setFw((f) => (live.current.hand && (f !== 'same' || same) ? f : d.fw || 'react'))
|
|
4729
|
+
}, (e) => { if (my === live.current.seq) setChecking(t('hoCheckFail', { msg: (e && e.message) || '?' })) })
|
|
4730
|
+
}
|
|
4731
|
+
const typeDest = (v) => { live.current.seq++; setChecking(''); setDest(v) }
|
|
3215
4732
|
const head = h('header', null, h(I.code), h('b', null, t('handoff')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
|
|
3216
4733
|
const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style ddp-ho', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
|
|
3217
4734
|
if (!fileTarget) return box(h('div', { className: 'bd' }, h('div', { className: 'none' }, t('hoProxy'))))
|
|
@@ -3224,15 +4741,19 @@ window.__ModuleLoader__.load({
|
|
|
3224
4741
|
h('p', { className: 'tip' }, t('hoTip')),
|
|
3225
4742
|
h('div', { className: 'bd' },
|
|
3226
4743
|
h('div', { className: 'row' }, h('label', null, t('hoFw')),
|
|
3227
|
-
...opts.map(([k, l]) => h('button', { key: k, className: 'opt' + (fw === k ? ' on' : ''), onClick: () => setFw(k) }, h('i'), h('span', null, l)))),
|
|
4744
|
+
...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)))),
|
|
3228
4745
|
h('div', { className: 'row' }, h('label', null, t('hoDest')),
|
|
3229
|
-
h('
|
|
4746
|
+
h('div', { className: 'pathrow' },
|
|
4747
|
+
h('input', { type: 'text', value: dest, spellCheck: false, 'aria-label': t('hoDest'), onChange: (e) => typeDest(e.target.value) }),
|
|
4748
|
+
h(DirButton, { t, onPicked: pickDest, onSay })),
|
|
4749
|
+
checking ? h('p', { className: 'moved' + (checking === 'busy' ? '' : ' bad') }, checking === 'busy' ? t('hoChecking') : checking)
|
|
4750
|
+
: moved && moved.dest === dest ? h('p', { className: 'moved' }, t(moved.why === 'beside' ? 'hoBeside' : 'hoMoved', { name: moved.dest.split(/[\\/]/).pop() })) : null),
|
|
3230
4751
|
count > 1 && cur ? h('div', { className: 'row' }, h('label', null, t('hoScope')),
|
|
3231
4752
|
h('div', { className: 'seg' },
|
|
3232
4753
|
h('button', { className: one ? '' : 'on', onClick: () => setOne(false) }, t('hoAll', { n: count })),
|
|
3233
4754
|
h('button', { className: one ? 'on' : '', onClick: () => setOne(true) }, t('hoOne')))) : null),
|
|
3234
4755
|
h('footer', null,
|
|
3235
|
-
h('button', { className: 'go', disabled: !fw || !dest.trim(), onClick: () => onGo({ fw, label, dest: dest.trim(), only: one && count > 1 ? cur : '' }) }, h(I.code), h('span', null, t('hoGo')))))
|
|
4756
|
+
h('button', { className: 'go', disabled: !fw || !dest.trim() || checking === 'busy', onClick: () => onGo({ fw, label, dest: dest.trim(), only: one && count > 1 ? cur : '' }) }, h(I.code), h('span', null, t('hoGo')))))
|
|
3236
4757
|
}
|
|
3237
4758
|
|
|
3238
4759
|
/** 菜单开着时:点外面、按 Esc 关掉 */
|
|
@@ -3559,8 +5080,11 @@ window.__ModuleLoader__.load({
|
|
|
3559
5080
|
const [sel, setSel] = useState('')
|
|
3560
5081
|
const [panel, setPanel] = useState(false)
|
|
3561
5082
|
const [styles, setStyles] = useState(false) // 换风格面板
|
|
3562
|
-
const [dropping, setDropping] = useState(
|
|
5083
|
+
const [dropping, setDropping] = useState('') // 正拖着文件进来:dragKind 给的种类
|
|
5084
|
+
const [dropSoft, setDropSoft] = useState(false) // 拖动在预览页面里:蒙层只是提示、不挡事件(松手由页面转告)
|
|
5085
|
+
const softTimer = useRef(0)
|
|
3563
5086
|
const refInput = useRef(null)
|
|
5087
|
+
const pageInput = useRef(null)
|
|
3564
5088
|
const [exMenu, setExMenu] = useState(false)
|
|
3565
5089
|
const [more, setMore] = useState(false) // 窄的时候收起来的几个按钮
|
|
3566
5090
|
const [hist, setHist] = useState(false) // 版本面板
|
|
@@ -3577,6 +5101,10 @@ window.__ModuleLoader__.load({
|
|
|
3577
5101
|
const [reveal, setReveal] = useState(null) // 画布上要露出来的那一屏 { key, n }(体检里点了一条)
|
|
3578
5102
|
const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
|
|
3579
5103
|
const [pane, setPane] = useState('') // 右边的面板:tweaks 调节滑块、systems 品牌设计系统、handoff 转成代码
|
|
5104
|
+
const [mark, setMark] = useState(false) // 画笔标注
|
|
5105
|
+
const [markBusy, setMarkBusy] = useState(false) // 正在把标注拼成图
|
|
5106
|
+
const [markSize, setMarkSize] = useState(null) // 开标注那一刻台面的大小:单页看网页时按它定住页面,台面变了整体缩放、不重排
|
|
5107
|
+
const markRun = useRef(0) // 每开 / 关一次标注加一:发送途中关掉(或关了又开)的,旧的那次作废
|
|
3580
5108
|
const [tw, setTw] = useState({}) // 拖过的滑块:id → 值(还没写进文件)
|
|
3581
5109
|
const apis = useRef(new Map()) // 屏 → 截图用的接口
|
|
3582
5110
|
const cmpRef = useRef(null)
|
|
@@ -3693,6 +5221,36 @@ window.__ModuleLoader__.load({
|
|
|
3693
5221
|
toastTimer.current = setTimeout(() => setToast(''), act ? 6000 : 2600)
|
|
3694
5222
|
}
|
|
3695
5223
|
|
|
5224
|
+
// ── 打开本地的 HTML:「浏览…」选一个,或者把文件 / 文件夹拖进来(桌面版才拿得到路径)──
|
|
5225
|
+
const openLocal = async (p) => {
|
|
5226
|
+
if (!p) return say(t('pathNone'))
|
|
5227
|
+
try {
|
|
5228
|
+
await openTarget(sid, p)
|
|
5229
|
+
setEditing(false)
|
|
5230
|
+
} catch (e) {
|
|
5231
|
+
say(t('openFailed', { msg: e.message }))
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
const browse = hostPaths() ? () => { if (pageInput.current) pageInput.current.click() } : null
|
|
5235
|
+
// 拖着文件在预览页面里:iframe 把拖动吃掉了(单页铺满时外面收不到 dragenter),页面转告过来。
|
|
5236
|
+
// 蒙层只当提示、不挡事件(不然页面自己的上传区就收不到后面的拖动了);松手也是页面接住、把文件转过来
|
|
5237
|
+
const pageDrag = (d) => {
|
|
5238
|
+
clearTimeout(softTimer.current)
|
|
5239
|
+
if (d.type === 'dragIn') {
|
|
5240
|
+
const list = (Array.isArray(d.items) ? d.items : []).slice(0, 50).filter((x) => x && typeof x.kind === 'string' && typeof x.type === 'string')
|
|
5241
|
+
setDropSoft(true)
|
|
5242
|
+
setDropping(dragKind({ items: list }))
|
|
5243
|
+
// 拖着时页面隔一会儿就报一次;一阵没报(拖出去了、按了 Esc)就收起
|
|
5244
|
+
softTimer.current = setTimeout(() => setDropping(''), 900)
|
|
5245
|
+
return
|
|
5246
|
+
}
|
|
5247
|
+
setDropping('')
|
|
5248
|
+
if (d.type !== 'dropIn') return // dragOut:到了页面自己的上传区
|
|
5249
|
+
const files = (Array.isArray(d.files) ? d.files : []).slice(0, 20).filter((x) => x && x.f && typeof x.f.name === 'string')
|
|
5250
|
+
onDropped({ items: files.map((x) => ({ kind: 'file', getAsFile: () => x.f, webkitGetAsEntry: () => ({ isDirectory: !!x.dir }) })) })
|
|
5251
|
+
}
|
|
5252
|
+
useEffect(() => () => clearTimeout(softTimer.current), [])
|
|
5253
|
+
|
|
3696
5254
|
const showFlash = (text) => {
|
|
3697
5255
|
setFlash(text)
|
|
3698
5256
|
clearTimeout(flashTimer.current)
|
|
@@ -3760,7 +5318,27 @@ window.__ModuleLoader__.load({
|
|
|
3760
5318
|
return () => { dead = true }
|
|
3761
5319
|
}, [hist, sid, vrev, origin])
|
|
3762
5320
|
|
|
3763
|
-
|
|
5321
|
+
// 新版 DSH:我们的整段在输入框里放成一个短标签(悬停看要点,发出去模型拿到整段)。放进去了返回 true
|
|
5322
|
+
const chipIn = (text, done) => {
|
|
5323
|
+
const s = String(text).trim()
|
|
5324
|
+
const c = chipOf(s, t)
|
|
5325
|
+
const ia = props.inputActions
|
|
5326
|
+
if (!c || !chips || !ia || typeof ia.captureInsertion !== 'function') return false
|
|
5327
|
+
try {
|
|
5328
|
+
if (!chips.insert(sid, s, c.label, ia.captureInsertion(), chipSrc(c.i))) return false
|
|
5329
|
+
} catch (e) {
|
|
5330
|
+
console.warn('[dsh-design-studio] 插标签失败', e)
|
|
5331
|
+
return false
|
|
5332
|
+
}
|
|
5333
|
+
// 先把焦点从预览页拿回输入框,再让编辑器把光标放回标签后面(只 focus 元素,光标会跑到最前面)
|
|
5334
|
+
try { const el = composerEl(); if (el) { el.focus({ preventScroll: true }); const ed = el.__lexicalEditor; if (ed && typeof ed.focus === 'function') ed.focus() } } catch { /* 用户自己点一下输入框也行 */ }
|
|
5335
|
+
say(t(done))
|
|
5336
|
+
return true
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
// copy:插不进输入框时退回复制到剪贴板(画笔标注不要:别动用户的剪贴板)。插进去了返回 true
|
|
5340
|
+
const insert = (text, done = 'inserted', copy = true) => {
|
|
5341
|
+
if (chipIn(text, done)) return true
|
|
3764
5342
|
const ia = props.inputActions
|
|
3765
5343
|
try {
|
|
3766
5344
|
if (ia && typeof ia.insertText === 'function') {
|
|
@@ -3772,17 +5350,20 @@ window.__ModuleLoader__.load({
|
|
|
3772
5350
|
const p = padInsert(text, ok ? all.slice(0, at) : null, ok ? all.slice(span.end) : null, at, done === 'inserted')
|
|
3773
5351
|
if (ia.insertText(p.text, span)) {
|
|
3774
5352
|
try { focusAfter(el, p.text, p.back) } catch { /* 用户自己点一下输入框也行 */ }
|
|
3775
|
-
|
|
5353
|
+
say(t(done))
|
|
5354
|
+
return true
|
|
3776
5355
|
}
|
|
3777
5356
|
}
|
|
3778
5357
|
} catch (e) {
|
|
3779
5358
|
console.warn('[dsh-design-studio] insertText 失败', e)
|
|
3780
5359
|
}
|
|
5360
|
+
if (!copy) return false
|
|
3781
5361
|
if (navigator.clipboard) {
|
|
3782
5362
|
navigator.clipboard.writeText(text).then(() => say(t('copied')), (e) => say(t('failed', { msg: e.message })))
|
|
3783
5363
|
} else say(t('failed', { msg: 'no input' }))
|
|
3784
5364
|
}
|
|
3785
5365
|
|
|
5366
|
+
// 点选的元素 / 区域:新版 DSH 放成标签,旧版照旧插成字
|
|
3786
5367
|
const deliver = (payload, note) => {
|
|
3787
5368
|
setPicking(false)
|
|
3788
5369
|
insert(formatPick(payload, stRef.current) + (note || ''))
|
|
@@ -3875,7 +5456,8 @@ window.__ModuleLoader__.load({
|
|
|
3875
5456
|
|
|
3876
5457
|
useEffect(() => {
|
|
3877
5458
|
if (!dropping) return undefined
|
|
3878
|
-
|
|
5459
|
+
// 晚一拍再收蒙层:真拖放时这个捕获监听先跑,马上收的话 React 还没把 drop 交给蒙层,蒙层就没了(松手什么都不发生)
|
|
5460
|
+
const off = () => setTimeout(() => setDropping(''), 0)
|
|
3879
5461
|
window.addEventListener('dragend', off, true)
|
|
3880
5462
|
window.addEventListener('drop', off, true)
|
|
3881
5463
|
return () => { window.removeEventListener('dragend', off, true); window.removeEventListener('drop', off, true) }
|
|
@@ -3958,8 +5540,8 @@ window.__ModuleLoader__.load({
|
|
|
3958
5540
|
else change({ view: 'single' })
|
|
3959
5541
|
}
|
|
3960
5542
|
|
|
3961
|
-
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)] },
|
|
3962
|
-
h(Screen, { t, remote,
|
|
5543
|
+
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)], os: meta.os },
|
|
5544
|
+
h(Screen, { t, remote, onPageDrag: pageDrag,
|
|
3963
5545
|
src: sc.url,
|
|
3964
5546
|
label: labelOf(sc),
|
|
3965
5547
|
busy: busy && !sc.v,
|
|
@@ -4058,6 +5640,7 @@ window.__ModuleLoader__.load({
|
|
|
4058
5640
|
kind,
|
|
4059
5641
|
size,
|
|
4060
5642
|
shots: shots.map((x, i) => ({ img: x.img, top: x.top, label: sp.labels[i] || x.title })),
|
|
5643
|
+
os: meta.os,
|
|
4061
5644
|
title: sp.brand || (st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : st.label) || '',
|
|
4062
5645
|
sub: t('showSub', { n: shots.length, kind: t(KIND_KEY[kind]), date: ymd(new Date()) }),
|
|
4063
5646
|
bg: colors.bg,
|
|
@@ -4072,7 +5655,7 @@ window.__ModuleLoader__.load({
|
|
|
4072
5655
|
if (!sc) throw new Error('no screen')
|
|
4073
5656
|
const base = prefix + sc.key.replace(/\.html?$/i, '') + suffix
|
|
4074
5657
|
const { img, top } = await captureOf(sc, mode === 'long')
|
|
4075
|
-
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top))
|
|
5658
|
+
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top, meta.os))
|
|
4076
5659
|
else {
|
|
4077
5660
|
const c = document.createElement('canvas')
|
|
4078
5661
|
c.width = img.width
|
|
@@ -4233,6 +5816,208 @@ window.__ModuleLoader__.load({
|
|
|
4233
5816
|
}
|
|
4234
5817
|
const checkOff = !url || !!ver || !!demo || inMulti
|
|
4235
5818
|
const styleOff = !url || !!ver || !!demo || inMulti || busy
|
|
5819
|
+
|
|
5820
|
+
// ── 画笔标注:画完把台面上看得到的几屏各截一张,按位置摆好,叠上画的东西,存进 refs/,路径放进输入框让模型用 read_image 看(不动剪贴板) ──
|
|
5821
|
+
const markOff = !url || !!ver || !!demo || inMulti
|
|
5822
|
+
const toggleMark = () => {
|
|
5823
|
+
markRun.current++
|
|
5824
|
+
if (mark) { setMark(false); return }
|
|
5825
|
+
setPicking(false)
|
|
5826
|
+
setTextEdit(false)
|
|
5827
|
+
setStyleEdit(false)
|
|
5828
|
+
setPanel(false)
|
|
5829
|
+
setStyles(false)
|
|
5830
|
+
setHist(false)
|
|
5831
|
+
setPhone(false)
|
|
5832
|
+
closeCheck()
|
|
5833
|
+
setPane('')
|
|
5834
|
+
// 之前点选 / 调样式留下的「已选」框别夹在标注里
|
|
5835
|
+
for (const a of apis.current.values()) if (a && a.unpin) a.unpin()
|
|
5836
|
+
setMarkSize(box.w && box.h ? { w: box.w, h: box.h } : null)
|
|
5837
|
+
setMark(true)
|
|
5838
|
+
}
|
|
5839
|
+
// 换了设计、看旧版本、演示:画的东西对不上了,收掉
|
|
5840
|
+
useEffect(() => { markRun.current++; setMark(false) }, [url, markOff])
|
|
5841
|
+
// 台面上现在看得到的每一屏:页面 iframe 的位置(台面坐标,画布缩放过的也对),一点没露出来的不要
|
|
5842
|
+
const markFrames = () => {
|
|
5843
|
+
const stage = stageRef.current
|
|
5844
|
+
if (!stage) return []
|
|
5845
|
+
const sr = stage.getBoundingClientRect()
|
|
5846
|
+
const out = []
|
|
5847
|
+
for (const sc of shown) {
|
|
5848
|
+
const api = apis.current.get(ik(sc))
|
|
5849
|
+
const f = api && typeof api.frame === 'function' ? api.frame() : null
|
|
5850
|
+
if (!f) continue
|
|
5851
|
+
const r = f.getBoundingClientRect()
|
|
5852
|
+
if (r.width < 8 || r.height < 8 || r.right <= sr.left || r.left >= sr.right || r.bottom <= sr.top || r.top >= sr.bottom) continue
|
|
5853
|
+
if (typeof f.checkVisibility === 'function' && !f.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue
|
|
5854
|
+
out.push({ key: ik(sc), sc, api, f, l: r.left - sr.left, t: r.top - sr.top, w: r.width, h: r.height, z: r.width / Math.max(1, f.offsetWidth) })
|
|
5855
|
+
}
|
|
5856
|
+
return out
|
|
5857
|
+
}
|
|
5858
|
+
const MARK_SHOTS = 40
|
|
5859
|
+
// 截到的是哪一页(只用来起名、写进提示):本地文件按文件名;开发服务器按路由(/account);单页应用的 #/account 路由带上
|
|
5860
|
+
const markPageOf = (sc, href) => {
|
|
5861
|
+
let key = pageOf(sc, href).key
|
|
5862
|
+
try {
|
|
5863
|
+
const u = new URL(href)
|
|
5864
|
+
if (u.origin !== new URL(sc.url).origin) return key
|
|
5865
|
+
if (key === sc.key && st && st.target && st.target.kind === 'proxy') key = decodeURIComponent(u.pathname) || sc.key
|
|
5866
|
+
if (/^#!?\//.test(u.hash)) key += decodeURIComponent(u.hash)
|
|
5867
|
+
} catch { /* 地址不对 */ }
|
|
5868
|
+
return key
|
|
5869
|
+
}
|
|
5870
|
+
// 把画的拼成一张图:画到的那几屏按现在的位置截图,叠上画的(交给模型、复制共用)
|
|
5871
|
+
const buildMark = async (list, stage) => {
|
|
5872
|
+
const sr = stage.getBoundingClientRect()
|
|
5873
|
+
const all = markFrames().filter((x) => x.api.ready())
|
|
5874
|
+
const ov = (a, b) => a.l < b.r && a.r > b.l && a.t < b.b && a.b > b.t
|
|
5875
|
+
const boxOf = (x) => ({ l: x.l, t: x.t, r: x.l + x.w, b: x.t + x.h })
|
|
5876
|
+
// 画到的那几屏(一屏都没碰到:画在屏外空白处的,就把看得到的都拼上)
|
|
5877
|
+
const marks = list.map(markBox)
|
|
5878
|
+
let shots = all.filter((x) => marks.some((m) => ov(boxOf(x), m)))
|
|
5879
|
+
if (!shots.length) shots = all
|
|
5880
|
+
// 裁到有东西的那块(画到的屏 + 画的),四周留一点;落在这块里的别的屏也拼上,免得图上空一块
|
|
5881
|
+
const pad = 16
|
|
5882
|
+
const span = (bs) => ({
|
|
5883
|
+
l: Math.max(0, Math.min(...bs.map((b) => b.l)) - pad),
|
|
5884
|
+
t: Math.max(0, Math.min(...bs.map((b) => b.t)) - pad),
|
|
5885
|
+
r: Math.min(sr.width, Math.max(...bs.map((b) => b.r)) + pad),
|
|
5886
|
+
b: Math.min(sr.height, Math.max(...bs.map((b) => b.b)) + pad),
|
|
5887
|
+
})
|
|
5888
|
+
const crop = span([...shots.map(boxOf), ...marks])
|
|
5889
|
+
shots = all.filter((x) => shots.includes(x) || ov(boxOf(x), crop))
|
|
5890
|
+
const { l: cl, t: ct, r: cr, b: cb } = crop
|
|
5891
|
+
if (!(cr - cl >= 1 && cb - ct >= 1)) throw new Error('empty')
|
|
5892
|
+
// 清晰度:至少按屏幕像素;画布缩小着看的,按页面原来的大小截(最多 4 倍),整张不超过 4096
|
|
5893
|
+
const zMin = Math.min(1, ...shots.map((x) => x.z))
|
|
5894
|
+
const k = Math.min(4096 / Math.max(cr - cl, cb - ct), Math.max(Math.min(2, Math.max(1, window.devicePixelRatio || 1)), Math.min(4, 1 / Math.max(0.05, zMin))))
|
|
5895
|
+
const c = document.createElement('canvas')
|
|
5896
|
+
c.width = Math.max(1, Math.round((cr - cl) * k))
|
|
5897
|
+
c.height = Math.max(1, Math.round((cb - ct) * k))
|
|
5898
|
+
const ctx = c.getContext('2d')
|
|
5899
|
+
let bg = ''
|
|
5900
|
+
for (let el = stage; el && !bg; el = el.parentElement) {
|
|
5901
|
+
const v = getComputedStyle(el).backgroundColor
|
|
5902
|
+
if (v && !/^(transparent|rgba\(.*,\s*0\))$/.test(v)) bg = v
|
|
5903
|
+
}
|
|
5904
|
+
ctx.fillStyle = bg || '#f4f4f5'
|
|
5905
|
+
ctx.fillRect(0, 0, c.width, c.height)
|
|
5906
|
+
if (shots.length > MARK_SHOTS) say(t('mkMany', { n: MARK_SHOTS }))
|
|
5907
|
+
const names = []
|
|
5908
|
+
for (const [i, x] of shots.entries()) {
|
|
5909
|
+
const X = (x.l - cl) * k
|
|
5910
|
+
const Y = (x.t - ct) * k
|
|
5911
|
+
// 手机框的圆角:往上找一层裁圆角的
|
|
5912
|
+
let rad = 0
|
|
5913
|
+
for (let el = x.f.parentElement, j = 0; el && el !== stage && j < 4 && !rad; el = el.parentElement, j++) {
|
|
5914
|
+
const cs = getComputedStyle(el)
|
|
5915
|
+
const v = parseFloat(cs.borderTopLeftRadius) || 0
|
|
5916
|
+
if (v && cs.overflow !== 'visible' && el.offsetWidth) rad = v * (el.getBoundingClientRect().width / el.offsetWidth)
|
|
5917
|
+
}
|
|
5918
|
+
ctx.save()
|
|
5919
|
+
ctx.beginPath()
|
|
5920
|
+
if (rad && typeof ctx.roundRect === 'function') ctx.roundRect(X, Y, x.w * k, x.h * k, rad * k)
|
|
5921
|
+
else ctx.rect(X, Y, x.w * k, x.h * k)
|
|
5922
|
+
ctx.clip()
|
|
5923
|
+
let name = x.sc.key
|
|
5924
|
+
try {
|
|
5925
|
+
if (i >= MARK_SHOTS) throw new Error('too many')
|
|
5926
|
+
const cap = await x.api.capture({ full: false, scale: Math.min(3, Math.max(1, x.z * k)) })
|
|
5927
|
+
// 页面里点链接跳到别的页了:按真正截到的那页起名
|
|
5928
|
+
if (cap.href) name = markPageOf(x.sc, cap.href)
|
|
5929
|
+
const img = await loadImg(cap.png)
|
|
5930
|
+
ctx.fillStyle = '#fff'
|
|
5931
|
+
ctx.fillRect(X, Y, x.w * k, x.h * k)
|
|
5932
|
+
ctx.drawImage(img, X, Y, cap.w * x.z * k, cap.h * x.z * k)
|
|
5933
|
+
} catch (e) {
|
|
5934
|
+
// 这一屏截不下来:留个灰块,画的东西还在
|
|
5935
|
+
if (i < MARK_SHOTS) console.warn('[dsh-design-studio] 标注截图失败', x.sc.key, e)
|
|
5936
|
+
ctx.fillStyle = 'rgba(127,127,127,.18)'
|
|
5937
|
+
ctx.fillRect(X, Y, x.w * k, x.h * k)
|
|
5938
|
+
}
|
|
5939
|
+
ctx.restore()
|
|
5940
|
+
if (!names.includes(name)) names.push(name)
|
|
5941
|
+
}
|
|
5942
|
+
for (const s of list) drawMark(ctx, s, k, cl, ct)
|
|
5943
|
+
return { c, names, blob: await canvasBlob(c) }
|
|
5944
|
+
}
|
|
5945
|
+
const sendMark = async (list) => {
|
|
5946
|
+
if (markBusy) return
|
|
5947
|
+
if (!list.length) { say(t('mkEmpty')); return }
|
|
5948
|
+
const stage = stageRef.current
|
|
5949
|
+
if (!stage) return
|
|
5950
|
+
const url0 = url
|
|
5951
|
+
const run0 = markRun.current
|
|
5952
|
+
const stale = () => stRef.current.url !== url0 || markRun.current !== run0
|
|
5953
|
+
const root0 = st && st.target && st.target.kind === 'file' ? st.target.root : null
|
|
5954
|
+
setMarkBusy(true)
|
|
5955
|
+
try {
|
|
5956
|
+
const { c, names, blob } = await buildMark(list, stage)
|
|
5957
|
+
// 拼图的时候换了设计、关了标注:这张图对不上了,别交
|
|
5958
|
+
if (stale()) { if (stRef.current.url !== url0) say(t('mkMoved')); return }
|
|
5959
|
+
if (!names.length && curScreen) names.push(curScreen.key)
|
|
5960
|
+
const stem = String(names[0] || 'page').replace(/\.html?(?=$|#)/i, '').split(/[/#!]+/).filter(Boolean).pop() || 'page'
|
|
5961
|
+
const body = { session: sid, name: `markup-${stem}`, data: await blobB64(blob) }
|
|
5962
|
+
if (root0 !== null) body.root = root0
|
|
5963
|
+
let saved
|
|
5964
|
+
try { saved = await post('ref', body) } catch (e) {
|
|
5965
|
+
if (/moved/.test(e.message)) { say(t('mkMoved')); return }
|
|
5966
|
+
throw e
|
|
5967
|
+
}
|
|
5968
|
+
if (stale()) { if (stRef.current.url !== url0) say(t('mkMoved')); return }
|
|
5969
|
+
keepThumb(saved.path, c)
|
|
5970
|
+
const where = (names.length ? names : [st.label || '']).join(t('sep'))
|
|
5971
|
+
// 写在图上的字也用文字带上:模型看图看漏了、或者用的模型看不了图,也知道要改什么
|
|
5972
|
+
const notes = list.filter((x) => x.t === 'text').map((x) => t('mkQuote', { v: x.v.replace(/\s*\n\s*/g, ' ') })).join(t('sep'))
|
|
5973
|
+
// 插不进输入框:标注层留着(画的还在),提示再点一次;不退回剪贴板
|
|
5974
|
+
if (insert(t('mkAsk', { path: saved.path, where, notes: notes ? t('mkNotes', { notes }) : '' }), 'mkIn', false)) setMark(false)
|
|
5975
|
+
else say(t('mkNoInput'))
|
|
5976
|
+
} catch (e) {
|
|
5977
|
+
say(t('mkFail', { msg: e.message }))
|
|
5978
|
+
} finally {
|
|
5979
|
+
setMarkBusy(false)
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
// 复制:同一张图放进剪贴板(issue #6)。点按钮这一下就把「等图拼好」交给剪贴板——拼完再写,
|
|
5983
|
+
// Safari 这类会说不是用户点的、不让写;不认这种写法的再用拼好的图写一次
|
|
5984
|
+
const copyMark = (list) => {
|
|
5985
|
+
if (markBusy) return
|
|
5986
|
+
if (!list.length) { say(t('mkEmpty')); return }
|
|
5987
|
+
const stage = stageRef.current
|
|
5988
|
+
if (!stage) return
|
|
5989
|
+
const url0 = url
|
|
5990
|
+
const run0 = markRun.current
|
|
5991
|
+
const stale = () => stRef.current.url !== url0 || markRun.current !== run0
|
|
5992
|
+
const STALE = 'ddp-mark-stale'
|
|
5993
|
+
setMarkBusy(true)
|
|
5994
|
+
const made = buildMark(list, stage).then((r) => { if (stale()) throw new Error(STALE); return r })
|
|
5995
|
+
const png = made.then((r) => r.blob)
|
|
5996
|
+
png.catch(() => {}) // 结果在下面看
|
|
5997
|
+
let wrote
|
|
5998
|
+
try { wrote = navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]) } catch (e) { wrote = Promise.reject(e) }
|
|
5999
|
+
wrote.catch(() => {})
|
|
6000
|
+
;(async () => {
|
|
6001
|
+
try {
|
|
6002
|
+
const r = await made
|
|
6003
|
+
if (!(await wrote.then(() => true, () => false))) {
|
|
6004
|
+
// 等写的工夫里取消了、换了设计:别再把这张旧图写进去
|
|
6005
|
+
if (stale()) { if (stRef.current.url !== url0) say(t('mkMoved')); return }
|
|
6006
|
+
if (!(await copyImage(r.blob))) { say(t('mkCopyFail')); return }
|
|
6007
|
+
}
|
|
6008
|
+
if (stale()) return
|
|
6009
|
+
say(t('mkCopied'))
|
|
6010
|
+
setMark(false)
|
|
6011
|
+
} catch (e) {
|
|
6012
|
+
if (e && e.message === STALE) { if (stRef.current.url !== url0) say(t('mkMoved')) } else {
|
|
6013
|
+
console.warn('[dsh-design-studio] 标注图复制失败', e)
|
|
6014
|
+
say(t('mkCopyFail'))
|
|
6015
|
+
}
|
|
6016
|
+
} finally {
|
|
6017
|
+
setMarkBusy(false)
|
|
6018
|
+
}
|
|
6019
|
+
})()
|
|
6020
|
+
}
|
|
4236
6021
|
useEffect(() => { if (styleOff) setStyleEdit(false) }, [styleOff])
|
|
4237
6022
|
autoCk.current = { ck, off: checkOff, run: runCheck, list: checkList, measure }
|
|
4238
6023
|
useEffect(() => { if (checkOff && ck) closeCheck() }, [checkOff])
|
|
@@ -4275,23 +6060,24 @@ window.__ModuleLoader__.load({
|
|
|
4275
6060
|
const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
|
|
4276
6061
|
const reload = () => setReloadSeq((x) => x + 1)
|
|
4277
6062
|
|
|
4278
|
-
const bar = h('div', { className: 'ddp-bar' },
|
|
6063
|
+
const bar = h('div', { className: 'ddp-bar' + (editing ? ' editing' : '') },
|
|
4279
6064
|
editing || !url
|
|
4280
|
-
? 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) })
|
|
6065
|
+
? 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 })
|
|
4281
6066
|
: h('div', { className: 'ddp-addr', title: t('editAddr'), onClick: () => setEditing(true) },
|
|
4282
6067
|
h('i', { className: 'ddp-dot' + (busy ? ' wait' : '') }),
|
|
4283
6068
|
h('span', null, canvas && screens.length > 1 ? `${st.label} · ${screens.length}` : st.label)),
|
|
4284
|
-
h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo, onClick: togglePick },
|
|
6069
|
+
h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo || mark, onClick: togglePick },
|
|
4285
6070
|
h(I.pick), h('span', null, picking ? t('picking') : t('pick'))),
|
|
4286
|
-
h('button', { className: 'ddp-btn' + (textEdit ? ' on' : ''), title: t('textEdit'), disabled: !url || !!ver || !!demo || busy, onClick: toggleText }, h(I.text)),
|
|
4287
|
-
h('button', { className: 'ddp-btn' + (styleEdit ? ' on' : ''), title: t('styleEdit'), disabled: styleOff, onClick: toggleStyle }, h(I.sliders)),
|
|
6071
|
+
h('button', { className: 'ddp-btn' + (textEdit ? ' on' : ''), title: t('textEdit'), disabled: !url || !!ver || !!demo || busy || mark, onClick: toggleText }, h(I.text)),
|
|
6072
|
+
h('button', { className: 'ddp-btn' + (styleEdit ? ' on' : ''), title: t('styleEdit'), disabled: styleOff || mark, onClick: toggleStyle }, h(I.sliders)),
|
|
6073
|
+
h('button', { className: 'ddp-btn' + (mark ? ' on' : ''), title: t('mark'), 'aria-pressed': mark, disabled: markOff, onClick: toggleMark }, h(I.pen)),
|
|
4288
6074
|
h('span', { className: 'ddp-sep' }),
|
|
4289
6075
|
h('div', { className: 'ddp-seg' },
|
|
4290
|
-
h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
|
|
4291
|
-
h('button', { className: 'ddp-btn' + (!canvas && !inMulti ? ' sel' : ''), title: t('single'), onClick: () => { setMulti(false); openSingle() } }, h(I.single)),
|
|
4292
|
-
h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
|
|
4293
|
-
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver, onClick: toggleDemo }, h(I.play)),
|
|
4294
|
-
h(KindMenu, { t, kind, open: menu, setOpen: setMenu, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
|
|
6076
|
+
h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), disabled: mark, onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
|
|
6077
|
+
h('button', { className: 'ddp-btn' + (!canvas && !inMulti ? ' sel' : ''), title: t('single'), disabled: mark, onClick: () => { setMulti(false); openSingle() } }, h(I.single)),
|
|
6078
|
+
h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver || mark, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
|
|
6079
|
+
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver || mark, onClick: toggleDemo }, h(I.play)),
|
|
6080
|
+
h(KindMenu, { t, kind, open: menu, setOpen: setMenu, disabled: mark, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
|
|
4295
6081
|
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)),
|
|
4296
6082
|
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)),
|
|
4297
6083
|
h('button', { className: 'ddp-btn o2' + (ck ? ' sel' : ''), title: ckCount && !ck ? t('checkCount', { n: ckCount }) : t('checkTip'), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
|
|
@@ -4304,7 +6090,7 @@ window.__ModuleLoader__.load({
|
|
|
4304
6090
|
h('button', { className: 'ddp-btn o1', title: t('refresh'), onClick: reload }, h(I.refresh)),
|
|
4305
6091
|
h('a', { className: 'ddp-btn o1', title: t('external'), href: url, target: '_blank', rel: 'noopener noreferrer' }, h(I.external)),
|
|
4306
6092
|
h(MoreMenu, { t, open: more, setOpen: setMore, has: (phoneOn && !phone) || (!!ckCount && !ck && !checkOff), items: [
|
|
4307
|
-
['o2', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo],
|
|
6093
|
+
['o2', I.play, t('demoShort'), !!demo, !url || !!ver || mark, toggleDemo],
|
|
4308
6094
|
['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
|
|
4309
6095
|
['o2', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
|
|
4310
6096
|
['o2', I.dial, t('tweaks'), pane === 'tweaks', !fileTarget || !!ver, () => openPane('tweaks')],
|
|
@@ -4328,6 +6114,7 @@ window.__ModuleLoader__.load({
|
|
|
4328
6114
|
try {
|
|
4329
6115
|
const r = await readRef(file)
|
|
4330
6116
|
const saved = await post('ref', { session: sid, name: file.name || 'ref', data: r.data })
|
|
6117
|
+
keepThumb(saved.path, r.img)
|
|
4331
6118
|
insert(formatRef(t, { path: saved.path, w: r.w, h: r.h, palette: r.palette, hasDesign: !!url }), 'insertedSend')
|
|
4332
6119
|
} catch (e) {
|
|
4333
6120
|
say(t('refFail', { msg: e.message }))
|
|
@@ -4336,11 +6123,30 @@ window.__ModuleLoader__.load({
|
|
|
4336
6123
|
const imageOf = (list) => [...(list || [])].find((f) => f && /^image\//.test(f.type))
|
|
4337
6124
|
const hasFiles = (e) => [...((e.dataTransfer && e.dataTransfer.types) || [])].includes('Files')
|
|
4338
6125
|
const pickRef = () => { if (refInput.current) refInput.current.click() }
|
|
6126
|
+
|
|
6127
|
+
const onDropped = (dt) => {
|
|
6128
|
+
const d = droppedOf(dt)
|
|
6129
|
+
if (d.page) return hostPaths() ? openLocal(pathOf(d.page)) : say(t('dropWebSay'))
|
|
6130
|
+
if (d.image) return addRef(d.image)
|
|
6131
|
+
return say(d.any ? t(hostPaths() ? 'dropWhat' : 'dropWhatWeb') : t('refNotImage'))
|
|
6132
|
+
}
|
|
6133
|
+
|
|
4339
6134
|
const rootProps = {
|
|
4340
6135
|
className: 'ddp',
|
|
4341
6136
|
tabIndex: -1, // 点了右侧栏就能在这里粘贴图片
|
|
4342
|
-
|
|
4343
|
-
|
|
6137
|
+
// 拖动在右侧栏里时别让 DSH 看到:它在整页上数进出、松手就把文件加进对话框——
|
|
6138
|
+
// 不拦的话它的「拖到这里添加」会盖住右侧栏,我们接住松手后它的蒙层还收不回去
|
|
6139
|
+
onDragEnter: (e) => { if (hasFiles(e)) { e.preventDefault(); e.stopPropagation(); clearTimeout(softTimer.current); setDropSoft(false); setDropping(dragKind(e.dataTransfer)) } },
|
|
6140
|
+
onDragOver: (e) => { if (hasFiles(e)) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy' } },
|
|
6141
|
+
onDragLeave: (e) => { if (hasFiles(e)) e.stopPropagation() },
|
|
6142
|
+
onDrop: (e) => {
|
|
6143
|
+
if (!hasFiles(e)) return
|
|
6144
|
+
// 平时是蒙层接住;蒙层刚好不在(比如鼠标动了一下收掉了)也别让文件掉进对话框
|
|
6145
|
+
e.preventDefault()
|
|
6146
|
+
e.stopPropagation()
|
|
6147
|
+
setDropping('')
|
|
6148
|
+
onDropped(e.dataTransfer)
|
|
6149
|
+
},
|
|
4344
6150
|
onPaste: (e) => {
|
|
4345
6151
|
const f = imageOf(e.clipboardData && e.clipboardData.files)
|
|
4346
6152
|
if (!f) return
|
|
@@ -4348,26 +6154,42 @@ window.__ModuleLoader__.load({
|
|
|
4348
6154
|
addRef(f)
|
|
4349
6155
|
},
|
|
4350
6156
|
}
|
|
4351
|
-
// 盖住整个右侧栏接住松手(不然落在页面 iframe
|
|
6157
|
+
// 盖住整个右侧栏接住松手(不然落在页面 iframe 上就被它吃掉了);拖动在预览页面里时只是提示(soft),事件照样给页面
|
|
4352
6158
|
const dropLayer = dropping ? h('div', {
|
|
4353
|
-
|
|
6159
|
+
key: 'drop',
|
|
6160
|
+
className: 'ddp-drop' + (dropSoft ? ' soft' : ''),
|
|
6161
|
+
// 拖放中不会有 mousemove:鼠标一动就说明拖已经结束了(没收到 drop / dragleave 时别让蒙层一直盖着)
|
|
6162
|
+
onMouseMove: () => setDropping(''),
|
|
4354
6163
|
onDragOver: (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy' },
|
|
4355
|
-
onDragLeave: (e) => { if (e.target === e.currentTarget) setDropping(
|
|
6164
|
+
onDragLeave: (e) => { if (e.target === e.currentTarget) setDropping('') },
|
|
4356
6165
|
onDrop: (e) => {
|
|
4357
6166
|
e.preventDefault()
|
|
4358
6167
|
e.stopPropagation()
|
|
4359
|
-
setDropping(
|
|
4360
|
-
|
|
6168
|
+
setDropping('')
|
|
6169
|
+
onDropped(e.dataTransfer)
|
|
4361
6170
|
},
|
|
4362
|
-
}, h('div', { className: 'in'
|
|
6171
|
+
}, h('div', { className: 'in' + (dropping === 'web' || dropping === 'other' ? ' no' : '') },
|
|
6172
|
+
...(dropping === 'page' || dropping === 'maybe' ? [h(I.page), h('b', null, t(dropping === 'page' ? 'pageDrop' : 'maybeDrop')), h('span', null, t('pageDropSub'))]
|
|
6173
|
+
: dropping === 'web' ? [h(I.page), h('b', null, t('dropWeb')), h('span', null, t('dropWebSub'))]
|
|
6174
|
+
: dropping === 'other' ? (hostPaths() ? [h(I.page), h('b', null, t('dropWhat')), h('span', null, t('dropWhatSub'))] : [h(I.page), h('b', null, t('dropWhatWeb')), h('span', null, t('dropWhatWebSub'))])
|
|
6175
|
+
: [h(I.image), h('b', null, t('refDrop')), h('span', null, t('refDropSub'))]))) : null
|
|
4363
6176
|
const fileInput = h('input', {
|
|
6177
|
+
key: 'refFile',
|
|
4364
6178
|
ref: refInput,
|
|
4365
6179
|
type: 'file',
|
|
4366
6180
|
accept: 'image/png,image/jpeg,image/webp,image/gif',
|
|
4367
6181
|
style: { display: 'none' },
|
|
4368
6182
|
onChange: (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) addRef(f) },
|
|
4369
6183
|
})
|
|
4370
|
-
const
|
|
6184
|
+
const pageFileInput = h('input', {
|
|
6185
|
+
key: 'pageFile',
|
|
6186
|
+
ref: pageInput,
|
|
6187
|
+
type: 'file',
|
|
6188
|
+
accept: '.html,.htm,text/html',
|
|
6189
|
+
style: { display: 'none' },
|
|
6190
|
+
onChange: (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) openLocal(pathOf(f)) },
|
|
6191
|
+
})
|
|
6192
|
+
const root = (...kids) => h('div', rootProps, ...kids, dropLayer, fileInput, pageFileInput)
|
|
4371
6193
|
|
|
4372
6194
|
// 调节滑块 / 品牌设计系统 / 转成代码(右边的面板)
|
|
4373
6195
|
const sidePane = () => {
|
|
@@ -4397,13 +6219,14 @@ window.__ModuleLoader__.load({
|
|
|
4397
6219
|
})
|
|
4398
6220
|
}
|
|
4399
6221
|
if (pane === 'handoff') {
|
|
4400
|
-
return h(HandoffPanel, { t, sid, root: fileTarget ? st.target.root : '', fileTarget, count: screens.length, cur: (curScreen && curScreen.key) || focus, onGo: doHandoff, onClose: () => setPane('') })
|
|
6222
|
+
return h(HandoffPanel, { t, sid, root: fileTarget ? st.target.root : '', fileTarget, count: screens.length, cur: (curScreen && curScreen.key) || focus, onGo: doHandoff, onSay: say, onClose: () => setPane('') })
|
|
4401
6223
|
}
|
|
4402
6224
|
return null
|
|
4403
6225
|
}
|
|
4404
6226
|
|
|
4405
6227
|
if (!url) {
|
|
4406
|
-
|
|
6228
|
+
const toastEl = toast ? h('div', { className: 'ddp-toast' }, h('span', null, toast.text)) : null
|
|
6229
|
+
if (st ? st.loading : !waited) return root(h('div', { className: 'ddp-stage', ref: stageRef }), toastEl)
|
|
4407
6230
|
if (busy) {
|
|
4408
6231
|
// 模型在做第一屏:先放一个生成中的占位
|
|
4409
6232
|
return root(
|
|
@@ -4411,7 +6234,8 @@ window.__ModuleLoader__.load({
|
|
|
4411
6234
|
h('div', { className: 'ddp-wait' }, h('div', { className: 'card' }, h(Skeleton, {})), h('p', null, t('waiting'))),
|
|
4412
6235
|
h('div', { className: 'ddp-glow' }),
|
|
4413
6236
|
h('div', { className: 'ddp-ring' }),
|
|
4414
|
-
busyChip(''))
|
|
6237
|
+
busyChip('')),
|
|
6238
|
+
toastEl)
|
|
4415
6239
|
}
|
|
4416
6240
|
return root(
|
|
4417
6241
|
h('div', { className: 'ddp-empty' },
|
|
@@ -4424,13 +6248,13 @@ window.__ModuleLoader__.load({
|
|
|
4424
6248
|
h('ol', { className: 'ddp-steps' }, h('li', null, t('step1')), h('li', null, t('step2')), h('li', null, t('step3'))),
|
|
4425
6249
|
h('div', { className: 'ddp-tries' }, h('span', null, t('tryHead')),
|
|
4426
6250
|
['try1', 'try2', 'try3'].map((k) => h('button', { key: k, className: 'ddp-try', onClick: () => insert(t(k), 'insertedSend') }, t(k)))),
|
|
4427
|
-
h('
|
|
4428
|
-
h('
|
|
4429
|
-
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
6251
|
+
h('p', null, t(browse ? 'emptyHintPick' : 'emptyHint')),
|
|
6252
|
+
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {}, onBrowse: browse }),
|
|
4430
6253
|
h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose'))),
|
|
4431
|
-
h('p', { className: 'ddp-refline' }, t('sysEmptyLine'), ' · ', h('button', { className: 'lk', onClick: () => openPane('systems') }, t('sysOpen')))
|
|
6254
|
+
h('p', { className: 'ddp-refline' }, t('sysEmptyLine'), ' · ', h('button', { className: 'lk', onClick: () => openPane('systems') }, t('sysOpen'))),
|
|
6255
|
+
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)) }))),
|
|
4432
6256
|
pane === 'systems' ? sidePane() : null,
|
|
4433
|
-
|
|
6257
|
+
toastEl)
|
|
4434
6258
|
}
|
|
4435
6259
|
|
|
4436
6260
|
let body
|
|
@@ -4446,8 +6270,8 @@ window.__ModuleLoader__.load({
|
|
|
4446
6270
|
return h('div', { key: l.id, className: cls },
|
|
4447
6271
|
h('div', { className: 'ddp-single' },
|
|
4448
6272
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4449
|
-
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key] },
|
|
4450
|
-
h(Screen, { t, remote,
|
|
6273
|
+
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key], os: meta.os },
|
|
6274
|
+
h(Screen, { t, remote, onPageDrag: pageDrag,
|
|
4451
6275
|
src: urlOfKey(l.key),
|
|
4452
6276
|
label,
|
|
4453
6277
|
busy: false,
|
|
@@ -4482,8 +6306,8 @@ window.__ModuleLoader__.load({
|
|
|
4482
6306
|
picking: picking || textEdit,
|
|
4483
6307
|
sel: '',
|
|
4484
6308
|
labelOf: (sc) => sc.label,
|
|
4485
|
-
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base), top: tops[ik(base) + '|' + i] },
|
|
4486
|
-
h(Screen, { t, remote,
|
|
6309
|
+
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base), top: tops[ik(base) + '|' + i], os: meta.os },
|
|
6310
|
+
h(Screen, { t, remote, onPageDrag: pageDrag,
|
|
4487
6311
|
src: base.url,
|
|
4488
6312
|
label: labelOf(base),
|
|
4489
6313
|
busy,
|
|
@@ -4514,12 +6338,12 @@ window.__ModuleLoader__.load({
|
|
|
4514
6338
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
4515
6339
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
4516
6340
|
const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
4517
|
-
const screenOf = (sc) => h(Screen, { t, remote, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
6341
|
+
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,
|
|
4518
6342
|
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk), onTop: (dk) => setTop(ik(sc), dk) })
|
|
4519
6343
|
body = h('div', { className: 'ddp-single' },
|
|
4520
6344
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4521
6345
|
// 胶囊那块露的是哪一边(把手拖过胶囊中间就是左边那一版),胶囊就跟哪一边的顶栏
|
|
4522
|
-
now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now), top: tops[ik(has && (cx / 100) * size[0] > size[0] - CAP.right - CAP.w / 2 ? old : now)] },
|
|
6346
|
+
now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now), top: tops[ik(has && (cx / 100) * size[0] > size[0] - CAP.right - CAP.w / 2 ? old : now)], os: meta.os },
|
|
4523
6347
|
h('div', { className: 'ddp-cmp' + (cmpDrag ? ' drag' : ''), ref: cmpRef },
|
|
4524
6348
|
h('div', { className: 'ly' }, has ? screenOf(old) : h('div', { className: 'miss' }, t('notInVer', { n: ver.n }))),
|
|
4525
6349
|
// 右边一层只露出把手右边那块:外层裁宽度,里面整屏往左挪(iframe 不变大小、不重排;clip-path 盖 iframe 有的内核画不出来)
|
|
@@ -4553,10 +6377,12 @@ window.__ModuleLoader__.load({
|
|
|
4553
6377
|
const [dw, dh] = deviceBox(kind, size)
|
|
4554
6378
|
// 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
|
|
4555
6379
|
const aside = !fill && (ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? 296 : 0
|
|
4556
|
-
|
|
6380
|
+
// 画笔标注开着:网页按开标注时的大小定住(侧栏拉窄了就整体缩小,不重排),画的才对得上
|
|
6381
|
+
const lock = fill && mark && markSize ? markSize : null
|
|
6382
|
+
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))
|
|
4557
6383
|
const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
|
|
4558
6384
|
body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
|
|
4559
|
-
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
6385
|
+
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})` } },
|
|
4560
6386
|
renderScreen(one, z, fill)))
|
|
4561
6387
|
}
|
|
4562
6388
|
|
|
@@ -4600,6 +6426,7 @@ window.__ModuleLoader__.load({
|
|
|
4600
6426
|
onClose: () => setPhone(false),
|
|
4601
6427
|
}) : null,
|
|
4602
6428
|
hist ? h(HistoryPanel, { t, versions, ver, onPick: (v) => { setVer(v); if (!v) setCmp(false) }, onClose: () => setHist(false) }) : null,
|
|
6429
|
+
mark && !markOff ? h(MarkLayer, { t, sending: markBusy, frames: markFrames, onCancel: () => { markRun.current++; setMark(false) }, onSend: sendMark, onCopy: canCopyImage() ? copyMark : null }) : null,
|
|
4603
6430
|
demo ? h('div', { className: 'ddp-banner' },
|
|
4604
6431
|
h(I.play),
|
|
4605
6432
|
h('span', null, t('demoOn') + ' · ' + ((infoOf(demoKey) || {}).title || demoKey.replace(/\.html?$/i, ''))),
|
|
@@ -4613,7 +6440,7 @@ window.__ModuleLoader__.load({
|
|
|
4613
6440
|
h('button', { className: 'ddp-go pri', onClick: doRestore }, t('restore')),
|
|
4614
6441
|
h('button', { className: 'ddp-btn', title: t('backNow'), onClick: () => { setVer(null); setCmp(false) } }, h(I.close))) : null,
|
|
4615
6442
|
chip,
|
|
4616
|
-
toast ? h('div', { className: 'ddp-toast' + (ver || demo || hintText ? ' low' : '') }, h('span', null, toast.text),
|
|
6443
|
+
toast ? h('div', { className: 'ddp-toast' + (mark ? ' mk' : ver || demo || hintText ? ' low' : '') }, h('span', null, toast.text),
|
|
4617
6444
|
toast.act ? h('button', { onClick: () => { toast.act.run(); setToast('') } }, toast.act.label) : null) : null))
|
|
4618
6445
|
}
|
|
4619
6446
|
|
|
@@ -4644,7 +6471,7 @@ window.__ModuleLoader__.load({
|
|
|
4644
6471
|
// ── 注册 ─────────────────────────────────────────────────────────────────
|
|
4645
6472
|
|
|
4646
6473
|
function apply(ctx) {
|
|
4647
|
-
ensureStyle()
|
|
6474
|
+
try { ctx.effect(() => ensureStyle(), 'dsh-design-studio: styles') } catch (e) { console.warn('[dsh-design-studio] styles', e) }
|
|
4648
6475
|
const t = makeT(ctx)
|
|
4649
6476
|
try { ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'dsh-design-studio: copy') } catch (e) { console.warn('[dsh-design-studio] locale', e) }
|
|
4650
6477
|
|
|
@@ -4675,6 +6502,46 @@ window.__ModuleLoader__.load({
|
|
|
4675
6502
|
inject: (sessionId) => ({ sid: sessionId, t }),
|
|
4676
6503
|
}, PreviewTab)), 'dsh-design-studio: tab body')
|
|
4677
6504
|
|
|
6505
|
+
// 点选标签(DSH 0.2.1 起有这些服务;没有就不注册,点选照旧插字)
|
|
6506
|
+
if (typeof ctx.inject === 'function') ctx.inject(['sessions', 'inputTriggers'], (scope) => {
|
|
6507
|
+
if (!scope.inputTriggers || typeof scope.inputTriggers.registerSource !== 'function') return
|
|
6508
|
+
// 每种图标一个来源(输入框里的标签只带来源名,图标靠它分);@ 菜单里不出东西
|
|
6509
|
+
for (const i of Object.keys(CHIP_ICON)) {
|
|
6510
|
+
scope.effect(() => scope.inputTriggers.registerSource({
|
|
6511
|
+
trigger: '@',
|
|
6512
|
+
name: chipSrc(i),
|
|
6513
|
+
order: 1000,
|
|
6514
|
+
showGroupTitle: false,
|
|
6515
|
+
candidates: async () => [],
|
|
6516
|
+
onPick: () => undefined,
|
|
6517
|
+
// 标签存的就是整段字:复制出来是它,发给模型也是它(前后空一行,和用户的话分开)
|
|
6518
|
+
codec: { clipboardText: pickClip, serialize: async (ref) => pickClip(ref) },
|
|
6519
|
+
}), `dsh-design-studio: chips (${i})`)
|
|
6520
|
+
}
|
|
6521
|
+
scope.effect(() => {
|
|
6522
|
+
chips = {
|
|
6523
|
+
insert(sid, text, label, span, source) {
|
|
6524
|
+
const b = scope.sessions && typeof scope.sessions.binding === 'function' ? scope.sessions.binding(sid) : null
|
|
6525
|
+
const actx = b && b.ctx
|
|
6526
|
+
if (!actx || !span || !label) return false
|
|
6527
|
+
return actx.bail(actx, 'slash/input-insert-reference', { reference: { source: source || PICK_SRC, ref: text, label, clipboardText: pickClip(text) }, span }) === true
|
|
6528
|
+
},
|
|
6529
|
+
}
|
|
6530
|
+
return () => { chips = null }
|
|
6531
|
+
}, 'dsh-design-studio: chip insert')
|
|
6532
|
+
})
|
|
6533
|
+
if (typeof ctx.inject === 'function') ctx.inject(['uiConversation'], (scope) => {
|
|
6534
|
+
const ut = scope.uiConversation && scope.uiConversation.userText
|
|
6535
|
+
if (!ut || typeof ut.register !== 'function') return
|
|
6536
|
+
scope.effect(() => ut.register(pickRule), 'dsh-design-studio: chips in messages')
|
|
6537
|
+
scope.effect(() => pickCard(t), 'dsh-design-studio: chip card')
|
|
6538
|
+
})
|
|
6539
|
+
ctx.effect(() => ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({
|
|
6540
|
+
name: 'conversation.user.text.node',
|
|
6541
|
+
key: 'ddp-pick',
|
|
6542
|
+
priority: 100,
|
|
6543
|
+
}, ({ node }) => { const c = chipOf(node.data.text, t); return h('span', { className: 'ddp-pickpill', 'data-pick': node.data.text, 'data-i': c ? c.i : 'pick' }, h('span', null, c ? c.label : t('chipOne'))) })), 'dsh-design-studio: chips in messages')
|
|
6544
|
+
|
|
4678
6545
|
ctx.effect(() => ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
4679
6546
|
name: 'conversation.session.header.utilities',
|
|
4680
6547
|
id: 'dsh-design-studio',
|
|
@@ -4688,7 +6555,7 @@ window.__ModuleLoader__.load({
|
|
|
4688
6555
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
4689
6556
|
exports.inject = INJECT
|
|
4690
6557
|
// 测试用
|
|
4691
|
-
exports._test = { STYLES, formatStyle, accentUse, 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 }
|
|
6558
|
+
exports._test = { 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 }
|
|
4692
6559
|
return module.exports
|
|
4693
6560
|
},
|
|
4694
6561
|
})
|