dsh-design-studio 0.4.0 → 0.5.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 +13 -7
- package/README.md +33 -11
- package/cordis.patch.yml +1 -1
- package/lib/browser.js +251 -0
- package/lib/bundle.js +228 -0
- package/lib/client.js +1074 -41
- package/lib/core.js +233 -21
- package/lib/doc.js +356 -0
- package/lib/index.js +428 -21
- package/lib/look.js +550 -0
- package/lib/picker.js +1118 -24
- package/lib/preset.js +20 -3
- package/lib/serve.js +23 -7
- package/lib/tool.js +6 -3
- package/locale/tool/en.json +2 -2
- package/locale/tool/zh.json +2 -2
- package/package.json +1 -1
- package/skills/ui-design/SKILL.md +40 -2
- package/skills/ui-design/references/app.md +1 -1
package/lib/client.js
CHANGED
|
@@ -99,12 +99,75 @@ window.__ModuleLoader__.load({
|
|
|
99
99
|
gRadius: '圆角',
|
|
100
100
|
gShadow: '阴影',
|
|
101
101
|
gOther: '其他',
|
|
102
|
-
exportT: '
|
|
102
|
+
exportT: '导出',
|
|
103
103
|
exFrame: '这一屏 · 带外框',
|
|
104
104
|
exRaw: '这一屏 · 纯画面',
|
|
105
105
|
exLong: '这一屏 · 整页长图',
|
|
106
106
|
exShow: '整套展示图',
|
|
107
|
-
|
|
107
|
+
exPdf: 'PDF · 每屏一页',
|
|
108
|
+
exPptx: 'PPT · 每屏一页(整页图片)',
|
|
109
|
+
exWhere: '存到设计目录的 exports/;图片还会复制到剪贴板',
|
|
110
|
+
exportedDoc: '已导出 {name}({n} 页)',
|
|
111
|
+
check: '设计体检',
|
|
112
|
+
checkTip: '设计体检:量一量文字溢出、重叠、对比度、点击区、字号',
|
|
113
|
+
checking: '正在体检…',
|
|
114
|
+
checkNone: '没发现问题',
|
|
115
|
+
checkSum: '{s} 屏 · {n} 个问题',
|
|
116
|
+
checkFix: '交给模型修',
|
|
117
|
+
checkAgain: '再查一次',
|
|
118
|
+
checkFail: '没查成:{msg}',
|
|
119
|
+
checkNote: '只量得出排版上的硬伤,好不好看还得你看',
|
|
120
|
+
checkCount: '设计体检:上次量出 {n} 个问题,点开看',
|
|
121
|
+
checkAsk: '〔设计体检〕发现 {n} 个问题,请逐个修好:改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量。',
|
|
122
|
+
ckToolHead: '量了 {s} 屏,发现 {n} 个问题。逐个改源文件,只动有问题的地方;颜色、字号能改设计变量就改设计变量:',
|
|
123
|
+
ckToolOk: '量了 {s} 屏({labels}),没发现问题。',
|
|
124
|
+
ckToolOff: '用户正在看旧版本、演示或多尺寸,右侧栏现在量不了;过一会儿再调,或照「自查」清单自己过一遍。',
|
|
125
|
+
ckToolErr: '{label}:没量成({msg})',
|
|
126
|
+
ck_hscroll: '超出屏幕',
|
|
127
|
+
ck_clipped: '文字被截断',
|
|
128
|
+
ck_overflow: '文字超出框',
|
|
129
|
+
ck_overlap: '文字叠在一起',
|
|
130
|
+
ck_contrast: '对比度不够',
|
|
131
|
+
ck_tap: '点击区太小',
|
|
132
|
+
ck_tiny: '字太小',
|
|
133
|
+
ck_img: '图片没加载出来',
|
|
134
|
+
ck_fonts: '字号太杂',
|
|
135
|
+
ck_distort: '图片被拉变形',
|
|
136
|
+
ck_long: '一行太长',
|
|
137
|
+
ck_wrap: '文字被挤断行',
|
|
138
|
+
ck_covered: '被固定栏压住',
|
|
139
|
+
ck_tight: '离固定栏太近',
|
|
140
|
+
ck_jserr: '页面脚本报错',
|
|
141
|
+
ck_missing: '文件没载出来',
|
|
142
|
+
cd_hscroll: '伸出屏幕 {px}px',
|
|
143
|
+
cd_clipped: '被切掉 {px}px',
|
|
144
|
+
cd_overflow: '伸出框 {px}px',
|
|
145
|
+
cd_overlap: '和 {other}「{otherText}」叠在一起',
|
|
146
|
+
cd_contrast: '{fg} 配 {bg},{ratio}:1(要 ≥ {need}:1)',
|
|
147
|
+
cd_tap: '{w}×{h}(至少 {min}×{min})',
|
|
148
|
+
cd_tiny: '{size}px(至少 {min}px)',
|
|
149
|
+
cd_img: '{src}',
|
|
150
|
+
cd_fonts: '一屏用了 {n} 种字号:{sizes}',
|
|
151
|
+
cd_distort: '原图 {nw}×{nh},显示成 {w}×{h}(用 object-fit: cover 或改尺寸)',
|
|
152
|
+
cd_long: '一行约 {chars} 字(最好 ≤ {max})',
|
|
153
|
+
cd_wrap: '折成了 {lines} 行(按钮、价格这种短字别断行:给它更多宽度、white-space: nowrap 或缩短文字)',
|
|
154
|
+
cd_covered: '{when}被{edgeName} {bar} 压住 {px}px(给内容留出栏的高度)',
|
|
155
|
+
cd_tight: '离{edgeName} {bar} 只有 {px}px(留 16px 以上)',
|
|
156
|
+
cd_jserr: '{msg}{at}',
|
|
157
|
+
cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
|
|
158
|
+
ckEdge_top: '顶栏',
|
|
159
|
+
ckEdge_bottom: '底栏',
|
|
160
|
+
ckWhen_top: '没滚时',
|
|
161
|
+
ckWhen_bottom: '滚到底还',
|
|
162
|
+
ckWhat_css: '样式表',
|
|
163
|
+
ckWhat_script: '脚本',
|
|
164
|
+
ckWhat_font: '字体',
|
|
165
|
+
ckWhat_other: '文件',
|
|
166
|
+
ckSize: '{w}×{h},页面高 {page}',
|
|
167
|
+
ckSizeBar: ',{edgeName} {bar} 离内容 {gap}px',
|
|
168
|
+
ckScreen: '{label}({where})',
|
|
169
|
+
ckItem: '{i}. {type}:{where}{text} — {detail}',
|
|
170
|
+
ckQuote: '「{text}」',
|
|
108
171
|
exporting: '正在导出…',
|
|
109
172
|
exported: '已导出 {name}',
|
|
110
173
|
exportedCopied: '已导出 {name},也复制到剪贴板了',
|
|
@@ -132,6 +195,9 @@ window.__ModuleLoader__.load({
|
|
|
132
195
|
cmpNow: '现在',
|
|
133
196
|
dark: '深色模式',
|
|
134
197
|
variants: '再出两版',
|
|
198
|
+
pickThis: '就要这个:其他版本挪进 drafts/,接着做全套',
|
|
199
|
+
pickText: '〔选定方向〕就用「{name}」({where})。先把同组的 {others} 挪进 drafts/ 子目录(别删),再把它改名成 {base}.html;它的设计变量搬进 tokens.css(没有就新建)、改用 tokens.css。然后接着做全套。\n',
|
|
200
|
+
pickKeep: '〔选定方向〕留原版「{name}」({where})。同组的 {others} 挪进 drafts/ 子目录(别删),其他不动。\n',
|
|
135
201
|
variantText: '给「{name}」再出两版不同方向的设计(布局或视觉风格明显不同),存成同目录下的 {b}-b.html 和 {b}-c.html,会紧挨着原版排在画布上。原版和 tokens.css 别动;新版要换配色就在自己文件里覆盖变量。\n{where}\n',
|
|
136
202
|
exThis: '导出这一屏',
|
|
137
203
|
demo: '演示:点页面里的按钮跳到别的屏',
|
|
@@ -142,8 +208,61 @@ window.__ModuleLoader__.load({
|
|
|
142
208
|
textEdit: '改字:点页面里的文字直接改,回车保存(Esc 退出)',
|
|
143
209
|
textSaved: '已改好,写回 {file}',
|
|
144
210
|
textAsk: '把页面上的「{before}」改成「{after}」(只改这一处文字,别的不动)。\n{where}\n',
|
|
211
|
+
styleEdit: '调样式:点页面里的元素,拖滑块改字号、颜色、圆角、间距(Esc 退出)',
|
|
212
|
+
styleAny: '点任意一屏里的元素调样式 · Esc 退出',
|
|
213
|
+
styleOne: '点页面里的元素调样式 · Esc 退出',
|
|
214
|
+
style: '调样式',
|
|
215
|
+
styleNone: '在页面上点一个元素。改的时候页面上先看效果,满意了再写回文件。',
|
|
216
|
+
styleFont: '字号',
|
|
217
|
+
styleWeight: '字重',
|
|
218
|
+
styleColor: '文字',
|
|
219
|
+
styleBg: '背景',
|
|
220
|
+
styleClear: '透明',
|
|
221
|
+
styleRadius: '圆角',
|
|
222
|
+
stylePadY: '上下内边距',
|
|
223
|
+
stylePadX: '左右内边距',
|
|
224
|
+
styleGap: '子元素间距',
|
|
225
|
+
styleW400: '常规',
|
|
226
|
+
styleW500: '中等',
|
|
227
|
+
styleW600: '半粗',
|
|
228
|
+
styleW700: '粗',
|
|
229
|
+
styleVar: '设计变量 {name}',
|
|
230
|
+
styleGradient: '现在是渐变,选一个颜色会换成纯色',
|
|
231
|
+
styleReset: '还原',
|
|
232
|
+
styleAsk: '交给模型',
|
|
233
|
+
styleSave: '写回文件',
|
|
234
|
+
styleSaveTip: '写进这个元素在源码里的 style 属性(改前改后各存一版)',
|
|
235
|
+
styleAskTip: '让模型改进样式表(能用设计变量就用)',
|
|
236
|
+
styleNoWrite: '开发服务器的页面不在这里写回,交给模型改',
|
|
237
|
+
styleBusyTip: '模型在干活,等它停了再写回',
|
|
238
|
+
styleSaved: '样式已写回 {file}',
|
|
239
|
+
styleMiss: '源码里没找到这个元素(可能是脚本生成的),改成交给模型',
|
|
240
|
+
styleN: '改了 {n} 项',
|
|
241
|
+
styleAskText: '〔调样式〕{name}({sel}):{list}。{rules}把这些写进它的样式规则(能用设计变量就用设计变量),别写成行内 style。\n{where}\n',
|
|
242
|
+
ruleFrom: '- {props}:现在由 {sel} 决定({at})',
|
|
243
|
+
ruleMaybe: '- {props}:大概由 {sel} 决定({at};它在 @layer / @container 里,没细算,改之前看一眼)',
|
|
244
|
+
ruleInline: '- {props}:现在写在元素的行内 style 上{over}',
|
|
245
|
+
ruleOver: ',盖过了 {sel}({at})',
|
|
246
|
+
ruleAt: '{file} 第 {line} 行',
|
|
247
|
+
styleArrow: '{prop} {from} → {to}',
|
|
248
|
+
styleJoin: ';',
|
|
249
|
+
lang: 'zh',
|
|
250
|
+
stylesHead: '先挑一个风格开工',
|
|
251
|
+
stylesTip: '点一个,整套配色、字体、圆角放进输入框;再写要做什么',
|
|
252
|
+
stylesOpen: '换个风格…',
|
|
253
|
+
stylesTitle: '换个风格',
|
|
254
|
+
stylesSwap: '点一个,让模型把现在的设计换成这个风格(先改 tokens.css,布局和文案不动)',
|
|
255
|
+
styleText: '〔风格〕{name}——{desc}\n配色:{colors}\n字体:{fonts}\n形状:{shape}\n要点:{notes}\n{ask}',
|
|
256
|
+
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}(只做图形、大字),链接和小字强调 {link}{bgImage}',
|
|
257
|
+
styleBgImage: ';底色上叠 {v}',
|
|
258
|
+
styleFonts: '标题 {head};正文 {body}',
|
|
259
|
+
styleShape: '圆角 {r}px;阴影 {s}',
|
|
260
|
+
styleStart: '照这个风格来做(先把这些定进 tokens.css;不用再问想要什么感觉):',
|
|
261
|
+
styleRestyle: '把现在的设计换成这个风格:先改 tokens.css(配色、字体、圆角、阴影),各屏跟着变;页面里写死的颜色也换成变量;布局和文案先不动。\n',
|
|
145
262
|
demoNone: '这一屏还没有跳到别的屏的按钮;可以让模型用 <a href="detail.html"> 把几屏串起来',
|
|
146
263
|
phone: '在手机上看',
|
|
264
|
+
away: '这里看不到预览:开发服务器跑在电脑本机上,从手机或别的电脑打开 DSH 时连不过去(本地 HTML 文件的设计能看)。回到那台电脑上看;想在手机上看,就在电脑上点「在手机上看」扫码(同一个 Wi-Fi)。',
|
|
265
|
+
remoteFail: '这一屏没能传过来(连接断了,或图片太大)。点「刷新」再试。',
|
|
147
266
|
phoneStarting: '正在打开…',
|
|
148
267
|
phoneHow: '用手机相机扫一扫',
|
|
149
268
|
phoneWifi: '手机和电脑要连同一个 Wi-Fi',
|
|
@@ -169,6 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
169
288
|
demoShort: '演示',
|
|
170
289
|
editAddr: '换一个地址',
|
|
171
290
|
working: '模型正在工作',
|
|
291
|
+
asking: '模型在等你回答(在对话里)',
|
|
172
292
|
writing: '正在写 {file}',
|
|
173
293
|
born: '已生成 {name}',
|
|
174
294
|
updated: '已更新{file}',
|
|
@@ -244,12 +364,75 @@ window.__ModuleLoader__.load({
|
|
|
244
364
|
gRadius: 'Radii',
|
|
245
365
|
gShadow: 'Shadows',
|
|
246
366
|
gOther: 'Other',
|
|
247
|
-
exportT: 'Export
|
|
367
|
+
exportT: 'Export',
|
|
248
368
|
exFrame: 'This screen · in frame',
|
|
249
369
|
exRaw: 'This screen · plain',
|
|
250
370
|
exLong: 'This screen · full page',
|
|
251
371
|
exShow: 'Showcase of all screens',
|
|
252
|
-
|
|
372
|
+
exPdf: 'PDF · one page per screen',
|
|
373
|
+
exPptx: 'PowerPoint · one slide per screen (as images)',
|
|
374
|
+
exWhere: 'Saved to exports/ in the design folder; images are also copied to the clipboard',
|
|
375
|
+
exportedDoc: 'Exported {name} ({n} pages)',
|
|
376
|
+
check: 'Design check',
|
|
377
|
+
checkTip: 'Design check: measure overflowing or overlapping text, contrast, tap targets and type sizes',
|
|
378
|
+
checking: 'Checking…',
|
|
379
|
+
checkNone: 'No problems found',
|
|
380
|
+
checkSum: '{s} screens · {n} problems',
|
|
381
|
+
checkFix: 'Ask the model to fix',
|
|
382
|
+
checkAgain: 'Check again',
|
|
383
|
+
checkFail: 'Check failed: {msg}',
|
|
384
|
+
checkNote: 'Only measurable layout problems; whether it looks good is still up to you',
|
|
385
|
+
checkCount: 'Design check: {n} problems last time. Click to see them',
|
|
386
|
+
checkAsk: '[Design check] Found {n} problems. Please fix each one in the source files, touching only what is listed; for colors and type sizes, change the design tokens where possible.',
|
|
387
|
+
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:',
|
|
388
|
+
ckToolOk: 'Measured {s} screens ({labels}). No problems found.',
|
|
389
|
+
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.',
|
|
390
|
+
ckToolErr: '{label}: could not measure ({msg})',
|
|
391
|
+
ck_hscroll: 'Wider than the screen',
|
|
392
|
+
ck_clipped: 'Text cut off',
|
|
393
|
+
ck_overflow: 'Text spills out of its box',
|
|
394
|
+
ck_overlap: 'Overlapping text',
|
|
395
|
+
ck_contrast: 'Low contrast',
|
|
396
|
+
ck_tap: 'Tap target too small',
|
|
397
|
+
ck_tiny: 'Text too small',
|
|
398
|
+
ck_img: 'Image failed to load',
|
|
399
|
+
ck_fonts: 'Too many type sizes',
|
|
400
|
+
ck_distort: 'Stretched image',
|
|
401
|
+
ck_long: 'Lines too long',
|
|
402
|
+
ck_wrap: 'Short text wraps',
|
|
403
|
+
ck_covered: 'Hidden under a fixed bar',
|
|
404
|
+
ck_tight: 'Too close to a fixed bar',
|
|
405
|
+
ck_jserr: 'Script error',
|
|
406
|
+
ck_missing: 'File failed to load',
|
|
407
|
+
cd_hscroll: 'sticks out {px}px',
|
|
408
|
+
cd_clipped: '{px}px cut off',
|
|
409
|
+
cd_overflow: 'sticks out {px}px',
|
|
410
|
+
cd_overlap: 'overlaps {other} "{otherText}"',
|
|
411
|
+
cd_contrast: '{fg} on {bg}, {ratio}:1 (needs ≥ {need}:1)',
|
|
412
|
+
cd_tap: '{w}×{h} (at least {min}×{min})',
|
|
413
|
+
cd_tiny: '{size}px (at least {min}px)',
|
|
414
|
+
cd_img: '{src}',
|
|
415
|
+
cd_fonts: '{n} type sizes on one screen: {sizes}',
|
|
416
|
+
cd_distort: '{nw}×{nh} shown at {w}×{h} (use object-fit: cover or fix the size)',
|
|
417
|
+
cd_long: 'about {chars} characters per line (keep it ≤ {max})',
|
|
418
|
+
cd_wrap: 'breaks onto {lines} lines (keep short labels and prices on one line: more width, white-space: nowrap, or shorter text)',
|
|
419
|
+
cd_covered: '{when}{px}px hidden under the {edgeName} {bar} (leave room for the bar)',
|
|
420
|
+
cd_tight: 'only {px}px from the {edgeName} {bar} (leave 16px or more)',
|
|
421
|
+
cd_jserr: '{msg}{at}',
|
|
422
|
+
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
423
|
+
ckEdge_top: 'top bar',
|
|
424
|
+
ckEdge_bottom: 'bottom bar',
|
|
425
|
+
ckWhen_top: 'before scrolling, ',
|
|
426
|
+
ckWhen_bottom: 'even scrolled to the end, ',
|
|
427
|
+
ckWhat_css: 'stylesheet',
|
|
428
|
+
ckWhat_script: 'script',
|
|
429
|
+
ckWhat_font: 'font',
|
|
430
|
+
ckWhat_other: 'file',
|
|
431
|
+
ckSize: '{w}×{h}, page height {page}',
|
|
432
|
+
ckSizeBar: ', {edgeName} {bar} {gap}px from the content',
|
|
433
|
+
ckScreen: '{label} ({where})',
|
|
434
|
+
ckItem: '{i}. {type}: {where}{text} — {detail}',
|
|
435
|
+
ckQuote: ' "{text}"',
|
|
253
436
|
exporting: 'Exporting…',
|
|
254
437
|
exported: 'Exported {name}',
|
|
255
438
|
exportedCopied: 'Exported {name} and copied it',
|
|
@@ -277,6 +460,9 @@ window.__ModuleLoader__.load({
|
|
|
277
460
|
cmpNow: 'Now',
|
|
278
461
|
dark: 'Dark mode',
|
|
279
462
|
variants: 'Two more takes',
|
|
463
|
+
pickThis: 'Use this one: move the other takes to drafts/ and build the rest',
|
|
464
|
+
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',
|
|
465
|
+
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',
|
|
280
466
|
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{where}\n',
|
|
281
467
|
exThis: 'Export this screen',
|
|
282
468
|
demo: 'Play: tap buttons in the page to move between screens',
|
|
@@ -287,8 +473,61 @@ window.__ModuleLoader__.load({
|
|
|
287
473
|
textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
|
|
288
474
|
textSaved: 'Saved to {file}',
|
|
289
475
|
textAsk: 'Change the text "{before}" to "{after}" on the page (only this text, nothing else).\n{where}\n',
|
|
476
|
+
styleEdit: 'Style: click an element, then drag sliders for size, colors, radius and spacing (Esc to stop)',
|
|
477
|
+
styleAny: 'Click an element in any screen to style it · Esc to stop',
|
|
478
|
+
styleOne: 'Click an element in the page to style it · Esc to stop',
|
|
479
|
+
style: 'Style',
|
|
480
|
+
styleNone: 'Click an element in the page. Changes show in the preview first; save them to the file when you like them.',
|
|
481
|
+
styleFont: 'Font size',
|
|
482
|
+
styleWeight: 'Weight',
|
|
483
|
+
styleColor: 'Text',
|
|
484
|
+
styleBg: 'Background',
|
|
485
|
+
styleClear: 'transparent',
|
|
486
|
+
styleRadius: 'Radius',
|
|
487
|
+
stylePadY: 'Padding (top / bottom)',
|
|
488
|
+
stylePadX: 'Padding (left / right)',
|
|
489
|
+
styleGap: 'Gap',
|
|
490
|
+
styleW400: 'Regular',
|
|
491
|
+
styleW500: 'Medium',
|
|
492
|
+
styleW600: 'Semi',
|
|
493
|
+
styleW700: 'Bold',
|
|
494
|
+
styleVar: 'Token {name}',
|
|
495
|
+
styleGradient: 'It is a gradient now; picking a color makes it solid',
|
|
496
|
+
styleReset: 'Reset',
|
|
497
|
+
styleAsk: 'Ask the model',
|
|
498
|
+
styleSave: 'Save to file',
|
|
499
|
+
styleSaveTip: 'Writes into this element\'s style attribute in the source (a version is kept before and after)',
|
|
500
|
+
styleAskTip: 'Let the model move it into the stylesheet (using tokens where it can)',
|
|
501
|
+
styleNoWrite: 'Dev-server pages are not written here; ask the model',
|
|
502
|
+
styleBusyTip: 'The model is working; save when it stops',
|
|
503
|
+
styleSaved: 'Style saved to {file}',
|
|
504
|
+
styleMiss: 'Could not find this element in the source (maybe a script made it); asking the model instead',
|
|
505
|
+
styleN: '{n} changed',
|
|
506
|
+
styleAskText: '[Style] {name} ({sel}): {list}. {rules}Put it in its stylesheet rule, using design tokens where you can, not an inline style.\n{where}\n',
|
|
507
|
+
ruleFrom: '- {props}: currently set by {sel} ({at})',
|
|
508
|
+
ruleMaybe: '- {props}: probably set by {sel} ({at}; it sits in @layer / @container and was not fully resolved, check before editing)',
|
|
509
|
+
ruleInline: '- {props}: currently in the element\'s inline style{over}',
|
|
510
|
+
ruleOver: ', overriding {sel} ({at})',
|
|
511
|
+
ruleAt: '{file} line {line}',
|
|
512
|
+
styleArrow: '{prop} {from} → {to}',
|
|
513
|
+
styleJoin: '; ',
|
|
514
|
+
lang: 'en',
|
|
515
|
+
stylesHead: 'Start from a style',
|
|
516
|
+
stylesTip: 'Pick one to put its colors, fonts and shapes in the chat box, then say what to make',
|
|
517
|
+
stylesOpen: 'Try another style…',
|
|
518
|
+
stylesTitle: 'Change the style',
|
|
519
|
+
stylesSwap: 'Pick one and the model restyles the current design (tokens.css first; layout and copy stay)',
|
|
520
|
+
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
521
|
+
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} (graphics and large text only), links and small emphasis {link}{bgImage}',
|
|
522
|
+
styleBgImage: '; on the background: {v}',
|
|
523
|
+
styleFonts: 'headings {head}; body {body}',
|
|
524
|
+
styleShape: 'radius {r}px; shadow {s}',
|
|
525
|
+
styleStart: 'Design in this style (put these into tokens.css first; no need to ask about the feel again):',
|
|
526
|
+
styleRestyle: 'Restyle the current design to this: update tokens.css first (colors, fonts, radius, shadows) so every screen follows; replace hard-coded colors in the pages with tokens; keep layout and copy for now.\n',
|
|
290
527
|
demoNone: 'This screen has no links to other screens yet. Ask the model to connect screens with <a href="detail.html">',
|
|
291
528
|
phone: 'View on phone',
|
|
529
|
+
away: 'The dev server runs on the computer itself, so it can\'t load when DSH is opened from a phone or another computer (designs made of local HTML files do show). View it on that computer, or click "View on phone" there and scan the code (same Wi-Fi).',
|
|
530
|
+
remoteFail: 'This screen couldn\'t be sent over (connection dropped, or the images are too large). Click Reload to try again.',
|
|
292
531
|
phoneStarting: 'Starting…',
|
|
293
532
|
phoneHow: 'Scan with your phone camera',
|
|
294
533
|
phoneWifi: 'Phone and computer must be on the same Wi-Fi',
|
|
@@ -314,6 +553,7 @@ window.__ModuleLoader__.load({
|
|
|
314
553
|
demoShort: 'Play',
|
|
315
554
|
editAddr: 'Change address',
|
|
316
555
|
working: 'The model is working',
|
|
556
|
+
asking: 'The model is waiting for your answer (in the chat)',
|
|
317
557
|
writing: 'Writing {file}',
|
|
318
558
|
born: 'Generated {name}',
|
|
319
559
|
updated: 'Updated{file}',
|
|
@@ -385,6 +625,7 @@ window.__ModuleLoader__.load({
|
|
|
385
625
|
.ddp-stage{position:relative;flex:1;min-height:0;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
|
|
386
626
|
.ddp-stage.full{background:#fff}
|
|
387
627
|
.ddp-scr{position:absolute;inset:0;overflow:hidden}
|
|
628
|
+
.ddp-away{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;font-size:13px;line-height:1.6;color:var(--dsw-alias-text-secondary,#888);background:var(--dsw-alias-bg-layer-1)}
|
|
388
629
|
.ddp-frame{position:absolute;left:0;top:0;width:100%;height:100%;border:0;background:#fff;opacity:0;z-index:0;pointer-events:none}
|
|
389
630
|
.ddp-frame.old{opacity:1;z-index:1}
|
|
390
631
|
.ddp-frame.cur{opacity:1;z-index:2;pointer-events:auto;transition:opacity .32s ease}
|
|
@@ -431,6 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
431
672
|
.ddp-hint{position:absolute;left:50%;top:12px;transform:translateX(-50%);z-index:7;max-width:calc(100% - 24px);padding:6px 13px;border-radius:999px;
|
|
432
673
|
background:rgba(18,18,24,.84);color:#ececf0;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
433
674
|
box-shadow:0 6px 18px rgba(0,0,0,.22);backdrop-filter:blur(8px);pointer-events:none;animation:ddp-in .2s ease}
|
|
675
|
+
.ddp-hint.side{left:calc((100% - 296px) / 2);max-width:calc(100% - 320px)}
|
|
434
676
|
@property --ddp-a{syntax:'<angle>';initial-value:0deg;inherits:false}
|
|
435
677
|
.ddp-ring{position:absolute;inset:0;z-index:4;pointer-events:none;padding:2.5px;
|
|
436
678
|
background:conic-gradient(from var(--ddp-a),transparent 0deg 140deg,${ACCENT} 240deg,#a78bfa 300deg,#5eead4 340deg,transparent 360deg);
|
|
@@ -448,6 +690,7 @@ window.__ModuleLoader__.load({
|
|
|
448
690
|
.ddp-chip .sp{width:12px;height:12px;flex:none;border-radius:50%;border:1.6px solid rgba(255,255,255,.22);border-top-color:#9db0ff;animation:ddp-rot .75s linear infinite}
|
|
449
691
|
.ddp-chip .tx{background:linear-gradient(90deg,#f1f1f4 0 38%,#a9b8ff 50%,#f1f1f4 62% 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:ddp-txt 2.2s linear infinite}
|
|
450
692
|
.ddp-chip.ok svg{width:14px;height:14px;flex:none;color:#4ade80}
|
|
693
|
+
.ddp-chip .q{width:8px;height:8px;flex:none;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.25)}
|
|
451
694
|
@keyframes ddp-rot{to{transform:rotate(360deg)}}
|
|
452
695
|
@keyframes ddp-txt{from{background-position:100% 0}to{background-position:-160% 0}}
|
|
453
696
|
@keyframes ddp-blink{50%{opacity:.25}}
|
|
@@ -525,6 +768,54 @@ window.__ModuleLoader__.load({
|
|
|
525
768
|
.ddp-empty .ddp-refline{margin-top:0}
|
|
526
769
|
.ddp-empty .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
|
|
527
770
|
.ddp-empty .lk:hover{text-decoration:underline;text-underline-offset:2px}
|
|
771
|
+
.ddp-ck{align-items:flex-start}
|
|
772
|
+
.ddp-ck .no{width:18px;height:18px;flex:none;margin-top:1px;border-radius:9px;background:#f59e0b;color:#fff;font:700 10px/18px system-ui,sans-serif;text-align:center}
|
|
773
|
+
.ddp-ck .no.hi{background:#e5484d}
|
|
774
|
+
.ddp-tok.ddp-ck .nm{font-family:inherit;font-size:12px;line-height:17px}
|
|
775
|
+
.ddp-tok.ddp-ck .vl{white-space:normal;overflow-wrap:anywhere}
|
|
776
|
+
.ddp-check .ok{display:flex;align-items:center;gap:8px;padding:14px 6px;color:var(--dsw-alias-label-secondary)}
|
|
777
|
+
.ddp-check .ok svg{width:16px;height:16px;flex:none;color:#30a46c}
|
|
778
|
+
.ddp-check footer.row,.ddp-style footer.row{display:flex;gap:6px}
|
|
779
|
+
.ddp-check footer.row button,.ddp-style footer.row button{flex:1;justify-content:center;padding:0 6px}
|
|
780
|
+
.ddp-check footer .go,.ddp-style footer .go{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
|
|
781
|
+
.ddp-check footer .go:hover,.ddp-style footer .go:hover{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);opacity:.88}
|
|
782
|
+
.ddp-check footer button:disabled,.ddp-style footer button:disabled{opacity:.45;cursor:default}
|
|
783
|
+
.ddp-style .bd{padding:0 14px 10px}
|
|
784
|
+
.ddp-style .who{margin:0;padding:0 14px 6px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
785
|
+
.ddp-style .who code{font:11px/17px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary)}
|
|
786
|
+
.ddp-style .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
|
|
787
|
+
.ddp-style label{display:flex;align-items:baseline;gap:8px;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
788
|
+
.ddp-style label span{flex:1}
|
|
789
|
+
.ddp-style label em{font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace;font-style:normal;color:var(--dsw-alias-label-tertiary);max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
790
|
+
.ddp-style label em.v{color:${ACCENT}}
|
|
791
|
+
.ddp-style input[type=range]{display:block;width:100%;margin:0;accent-color:${ACCENT};cursor:pointer}
|
|
792
|
+
.ddp-style .chips{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin-top:7px}
|
|
793
|
+
.ddp-style .row>.chips:first-of-type{margin-top:0}
|
|
794
|
+
.ddp-style .chip{height:22px;max-width:100%;padding:0 7px;border:0;border-radius:6px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));color:var(--dsw-alias-label-secondary);font:11px/22px inherit;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
795
|
+
.ddp-style .chip:hover{color:var(--dsw-alias-label-primary)}
|
|
796
|
+
.ddp-style .chip.on{box-shadow:inset 0 0 0 1.5px ${ACCENT};color:var(--dsw-alias-label-primary)}
|
|
797
|
+
.ddp-style .chip.c{width:22px;padding:2px}
|
|
798
|
+
.ddp-style .chip.c i{display:block;width:100%;height:100%;border-radius:4px;box-shadow:inset 0 0 0 .5px rgba(127,127,127,.45)}
|
|
799
|
+
.ddp-style input[type=color]{width:30px;height:22px;padding:0;border:0;border-radius:6px;background:none;cursor:pointer}
|
|
800
|
+
.ddp-style .seg{display:flex;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1))}
|
|
801
|
+
.ddp-style .seg button{flex:1;height:24px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/24px inherit;cursor:pointer}
|
|
802
|
+
.ddp-style .seg button.on{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.14)}
|
|
803
|
+
.ddp-style .note{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
804
|
+
.ddp-style .cnt{padding:0 14px 6px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
805
|
+
.ddp-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px 8px}
|
|
806
|
+
.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}
|
|
807
|
+
.ddp-sc .pv{position:relative;display:block;height:58px;border-radius:10px;overflow:hidden;box-shadow:0 0 0 .5px rgba(127,127,127,.35);transition:transform .15s ${EASE},box-shadow .15s}
|
|
808
|
+
.ddp-sc:hover .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
809
|
+
.ddp-sc:hover{color:var(--dsw-alias-label-primary)}
|
|
810
|
+
.ddp-sc .pv b{position:absolute;left:8px;top:6px;font-size:17px;line-height:1}
|
|
811
|
+
.ddp-sc .pv .bt{position:absolute;right:8px;top:8px;width:20px;height:9px}
|
|
812
|
+
.ddp-sc .pv .cd{position:absolute;left:8px;right:8px;bottom:7px;height:20px}
|
|
813
|
+
.ddp-sc .pv .ln{position:absolute;left:6px;top:6px;width:46%;height:3px;border-radius:2px;opacity:.85}
|
|
814
|
+
.ddp-sc .pv .ln.s{top:12px;width:30%;opacity:.6}
|
|
815
|
+
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
816
|
+
.ddp-stylepick{width:100%;max-width:440px;margin:4px 0 6px;text-align:left}
|
|
817
|
+
.ddp-stylepick>b{display:block;margin-bottom:2px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
818
|
+
.ddp-stylepick>p{margin:0 0 10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
528
819
|
.ddp-tok{display:flex;align-items:center;gap:9px;width:100%;min-height:32px;padding:4px 6px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}
|
|
529
820
|
.ddp-tok:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
530
821
|
.ddp-tok .pv{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
|
@@ -539,6 +830,7 @@ window.__ModuleLoader__.load({
|
|
|
539
830
|
.ddp-tok .vl{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}
|
|
540
831
|
.ddp-btn.has::after{content:"";position:absolute;right:4px;top:4px;width:5px;height:5px;border-radius:50%;background:${ACCENT}}
|
|
541
832
|
.ddp-btn{position:relative}
|
|
833
|
+
.ddp-btn .bdg{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 3px;box-sizing:border-box;border-radius:7px;background:#f59e0b;color:#fff;font:700 9px/14px system-ui,sans-serif;text-align:center;pointer-events:none}
|
|
542
834
|
.ddp-menu .ft{margin:4px 4px 2px;padding:6px 6px 2px;border-top:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px;max-width:220px;white-space:normal}
|
|
543
835
|
.ddp-ver{display:flex;flex-direction:column;align-items:stretch;gap:2px;width:100%;padding:8px 10px;margin-bottom:2px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}
|
|
544
836
|
.ddp-ver:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
@@ -632,6 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
632
924
|
sizes: () => svg([h('rect', { key: 1, x: 2.5, y: 5, width: 12, height: 9.5, rx: 1.4 }), h('path', { key: 2, d: 'M6.5 18h4' }),
|
|
633
925
|
h('rect', { key: 3, x: 16.5, y: 8, width: 5, height: 10.5, rx: 1.2 })]),
|
|
634
926
|
text: () => svg([h('path', { key: 1, d: 'M5 7V5h11v2M10.5 5v14M8 19h5' }), h('path', { key: 2, d: 'M17 12v8M15.5 12h3M15.5 20h3' })]),
|
|
927
|
+
sliders: () => svg([h('path', { key: 1, d: 'M4 7h9M17 7h3M4 17h3M11 17h9' }), h('circle', { key: 2, cx: 15, cy: 7, r: 2 }), h('circle', { key: 3, cx: 9, cy: 17, r: 2 })]),
|
|
635
928
|
image: () => svg([h('rect', { key: 1, x: 3.5, y: 4.5, width: 17, height: 15, rx: 2.5 }), h('circle', { key: 2, cx: 9, cy: 10, r: 1.6 }), h('path', { key: 3, d: 'M4 17l5-4.5 4 3.5 3-2.5 4.5 4', strokeLinejoin: 'round' })]),
|
|
636
929
|
more: () => svg([h('circle', { key: 1, cx: 5.5, cy: 12, r: 1.3 }), h('circle', { key: 2, cx: 12, cy: 12, r: 1.3 }), h('circle', { key: 3, cx: 18.5, cy: 12, r: 1.3 })]),
|
|
637
930
|
play: () => svg([h('path', { key: 1, d: 'M8 5.5v13l10.5-6.5z', strokeLinejoin: 'round' })]),
|
|
@@ -644,6 +937,9 @@ window.__ModuleLoader__.load({
|
|
|
644
937
|
refresh: () => svg([h('path', { key: 1, d: 'M20 11a8 8 0 1 0-2.3 5.7' }), h('path', { key: 2, d: 'M20 5v6h-6' })]),
|
|
645
938
|
external: () => svg([h('path', { key: 1, d: 'M14 4h6v6' }), h('path', { key: 2, d: 'M20 4l-9 9' }), h('path', { key: 3, d: 'M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5' })]),
|
|
646
939
|
check: () => svg([h('path', { key: 1, d: 'M5 12.5l4.5 4.5L19 7.5', strokeWidth: 2.4 })]),
|
|
940
|
+
stetho: () => svg([h('path', { key: 1, d: 'M6 3.5H5a1 1 0 0 0-1 1V9a5 5 0 0 0 10 0V4.5a1 1 0 0 0-1-1h-1' }), h('path', { key: 2, d: 'M9 14v1.5a4.5 4.5 0 0 0 9 0V13' }), h('circle', { key: 3, cx: 18, cy: 11, r: 2 })]),
|
|
941
|
+
pdf: () => svg([h('path', { key: 1, d: 'M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5z' }), h('path', { key: 2, d: 'M14 3.5v5h5' }), h('path', { key: 3, d: 'M8.5 13h7M8.5 16.5h4.5' })]),
|
|
942
|
+
deck: () => svg([h('rect', { key: 1, x: 3, y: 4, width: 18, height: 12.5, rx: 1.8 }), h('path', { key: 2, d: 'M12 16.5V20M8.5 20.5h7' }), h('path', { key: 3, d: 'M7 12.5l3-3 2.5 2 4-4' })]),
|
|
647
943
|
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),
|
|
648
944
|
}
|
|
649
945
|
// 引导页上的大图标
|
|
@@ -716,6 +1012,19 @@ window.__ModuleLoader__.load({
|
|
|
716
1012
|
// ── 选中内容 → 输入框里的一段字 ──────────────────────────────────────────
|
|
717
1013
|
|
|
718
1014
|
/** 页面地址 → 用户认得的位置:本地文件给路径,开发服务器给 localhost 地址 */
|
|
1015
|
+
/**
|
|
1016
|
+
* 这一屏里现在显示的是哪页(在预览里点链接跳过页,就是跳到的那页)→ { key, url }。
|
|
1017
|
+
* 同一个预览服务器上的 .html 才算、旧版本不算;不然还是这一屏。写回文件按它找文件,别写到原来那页去。
|
|
1018
|
+
*/
|
|
1019
|
+
function pageOf(sc, href) {
|
|
1020
|
+
try {
|
|
1021
|
+
const u = new URL(href)
|
|
1022
|
+
const p = decodeURIComponent(u.pathname).replace(/^\/+/, '')
|
|
1023
|
+
if (u.origin === new URL(sc.url).origin && /\.html?$/i.test(p) && !p.startsWith('__dsh_design__/')) return { key: p, url: u.origin + u.pathname }
|
|
1024
|
+
} catch { /* 地址不对 */ }
|
|
1025
|
+
return { key: sc.key, url: sc.url }
|
|
1026
|
+
}
|
|
1027
|
+
|
|
719
1028
|
function whereOf(href, st) {
|
|
720
1029
|
try {
|
|
721
1030
|
const u = new URL(href)
|
|
@@ -800,6 +1109,231 @@ window.__ModuleLoader__.load({
|
|
|
800
1109
|
return `〔设计变量〕${tok.name}: ${tok.value}\n${whereOf(tok.href, st)}\n`
|
|
801
1110
|
}
|
|
802
1111
|
|
|
1112
|
+
// ── 风格卡:开局挑一个风格(或给已有的设计换风格),整套配色、字体、圆角、阴影、要点放进输入框 ──────
|
|
1113
|
+
// 配色都量过对比度:正文和次要文字对底色 ≥ 4.5:1,主色按钮上的字 ≥ 4.5:1
|
|
1114
|
+
|
|
1115
|
+
const SANS = '"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1116
|
+
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1117
|
+
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1118
|
+
const STYLES = [
|
|
1119
|
+
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1120
|
+
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
1121
|
+
head: SANS, w: 600, radius: 10, shadow: '0 1px 2px rgba(24,24,27,.06)',
|
|
1122
|
+
nzh: '靠字号和留白分层级,不靠色块;分隔用 1px 细线;蓝色只给链接和选中态;大标题字距收 -0.02em',
|
|
1123
|
+
nen: 'Hierarchy from type size and whitespace, not color blocks; 1px hairlines; blue only for links and selection; tighten big headings to -0.02em' },
|
|
1124
|
+
{ id: 'warm', zh: '温暖手作', en: 'Warm craft', dzh: '纸感底色、圆润、手绘感', den: 'Paper tones, soft and rounded, hand-made feel',
|
|
1125
|
+
c: { bg: '#F6EFE6', surface: '#FFF9F2', text: '#2B1D14', muted: '#6E5A4B', line: '#E7D9C8', primary: '#A9542A', accent: '#5E7A4A', on: '#FFFFFF', link: '#52703F' },
|
|
1126
|
+
head: SERIF, w: 600, radius: 16, shadow: '0 8px 24px rgba(120,72,30,.12)',
|
|
1127
|
+
nzh: '标题用衬线字,正文无衬线;插画用手绘线条和色块;阴影带暖色;中性色都偏暖,不用纯灰',
|
|
1128
|
+
nen: 'Serif headings, sans body; hand-drawn line illustrations and color blocks; warm-tinted shadows; warm neutrals, no pure gray' },
|
|
1129
|
+
{ id: 'editorial', zh: '杂志编辑', en: 'Editorial', dzh: '大标题、黑线栅格、红色点题', den: 'Big headlines, black-rule grid, a red accent',
|
|
1130
|
+
c: { bg: '#F4F1EA', surface: '#FFFFFF', text: '#111111', muted: '#555555', line: '#111111', primary: '#C8231B', accent: '#111111', on: '#FFFFFF', link: '#C8231B' },
|
|
1131
|
+
head: SERIF, w: 700, radius: 2, shadow: 'none',
|
|
1132
|
+
nzh: '标题很大、粗细对比强烈;用粗细两种黑线分栏;版式不对称、有留白;红色只在一两处;几乎没有圆角和阴影',
|
|
1133
|
+
nen: 'Very large headlines with strong weight contrast; thick and thin black rules for columns; asymmetric layout; red in one or two places only; almost no radius or shadow' },
|
|
1134
|
+
{ id: 'dark', zh: '深色高级', en: 'Dark luxe', dzh: '近黑底、香槟金点缀、大图', den: 'Near-black, champagne gold accents, big imagery',
|
|
1135
|
+
c: { bg: '#0E0E10', surface: '#18181B', text: '#F2EFE9', muted: '#A19C93', line: '#2C2C30', primary: '#C9A86A', accent: '#E8E2D6', on: '#17140E', link: '#C9A86A' },
|
|
1136
|
+
head: SERIF, w: 500, radius: 12, shadow: '0 12px 40px rgba(0,0,0,.45)',
|
|
1137
|
+
nzh: '金色只点在关键按钮和细线上;层级靠表面色深浅,不靠阴影;标题细一点、字距放开;留白要多',
|
|
1138
|
+
nen: 'Gold only on key buttons and fine lines; layers from surface shades, not shadows; lighter, tracked-out headings; generous space' },
|
|
1139
|
+
{ id: 'playful', zh: '活泼年轻', en: 'Playful', dzh: '粗描边、硬阴影、贴纸感', den: 'Thick outlines, hard shadows, sticker feel',
|
|
1140
|
+
c: { bg: '#FFF7E8', surface: '#FFFFFF', text: '#1D1B3A', muted: '#5D5A82', line: '#1D1B3A', primary: '#FF6A3D', accent: '#1F9E92', on: '#1D1B3A', link: '#137167' },
|
|
1141
|
+
head: ROUND, w: 800, radius: 20, shadow: '4px 4px 0 #1D1B3A',
|
|
1142
|
+
nzh: '卡片和按钮 2px 深色描边 + 不模糊的硬阴影;主色按钮上用深色字;元素可以略微倾斜、像贴纸;颜色最多三种',
|
|
1143
|
+
nen: '2px dark outlines and hard, unblurred shadows on cards and buttons; dark text on the primary button; slightly tilted sticker-like bits; three colors max' },
|
|
1144
|
+
{ id: 'fresh', zh: '清新自然', en: 'Fresh', dzh: '浅绿底、圆角卡片、轻插画', den: 'Pale green, rounded cards, light illustration',
|
|
1145
|
+
c: { bg: '#F3F7F2', surface: '#FFFFFF', text: '#1F2A22', muted: '#56665A', line: '#DCE6DA', primary: '#2F7D4F', accent: '#C9962A', on: '#FFFFFF', link: '#2F7D4F' },
|
|
1146
|
+
head: SANS, w: 600, radius: 14, shadow: '0 6px 18px rgba(47,125,79,.10)',
|
|
1147
|
+
nzh: '大面积浅色、卡片白底;插画用叶子、阳光这类自然元素;黄色只做小点缀;整体轻、透气',
|
|
1148
|
+
nen: 'Large light areas with white cards; nature motifs (leaves, sunlight) in illustrations; yellow only as a small accent; light and airy' },
|
|
1149
|
+
{ id: 'tech', zh: '专业科技', en: 'Pro / tech', dzh: '冷灰蓝、信息密、数字清楚', den: 'Cool gray-blue, dense info, crisp numbers',
|
|
1150
|
+
c: { bg: '#F5F7FB', surface: '#FFFFFF', text: '#0F172A', muted: '#55657D', line: '#E2E8F0', primary: '#2457F5', accent: '#0F8F8E', on: '#FFFFFF', link: '#2457F5' },
|
|
1151
|
+
head: SANS, w: 600, radius: 8, shadow: '0 1px 3px rgba(15,23,42,.08)',
|
|
1152
|
+
nzh: '信息密度高但对齐严格;数字用等宽(font-variant-numeric: tabular-nums);表格行高 44–48;状态用小色点和标签;主色只给主操作',
|
|
1153
|
+
nen: 'Dense but strictly aligned; tabular numbers (font-variant-numeric: tabular-nums); 44–48px table rows; status as small dots and tags; primary color only for the main action' },
|
|
1154
|
+
{ id: 'glass', zh: '玻璃质感', en: 'Glass', dzh: '柔光底、浮层毛玻璃', den: 'Soft glow background, frosted overlays',
|
|
1155
|
+
c: { bg: '#EAF0F6', surface: '#F7F9FC', text: '#10151C', muted: '#4F5B69', line: '#D5DEE8', primary: '#CC4520', accent: '#2B6CB0', on: '#FFFFFF', link: '#2B6CB0' },
|
|
1156
|
+
bgImage: 'radial-gradient(circle at 20% 15%, #FFD9C7 0, transparent 45%), radial-gradient(circle at 85% 70%, #C9E2FF 0, transparent 50%)',
|
|
1157
|
+
head: SANS, w: 600, radius: 18, shadow: '0 10px 30px rgba(16,21,28,.10)',
|
|
1158
|
+
nzh: '底色加两三团柔和的光斑(别用紫蓝渐变);只有浮层、卡片、标签栏用毛玻璃(半透明白 + backdrop-filter: blur(20px) + 1px 白色描边),正文区是实色;毛玻璃上的字照样要够深',
|
|
1159
|
+
nen: 'Two or three soft light blobs on the background (not a purple-blue gradient); frosted glass only on overlays, cards and tab bars (translucent white + backdrop-filter: blur(20px) + 1px white border), solid elsewhere; text on glass stays dark enough' },
|
|
1160
|
+
{ id: 'soft', zh: '柔和立体', en: 'Soft UI', dzh: '同色底、凸起凹陷的柔光阴影', den: 'Same-color surfaces with raised and pressed soft shadows',
|
|
1161
|
+
c: { bg: '#E9EDF2', surface: '#E9EDF2', text: '#2D3440', muted: '#556072', line: '#D3D9E1', primary: '#3E5BD8', accent: '#22A06B', on: '#FFFFFF', link: '#3E5BD8' },
|
|
1162
|
+
head: ROUND, w: 700, radius: 18, shadow: '6px 6px 14px #C8CED6, -6px -6px 14px #FFFFFF',
|
|
1163
|
+
nzh: '卡片和底同色,靠一亮一暗两道阴影凸起,按下用内阴影;容易看不清,所以主按钮一定用主色实底、文字够深;别整屏都是凸起',
|
|
1164
|
+
nen: 'Cards share the background color and rise with a light and a dark shadow; pressed states use inset shadows; it gets low-contrast fast, so the main button is solid primary with strong text; don\'t make everything raised' },
|
|
1165
|
+
]
|
|
1166
|
+
|
|
1167
|
+
/** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
|
|
1168
|
+
function formatStyle(t, pre, restyle) {
|
|
1169
|
+
const en = t('lang') === 'en'
|
|
1170
|
+
const c = pre.c
|
|
1171
|
+
const fonts = t('styleFonts', { head: `${pre.head} · ${pre.w}`, body: SANS })
|
|
1172
|
+
return t('styleText', {
|
|
1173
|
+
name: en ? pre.en : pre.zh,
|
|
1174
|
+
desc: en ? pre.den : pre.dzh,
|
|
1175
|
+
colors: t('styleColors', { ...c, bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1176
|
+
fonts,
|
|
1177
|
+
shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
|
|
1178
|
+
notes: en ? pre.nen : pre.nzh,
|
|
1179
|
+
ask: t(restyle ? 'styleRestyle' : 'styleStart'),
|
|
1180
|
+
})
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// ── 调样式 ───────────────────────────────────────────────────────────────
|
|
1184
|
+
|
|
1185
|
+
const VAR_ONLY = /^var\(\s*(--[\w-]+)\s*\)$/
|
|
1186
|
+
/** 面板上能调的几项:属性 → [文案, 哪一行](上下 / 左右内边距各管两条) */
|
|
1187
|
+
const STYLE_LABEL = { 'font-size': 'styleFont', 'font-weight': 'styleWeight', color: 'styleColor', background: 'styleBg', 'border-radius': 'styleRadius',
|
|
1188
|
+
'padding-top': 'stylePadY', 'padding-bottom': 'stylePadY', 'padding-left': 'stylePadX', 'padding-right': 'stylePadX', gap: 'styleGap' }
|
|
1189
|
+
|
|
1190
|
+
/** 变量名 → 它现在的值(页面 :root 上的) */
|
|
1191
|
+
const tokenVal = (name, tokens) => { const tok = (tokens || []).find((x) => x.name === name); return tok ? String(tok.value).trim() : '' }
|
|
1192
|
+
|
|
1193
|
+
/** 值 → 像素数(var(--x) 先换成变量的值;rem / em 按 16);认不出返回 null */
|
|
1194
|
+
function pxOf(v, tokens) {
|
|
1195
|
+
let s = String(v || '').trim()
|
|
1196
|
+
const m = VAR_ONLY.exec(s)
|
|
1197
|
+
if (m) s = tokenVal(m[1], tokens)
|
|
1198
|
+
const n = parseFloat(s)
|
|
1199
|
+
if (!isFinite(n)) return null
|
|
1200
|
+
return /r?em$/.test(s) ? n * 16 : n
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** 透明的颜色(没设背景时 getComputedStyle 给 rgba(0, 0, 0, 0)) */
|
|
1204
|
+
const clearColor = (v) => /^(transparent|rgba\(.*,\s*0\)|hsla\(.*,\s*0%?\))$/i.test(String(v || '').trim())
|
|
1205
|
+
|
|
1206
|
+
/** 值 → '#rrggbb'(颜色框只认这个);透明的给白色,不然显示成黑的 */
|
|
1207
|
+
const colorHex = (v, tokens) => {
|
|
1208
|
+
const m = VAR_ONLY.exec(String(v || '').trim())
|
|
1209
|
+
const c = m ? tokenVal(m[1], tokens) : v
|
|
1210
|
+
return '#' + (clearColor(c) ? 'ffffff' : hexOf(c))
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** 给人看的值:颜色换成 #RRGGBB,透明的说透明,别的原样 */
|
|
1214
|
+
const showVal = (prop, v, t) => {
|
|
1215
|
+
if (!/^(color|background)$/.test(prop)) return String(v)
|
|
1216
|
+
if (clearColor(v)) return t ? t('styleClear') : 'transparent'
|
|
1217
|
+
return /^(rgb|hsl)/i.test(String(v)) ? '#' + hexOf(v).toUpperCase() : String(v)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** 改了的几项 → 「字号 15px → 17px」这种;上下 / 左右内边距只说一次 */
|
|
1221
|
+
function styleChanges(t, sty) {
|
|
1222
|
+
const out = []
|
|
1223
|
+
const seen = new Set()
|
|
1224
|
+
for (const [k, v] of Object.entries((sty && sty.live) || {})) {
|
|
1225
|
+
const key = STYLE_LABEL[k]
|
|
1226
|
+
if (!key || seen.has(key)) continue
|
|
1227
|
+
seen.add(key)
|
|
1228
|
+
out.push(t('styleArrow', { prop: t(key), from: showVal(k, sty.base[k], t), to: showVal(k, v, t) }))
|
|
1229
|
+
}
|
|
1230
|
+
return out
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const ruleKey = (r) => `${r.href}|${r.sel}|${r.n}`
|
|
1234
|
+
/** 规则在哪:服务端找到了行号就是「app.css 第 42 行」,没找到就是文件名 */
|
|
1235
|
+
function ruleAt(t, r, located) {
|
|
1236
|
+
const hit = located && located.get(ruleKey(r))
|
|
1237
|
+
if (hit && hit.file) return hit.line ? t('ruleAt', { file: hit.file, line: hit.line }) : hit.file
|
|
1238
|
+
try { return decodeURIComponent(new URL(r.href).pathname.split('/').pop()) || r.href } catch { return String(r.href || '') }
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** 改了的这几项现在各由哪条规则决定(同一条规则的合成一行),模型照着去改那条,不用再 grep */
|
|
1242
|
+
function ruleLines(t, sty, located) {
|
|
1243
|
+
const groups = new Map()
|
|
1244
|
+
for (const k of Object.keys((sty && sty.live) || {})) {
|
|
1245
|
+
const r = sty.rules && sty.rules[k]
|
|
1246
|
+
const key = STYLE_LABEL[k]
|
|
1247
|
+
if (!r || !key) continue
|
|
1248
|
+
const g = r.inline ? `inline|${r.sel ? ruleKey(r) : ''}` : ruleKey(r)
|
|
1249
|
+
if (!groups.has(g)) groups.set(g, { r, props: [] })
|
|
1250
|
+
const label = t(key)
|
|
1251
|
+
if (!groups.get(g).props.includes(label)) groups.get(g).props.push(label)
|
|
1252
|
+
}
|
|
1253
|
+
const out = []
|
|
1254
|
+
for (const { r, props } of groups.values()) {
|
|
1255
|
+
const p = props.join('、')
|
|
1256
|
+
if (r.inline) out.push(t('ruleInline', { props: p, over: r.sel ? t('ruleOver', { sel: r.sel, at: ruleAt(t, r, located) }) : '' }))
|
|
1257
|
+
else out.push(t(r.maybe ? 'ruleMaybe' : 'ruleFrom', { props: p, sel: r.sel, at: ruleAt(t, r, located) }))
|
|
1258
|
+
}
|
|
1259
|
+
return out
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** 调好的样式交给模型:哪一屏、哪个元素、改了什么、现在由哪条规则决定(located:服务端找到的行号),让它改进样式表 */
|
|
1263
|
+
function formatStyleAsk(t, sty, st, located) {
|
|
1264
|
+
const rules = ruleLines(t, sty, located)
|
|
1265
|
+
return t('styleAskText', { name: sty.name, sel: sty.selector, list: styleChanges(t, sty).join(t('styleJoin')), rules: rules.length ? `\n${rules.join('\n')}\n` : '', where: whereOf(sty.url, st) })
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ── 设计体检 ─────────────────────────────────────────────────────────────
|
|
1269
|
+
|
|
1270
|
+
/** 一条问题的具体数字(「#C8C8C8 配 #FFFFFF,1.7:1」这种) */
|
|
1271
|
+
function checkDetail(t, it) {
|
|
1272
|
+
const edge = it.edge === 'top' || it.edge === 'bottom' ? it.edge : ''
|
|
1273
|
+
return t('cd_' + it.type, {
|
|
1274
|
+
...it,
|
|
1275
|
+
n: it.sizes ? it.sizes.length : '',
|
|
1276
|
+
sizes: it.sizes ? it.sizes.join(' / ') : '',
|
|
1277
|
+
otherText: String(it.otherText || '').slice(0, 20),
|
|
1278
|
+
edgeName: edge ? t('ckEdge_' + edge) : '',
|
|
1279
|
+
when: edge ? t('ckWhen_' + edge) : '',
|
|
1280
|
+
what: it.type === 'missing' ? t('ckWhat_' + (['css', 'script', 'font'].includes(it.what) ? it.what : 'other')) : '',
|
|
1281
|
+
at: it.type === 'jserr' && it.src ? ` (${it.src}${it.line ? ':' + it.line : ''})` : '',
|
|
1282
|
+
})
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** 量的时候这一屏多大、页面多高、固定栏离内容多远(让模型知道量的是什么) */
|
|
1286
|
+
function sizeLine(t, m) {
|
|
1287
|
+
if (!m || !m.w) return ''
|
|
1288
|
+
let out = t('ckSize', { w: m.w, h: m.h, page: m.page || m.h })
|
|
1289
|
+
for (const b of m.bars || []) if (b.gap != null) out += t('ckSizeBar', { edgeName: t('ckEdge_' + b.edge), bar: b.name, gap: b.gap })
|
|
1290
|
+
return out
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** 体检结果 → 输入框里的一段字:按屏分组,带文件位置、选择器和数字,模型照着改 */
|
|
1294
|
+
function formatCheck(t, list, st, head = 'checkAsk') {
|
|
1295
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
1296
|
+
const lines = [t(head, { n, s: list.length })]
|
|
1297
|
+
for (const sc of list) {
|
|
1298
|
+
if (sc.error && head !== 'checkAsk') lines.push('', t('ckToolErr', { label: sc.label, msg: sc.error }))
|
|
1299
|
+
if (!sc.issues.length) continue
|
|
1300
|
+
const sz = head === 'checkAsk' ? '' : sizeLine(t, sc.metrics)
|
|
1301
|
+
lines.push('', t('ckScreen', { label: sc.label, where: whereOf(sc.url, st) }) + (sz ? ` · ${sz}` : ''))
|
|
1302
|
+
sc.issues.forEach((it, i) => lines.push(t('ckItem', {
|
|
1303
|
+
i: i + 1,
|
|
1304
|
+
type: t('ck_' + it.type),
|
|
1305
|
+
where: it.sel || '',
|
|
1306
|
+
text: it.text ? t('ckQuote', { text: it.text }) : '',
|
|
1307
|
+
detail: checkDetail(t, it),
|
|
1308
|
+
})))
|
|
1309
|
+
}
|
|
1310
|
+
return lines.join('\n') + '\n'
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* 一屏是不是「同一屏的几个版本」之一:home-a / home-b / home-c,或 login / login-b / login-c(和 core.js 的 placeScreen 一个规则)。
|
|
1315
|
+
* 是就给出 { stem, letter, others }(others:同组的其他文件名),不是给 null
|
|
1316
|
+
*/
|
|
1317
|
+
function takesOf(keys, key) {
|
|
1318
|
+
const VAR = /^(.+)-([a-z])\.html?$/i
|
|
1319
|
+
const part = (x) => {
|
|
1320
|
+
const v = VAR.exec(x)
|
|
1321
|
+
return v ? { stem: v[1].toLowerCase(), letter: v[2].toLowerCase() } : { stem: x.toLowerCase().replace(/\.html?$/i, ''), letter: '' }
|
|
1322
|
+
}
|
|
1323
|
+
if (!/\.html?$/i.test(key)) return null
|
|
1324
|
+
const me = part(key)
|
|
1325
|
+
const others = keys.filter((x) => x !== key && /\.html?$/i.test(x) && part(x).stem === me.stem && (me.letter || part(x).letter))
|
|
1326
|
+
if (!others.length || (!me.letter && !others.some((x) => part(x).letter))) return null
|
|
1327
|
+
return { stem: VAR.exec(key) ? VAR.exec(key)[1] : key.replace(/\.html?$/i, ''), letter: me.letter, others }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** 体检结果 → 交给 check_design 工具的一段字(模型自己调的,不带〔设计体检〕开头) */
|
|
1331
|
+
function formatCheckTool(t, list, st) {
|
|
1332
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
1333
|
+
if (n || list.some((x) => x.error)) return formatCheck(t, list, st, 'ckToolHead')
|
|
1334
|
+
return t('ckToolOk', { s: list.length, labels: list.map((x) => { const sz = sizeLine(t, x.metrics); return sz ? `${x.label} ${sz}` : x.label }).join(' / ') })
|
|
1335
|
+
}
|
|
1336
|
+
|
|
803
1337
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
804
1338
|
|
|
805
1339
|
const SHOW_COLS = { app: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
@@ -1137,6 +1671,32 @@ window.__ModuleLoader__.load({
|
|
|
1137
1671
|
return canvasBlob(c)
|
|
1138
1672
|
}
|
|
1139
1673
|
|
|
1674
|
+
/** CSS 颜色 → 'RRGGBB'(PPT 留边的底色);认不出就白色 */
|
|
1675
|
+
function hexOf(color) {
|
|
1676
|
+
try {
|
|
1677
|
+
const c = document.createElement('canvas')
|
|
1678
|
+
c.width = c.height = 1
|
|
1679
|
+
const ctx = c.getContext('2d')
|
|
1680
|
+
ctx.fillStyle = '#ffffff'
|
|
1681
|
+
ctx.fillStyle = color || '#ffffff'
|
|
1682
|
+
ctx.fillRect(0, 0, 1, 1)
|
|
1683
|
+
return [...ctx.getImageData(0, 0, 1, 1).data.slice(0, 3)].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
1684
|
+
} catch { return 'ffffff' }
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** 截图 → JPEG(base64)。PDF / PPT 里一页一张,JPEG 小得多;透明的地方垫白 */
|
|
1688
|
+
async function jpegB64(img) {
|
|
1689
|
+
const c = document.createElement('canvas')
|
|
1690
|
+
c.width = img.width
|
|
1691
|
+
c.height = img.height
|
|
1692
|
+
const ctx = c.getContext('2d')
|
|
1693
|
+
ctx.fillStyle = '#ffffff'
|
|
1694
|
+
ctx.fillRect(0, 0, c.width, c.height)
|
|
1695
|
+
ctx.drawImage(img, 0, 0)
|
|
1696
|
+
const blob = await new Promise((resolve, reject) => c.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/jpeg', 0.9))
|
|
1697
|
+
return blobB64(blob)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1140
1700
|
function blobB64(blob) {
|
|
1141
1701
|
return new Promise((resolve, reject) => {
|
|
1142
1702
|
const r = new FileReader()
|
|
@@ -1468,14 +2028,37 @@ window.__ModuleLoader__.load({
|
|
|
1468
2028
|
* 一屏:几帧 iframe 叠在一起。cur 是正在显示的;比它新的在后台载,载好了淡入换上来,旧的随后拆掉(不白屏)。
|
|
1469
2029
|
* 和页面里 picker.js 的握手:hello → init(上一帧的滚动位置和指纹)→ ready → 换上来 → reveal(闪改动 / 播生成动画)。
|
|
1470
2030
|
*/
|
|
2031
|
+
const LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]|::1)$/i
|
|
2032
|
+
/** 预览在电脑本机,而 DSH 网页是从别的设备打开的 */
|
|
2033
|
+
function away(src) {
|
|
2034
|
+
try { return !!src && LOCAL_HOST.test(new URL(src).hostname) && /^https?:$/.test(location.protocol) && !LOCAL_HOST.test(location.hostname) } catch { return false }
|
|
2035
|
+
}
|
|
2036
|
+
// 远程看(本地文件的预览):服务端把这一屏连同样式、图片打成一份 HTML,放进沙盒 iframe 的 srcdoc
|
|
2037
|
+
const remOf = (p) => (p.remote && away(p.src) ? p.remote : null)
|
|
2038
|
+
const OLD_VER = /^__dsh_design__\/v\/(\d+)\/(.*)$/
|
|
2039
|
+
/** 这一屏(预览服务器上的地址)打包后的取法 */
|
|
2040
|
+
function bundleOf(href, sid) {
|
|
2041
|
+
let p
|
|
2042
|
+
try { p = new URL(href).pathname } catch { return '' }
|
|
2043
|
+
try { p = decodeURIComponent(p) } catch { /* 原样 */ }
|
|
2044
|
+
p = p.replace(/^\/+/, '')
|
|
2045
|
+
let v = ''
|
|
2046
|
+
const m = OLD_VER.exec(p)
|
|
2047
|
+
if (m) { v = m[1]; p = m[2] }
|
|
2048
|
+
if (!p || p.endsWith('/')) p += 'index.html'
|
|
2049
|
+
return `${API}bundle?${new URLSearchParams({ session: sid, key: p, v, parent: location.origin })}`
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1471
2052
|
function Screen(props) {
|
|
1472
2053
|
const src = props.src
|
|
1473
2054
|
const origin = src ? new URL(src).origin : ''
|
|
1474
2055
|
const [frames, setFrames] = useState([])
|
|
1475
2056
|
const [cur, setCur] = useState(0)
|
|
2057
|
+
const [failed, setFailed] = useState(false) // 远程看:打包没取回来
|
|
2058
|
+
const rem = remOf(props)
|
|
1476
2059
|
const wins = useRef(new Map())
|
|
1477
2060
|
const life = useRef(null)
|
|
1478
|
-
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), snapTimer: null }
|
|
2061
|
+
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), snapTimer: null, gen: 0, at: '', deps: null }
|
|
1479
2062
|
const curRef = useRef(0)
|
|
1480
2063
|
const P = useRef(props)
|
|
1481
2064
|
P.current = props
|
|
@@ -1494,6 +2077,16 @@ window.__ModuleLoader__.load({
|
|
|
1494
2077
|
postTo(curRef.current, { type: 'capture', id, ...opts })
|
|
1495
2078
|
setTimeout(() => { if (caps.current.has(id)) { caps.current.delete(id); reject(new Error('timeout')) } }, 30000)
|
|
1496
2079
|
}),
|
|
2080
|
+
// 设计体检:页面里量完回 { issues },问题同时标在页面上(quiet:只要结果,不标)
|
|
2081
|
+
check: (kind, quiet) => new Promise((resolve, reject) => {
|
|
2082
|
+
if (!curRef.current) { reject(new Error('not loaded')); return }
|
|
2083
|
+
const id = `${Date.now()}-${++seq}`
|
|
2084
|
+
caps.current.set(id, { resolve, reject })
|
|
2085
|
+
postTo(curRef.current, { type: 'check', id, kind, quiet: !!quiet })
|
|
2086
|
+
setTimeout(() => { if (caps.current.has(id)) { caps.current.delete(id); reject(new Error('timeout')) } }, 15000)
|
|
2087
|
+
}),
|
|
2088
|
+
checkFocus: (i) => { if (curRef.current) postTo(curRef.current, { type: 'checkFocus', i }) },
|
|
2089
|
+
checkClear: () => { if (curRef.current) postTo(curRef.current, { type: 'checkClear' }) },
|
|
1497
2090
|
}
|
|
1498
2091
|
}
|
|
1499
2092
|
// 登记到右侧栏:导出图片时按屏找到它
|
|
@@ -1507,7 +2100,10 @@ window.__ModuleLoader__.load({
|
|
|
1507
2100
|
|
|
1508
2101
|
const postTo = (id, msg) => {
|
|
1509
2102
|
const f = wins.current.get(id)
|
|
1510
|
-
if (f
|
|
2103
|
+
if (!f || !f.contentWindow) return
|
|
2104
|
+
// 远程看:沙盒里的页面是不透明的源,只能发给 *(发的都是界面指令,没有要保密的)
|
|
2105
|
+
if (f.hasAttribute('srcdoc')) f.contentWindow.postMessage({ dshDesign: 1, ...msg }, '*')
|
|
2106
|
+
else if (origin) f.contentWindow.postMessage({ dshDesign: 1, ...msg }, origin)
|
|
1511
2107
|
}
|
|
1512
2108
|
|
|
1513
2109
|
// 在后台载一帧新的;正在载就记一笔,等它换上来再载
|
|
@@ -1516,13 +2112,44 @@ window.__ModuleLoader__.load({
|
|
|
1516
2112
|
if (L.loading) { L.again = true; return }
|
|
1517
2113
|
const id = L.next++
|
|
1518
2114
|
L.loading = id
|
|
2115
|
+
if (seed) L.seedOf.set(id, seed)
|
|
2116
|
+
const r = remOf(P.current)
|
|
2117
|
+
if (r) {
|
|
2118
|
+
const gen = L.gen
|
|
2119
|
+
fetch(bundleOf(L.at || P.current.src, r.sid), { credentials: 'same-origin', cache: 'no-store' })
|
|
2120
|
+
.then((res) => res.json().then((d) => {
|
|
2121
|
+
if (!res.ok || !d || typeof d.html !== 'string') throw new Error((d && d.error) || `HTTP ${res.status}`)
|
|
2122
|
+
return d
|
|
2123
|
+
}))
|
|
2124
|
+
.then((d) => {
|
|
2125
|
+
if (L.gen !== gen || L.loading !== id) return
|
|
2126
|
+
L.deps = Array.isArray(d.deps) ? d.deps : null
|
|
2127
|
+
L.fails = 0
|
|
2128
|
+
setFailed(false)
|
|
2129
|
+
setFrames((fs) => [...fs, { id, doc: d.html }])
|
|
2130
|
+
})
|
|
2131
|
+
.catch(() => {
|
|
2132
|
+
if (L.gen !== gen || L.loading !== id) return
|
|
2133
|
+
L.loading = 0
|
|
2134
|
+
if (!curRef.current) setFailed(true)
|
|
2135
|
+
// 没取回来:那次改动还没显示出来,过一会儿再试(最多三回),别等下一次改动
|
|
2136
|
+
L.fails = (L.fails || 0) + 1
|
|
2137
|
+
if (L.again || L.fails <= 3) { L.again = false; setTimeout(() => { if (L.gen === gen) load(null) }, 1500 * Math.min(L.fails, 3)) }
|
|
2138
|
+
})
|
|
2139
|
+
return
|
|
2140
|
+
}
|
|
1519
2141
|
let url = P.current.src
|
|
1520
2142
|
try { if (seed && seed.href && new URL(seed.href).origin === origin) url = seed.href } catch { /* 用原地址 */ }
|
|
1521
|
-
if (seed) L.seedOf.set(id, seed)
|
|
1522
2143
|
setFrames((fs) => [...fs, { id, src: url }])
|
|
1523
2144
|
}
|
|
1524
2145
|
|
|
1525
2146
|
const pickMsg = () => ({ type: 'pick', hint: false }) // 提示放在右侧栏(页面顶上会被手机的灵动岛挡住)
|
|
2147
|
+
// 调样式:开模式、选中右侧栏记着的那个元素、把改到一半的样式再套上(新载的一帧要从头告诉它)
|
|
2148
|
+
const styleTo = (id, p) => {
|
|
2149
|
+
postTo(id, { type: 'styleMode', on: true })
|
|
2150
|
+
if (p.styleSel) postTo(id, { type: 'styleSel', selector: p.styleSel })
|
|
2151
|
+
if (p.styleCss) postTo(id, { type: 'style', css: p.styleCss })
|
|
2152
|
+
}
|
|
1526
2153
|
|
|
1527
2154
|
// 一帧好了:淡入换上来
|
|
1528
2155
|
const swap = (id, changes) => {
|
|
@@ -1539,6 +2166,7 @@ window.__ModuleLoader__.load({
|
|
|
1539
2166
|
if (p.picking) postTo(id, pickMsg())
|
|
1540
2167
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1541
2168
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2169
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1542
2170
|
if (first && p.onShown) p.onShown()
|
|
1543
2171
|
if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
|
|
1544
2172
|
L.file = ''
|
|
@@ -1556,16 +2184,21 @@ window.__ModuleLoader__.load({
|
|
|
1556
2184
|
L.again = false
|
|
1557
2185
|
L.seeds.clear()
|
|
1558
2186
|
L.seedOf.clear()
|
|
2187
|
+
L.gen++
|
|
2188
|
+
L.at = ''
|
|
2189
|
+
L.deps = null
|
|
2190
|
+
L.fails = 0
|
|
1559
2191
|
curRef.current = 0
|
|
1560
2192
|
setCur(0)
|
|
1561
2193
|
setFrames([])
|
|
2194
|
+
setFailed(false)
|
|
1562
2195
|
if (src) setTimeout(() => load(null), 0)
|
|
1563
|
-
}, [src])
|
|
2196
|
+
}, [src, !!rem])
|
|
1564
2197
|
|
|
1565
2198
|
// 页面发来的消息(几屏共用一个源,按 iframe 认是不是自己的)
|
|
1566
2199
|
useEffect(() => {
|
|
1567
2200
|
const on = (e) => {
|
|
1568
|
-
if (!origin || e.origin !== origin) return
|
|
2201
|
+
if (rem ? e.origin !== 'null' : !origin || e.origin !== origin) return
|
|
1569
2202
|
let id = 0
|
|
1570
2203
|
for (const [k, f] of wins.current) if (f && f.contentWindow === e.source) { id = k; break }
|
|
1571
2204
|
const d = e.data
|
|
@@ -1594,6 +2227,7 @@ window.__ModuleLoader__.load({
|
|
|
1594
2227
|
if (p.picking) postTo(id, pickMsg())
|
|
1595
2228
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1596
2229
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2230
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1597
2231
|
if (d.changes && d.changes.count && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1598
2232
|
} else if (id > curRef.current) swap(id, d.changes)
|
|
1599
2233
|
break
|
|
@@ -1613,14 +2247,20 @@ window.__ModuleLoader__.load({
|
|
|
1613
2247
|
if (isCur && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1614
2248
|
break
|
|
1615
2249
|
case 'cancelled':
|
|
1616
|
-
if (isCur
|
|
2250
|
+
if (!isCur) break
|
|
2251
|
+
if (d.style) { if (p.onStyleCancel) p.onStyleCancel() } else if (p.onCancelled) p.onCancelled()
|
|
2252
|
+
break
|
|
2253
|
+
case 'stylePick':
|
|
2254
|
+
if (isCur && p.styleEdit && p.onStylePick && d.payload && d.payload.loc) p.onStylePick(d.payload)
|
|
1617
2255
|
break
|
|
1618
2256
|
// 点选、改字只在用户开着对应模式时收(页面自己的脚本也能发这种消息,不能随时往输入框里塞字、往文件里写)
|
|
1619
2257
|
case 'picked':
|
|
1620
2258
|
if (isCur && p.picking && p.onPicked) p.onPicked(d.payload)
|
|
1621
2259
|
break
|
|
1622
2260
|
case 'nav':
|
|
1623
|
-
if (isCur
|
|
2261
|
+
if (!isCur || typeof d.path !== 'string') break
|
|
2262
|
+
if (p.onNav) p.onNav(d.path)
|
|
2263
|
+
else if (remOf(p)) { L.at = origin + d.path; load(null) } // 远程看:页面里点了跳页,这一框换成那一页(和本机预览时 iframe 自己跳过去一样)
|
|
1624
2264
|
break
|
|
1625
2265
|
case 'edited':
|
|
1626
2266
|
if (isCur && (p.textEdit || Date.now() - editOffAt.current < EDIT_GRACE_MS) && p.onEdited && typeof d.before === 'string' && typeof d.after === 'string') p.onEdited(d)
|
|
@@ -1628,6 +2268,7 @@ window.__ModuleLoader__.load({
|
|
|
1628
2268
|
case 'editDone':
|
|
1629
2269
|
if (isCur && p.onEditDone) p.onEditDone()
|
|
1630
2270
|
break
|
|
2271
|
+
case 'checked':
|
|
1631
2272
|
case 'captured': {
|
|
1632
2273
|
const c = caps.current.get(d.id)
|
|
1633
2274
|
if (c) {
|
|
@@ -1642,7 +2283,7 @@ window.__ModuleLoader__.load({
|
|
|
1642
2283
|
}
|
|
1643
2284
|
window.addEventListener('message', on)
|
|
1644
2285
|
return () => window.removeEventListener('message', on)
|
|
1645
|
-
}, [origin, src])
|
|
2286
|
+
}, [origin, src, !!rem])
|
|
1646
2287
|
|
|
1647
2288
|
// 模型在不在干活、画布缩放比例、选取开关:告诉页面
|
|
1648
2289
|
useEffect(() => { if (cur) postTo(cur, { type: 'busy', on: !!props.busy }) }, [!!props.busy, cur])
|
|
@@ -1650,6 +2291,10 @@ window.__ModuleLoader__.load({
|
|
|
1650
2291
|
useEffect(() => { if (cur) postTo(cur, { type: 'zoom', z: zq / 100 }) }, [zq])
|
|
1651
2292
|
useEffect(() => { if (cur) postTo(cur, props.picking ? pickMsg() : { type: 'cancel' }) }, [!!props.picking])
|
|
1652
2293
|
useEffect(() => { if (cur) postTo(cur, { type: 'theme', dark: !!props.dark }) }, [!!props.dark])
|
|
2294
|
+
useEffect(() => { if (cur) { if (props.styleEdit) styleTo(cur, props); else postTo(cur, { type: 'styleMode', on: false }) } }, [!!props.styleEdit])
|
|
2295
|
+
useEffect(() => { if (cur && props.styleEdit) postTo(cur, { type: 'styleSel', selector: props.styleSel || '' }) }, [props.styleSel || ''])
|
|
2296
|
+
const cssSig = JSON.stringify(props.styleCss || {})
|
|
2297
|
+
useEffect(() => { if (cur && props.styleEdit) postTo(cur, { type: 'style', css: props.styleCss || {} }) }, [cssSig])
|
|
1653
2298
|
useEffect(() => { if (cur) postTo(cur, { type: 'demo', on: !!props.demo }) }, [!!props.demo])
|
|
1654
2299
|
useEffect(() => {
|
|
1655
2300
|
// 刚退出改字(点按钮、Esc、模型开始干活):页面会把正在改的那一处交上来,这一小会儿还收
|
|
@@ -1659,17 +2304,46 @@ window.__ModuleLoader__.load({
|
|
|
1659
2304
|
}, [!!props.textEdit])
|
|
1660
2305
|
|
|
1661
2306
|
// 刷新:先问当前这帧要滚动位置和指纹,问不到就直接载
|
|
1662
|
-
const
|
|
1663
|
-
useEffect(() => {
|
|
1664
|
-
if (seq.current === props.reloadSeq) return
|
|
1665
|
-
seq.current = props.reloadSeq
|
|
2307
|
+
const refresh = () => {
|
|
1666
2308
|
const L = life.current
|
|
1667
2309
|
if (!curRef.current) { load(null); return }
|
|
1668
2310
|
clearTimeout(L.snapTimer)
|
|
1669
2311
|
L.snapTimer = setTimeout(() => load(null), 400)
|
|
1670
2312
|
postTo(curRef.current, { type: 'snapshot' })
|
|
2313
|
+
}
|
|
2314
|
+
const seq = useRef(props.reloadSeq)
|
|
2315
|
+
useEffect(() => {
|
|
2316
|
+
if (seq.current === props.reloadSeq) return
|
|
2317
|
+
seq.current = props.reloadSeq
|
|
2318
|
+
refresh()
|
|
1671
2319
|
}, [props.reloadSeq])
|
|
1672
2320
|
|
|
2321
|
+
// 远程看:页面连不上预览服务器、收不到它的改动通知,跟着右侧栏轮询到的「目录里改了哪些文件」重新打包
|
|
2322
|
+
// (改的全是这一屏没用到的就不动;旧版本不会变)。两次轮询之间可能来了好几批:按序号取上次看到之后所有批的并集,
|
|
2323
|
+
// 记录断了档(中间的批已经挤掉了)就不知道改了啥,整屏重打包
|
|
2324
|
+
const changes = (rem && rem.changes) || []
|
|
2325
|
+
const chgN = changes.length ? changes[changes.length - 1].n : 0
|
|
2326
|
+
const chg = rem ? `${rem.at || 0}|${chgN}` : ''
|
|
2327
|
+
const chgWas = useRef(chg)
|
|
2328
|
+
const seenN = useRef(chgN)
|
|
2329
|
+
useEffect(() => {
|
|
2330
|
+
if (chgWas.current === chg) return
|
|
2331
|
+
chgWas.current = chg
|
|
2332
|
+
const from = seenN.current
|
|
2333
|
+
seenN.current = chgN
|
|
2334
|
+
if (!rem || !rem.at) return
|
|
2335
|
+
const L = life.current
|
|
2336
|
+
let path = ''
|
|
2337
|
+
try { path = new URL(L.at || src).pathname.replace(/^\/+/, '') } catch { /* 地址不对 */ }
|
|
2338
|
+
if (OLD_VER.test(path)) return
|
|
2339
|
+
const fresh = changes.filter((c) => c.n > from)
|
|
2340
|
+
const unknown = !changes.length || changes[0].n > from + 1 || fresh.some((c) => !(c.files || []).length)
|
|
2341
|
+
const files = [...new Set(fresh.flatMap((c) => c.files || []))]
|
|
2342
|
+
if (!unknown && L.deps && !files.some((f) => L.deps.includes(f))) return
|
|
2343
|
+
L.file = files[files.length - 1] || L.file
|
|
2344
|
+
refresh()
|
|
2345
|
+
}, [chg])
|
|
2346
|
+
|
|
1673
2347
|
// 页面没装上脚本(极少数,比如不是 HTML):载完等一会儿也换上来
|
|
1674
2348
|
const onFrameLoad = (id) => {
|
|
1675
2349
|
setTimeout(() => {
|
|
@@ -1678,12 +2352,17 @@ window.__ModuleLoader__.load({
|
|
|
1678
2352
|
}, 1600)
|
|
1679
2353
|
}
|
|
1680
2354
|
|
|
2355
|
+
// 预览服务器只听电脑本机(127.0.0.1)。从手机(dsh-mobile)或别的电脑打开 DSH 网页时,这个地址指的是它们自己:
|
|
2356
|
+
// 本地文件的预览打包传过来(上面);开发服务器的没法打包,框里会一直空白,说清楚
|
|
2357
|
+
const tell = (k) => h('div', { className: 'ddp-scr' + (props.dark ? ' dk' : '') }, h('div', { className: 'ddp-away', style: { fontSize: 13 / Math.max(0.25, props.zoom || 1) } }, props.t ? props.t(k) : ZH[k])) // 画布缩小了,字别跟着变小
|
|
2358
|
+
if (away(src) && !rem) return tell('away')
|
|
2359
|
+
if (rem && failed && !cur) return tell('remoteFail')
|
|
1681
2360
|
return h('div', { className: 'ddp-scr' + (props.dark ? ' dk' : '') },
|
|
1682
2361
|
...frames.map((f) => h('iframe', {
|
|
1683
2362
|
key: f.id,
|
|
1684
2363
|
ref: (el) => { if (el) wins.current.set(f.id, el); else wins.current.delete(f.id) },
|
|
1685
2364
|
className: 'ddp-frame' + (f.id === cur ? ' cur' : f.id < cur ? ' old' : ''),
|
|
1686
|
-
src: f.src,
|
|
2365
|
+
...(typeof f.doc === 'string' ? { srcDoc: f.doc, sandbox: 'allow-scripts allow-forms', referrerPolicy: 'no-referrer' } : { src: f.src }),
|
|
1687
2366
|
title: props.label || '',
|
|
1688
2367
|
tabIndex: f.id === cur ? 0 : -1,
|
|
1689
2368
|
'aria-hidden': f.id === cur ? undefined : true,
|
|
@@ -1717,7 +2396,7 @@ window.__ModuleLoader__.load({
|
|
|
1717
2396
|
}
|
|
1718
2397
|
|
|
1719
2398
|
/** 设计规范面板:按组列出变量,点一个放进输入框 */
|
|
1720
|
-
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onClose }) {
|
|
2399
|
+
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onClose }) {
|
|
1721
2400
|
const groups = {}
|
|
1722
2401
|
for (const tok of tokens) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
1723
2402
|
const px = (v) => Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
|
|
@@ -1743,7 +2422,112 @@ window.__ModuleLoader__.load({
|
|
|
1743
2422
|
h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, preview(g, tok.value)),
|
|
1744
2423
|
h('span', { className: 'tx' }, h('span', { className: 'nm' }, tok.name), h('span', { className: 'vl' }, tok.value))))))
|
|
1745
2424
|
: h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onAsk }, t('tokensAsk'))))),
|
|
1746
|
-
onRef ? h('footer', null,
|
|
2425
|
+
onRef || onStyles ? h('footer', null,
|
|
2426
|
+
onStyles ? h('button', { onClick: onStyles }, h(I.sparkle), h('span', null, t('stylesOpen'))) : null,
|
|
2427
|
+
onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('refPick'))) : null) : null)
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/** 设计体检的结果:按屏列出问题(和页面上的编号一样),点一条在页面上滚过去、那个框闪一下 */
|
|
2431
|
+
function CheckPanel({ t, ck, onFocus, onFix, onAgain, onClose }) {
|
|
2432
|
+
const list = ck.list || []
|
|
2433
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
2434
|
+
const multi = list.length > 1
|
|
2435
|
+
return h('div', { className: 'ddp-tokens ddp-check', onPointerDown: (e) => e.stopPropagation() },
|
|
2436
|
+
h('header', null, h(I.stetho), h('b', null, t('check')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
|
|
2437
|
+
h('p', { className: 'tip' }, ck.running ? t('checking') : n ? t('checkSum', { s: list.filter((x) => x.issues.length).length, n }) : t('checkNote')),
|
|
2438
|
+
h('div', { className: 'bd' },
|
|
2439
|
+
...list.filter((sc) => sc.issues.length || sc.error).map((sc) => h('div', { key: sc.key },
|
|
2440
|
+
multi ? h('h4', null, sc.label) : null,
|
|
2441
|
+
sc.error ? h('div', { className: 'none' }, t('checkFail', { msg: sc.error })) : null,
|
|
2442
|
+
...sc.issues.map((it, i) => h('button', { key: i, className: 'ddp-tok ddp-ck', title: it.sel || '', onClick: () => onFocus(sc.key, i) },
|
|
2443
|
+
h('span', { className: 'no' + (it.sev > 1 ? ' hi' : '') }, String(i + 1)),
|
|
2444
|
+
h('span', { className: 'tx' },
|
|
2445
|
+
h('span', { className: 'nm' }, t('ck_' + it.type) + (it.text ? ` · ${it.text}` : '')),
|
|
2446
|
+
h('span', { className: 'vl' }, checkDetail(t, it))))))),
|
|
2447
|
+
!ck.running && !n ? h('div', { className: 'ok' }, h(I.check), h('span', null, t('checkNone'))) : null),
|
|
2448
|
+
n ? h('p', { className: 'tip' }, t('checkNote')) : null,
|
|
2449
|
+
h('footer', { className: 'row' },
|
|
2450
|
+
h('button', { onClick: onAgain, disabled: ck.running }, h(I.refresh), h('span', null, t('checkAgain'))),
|
|
2451
|
+
h('button', { className: 'go', onClick: onFix, disabled: ck.running || !n }, h(I.sparkle), h('span', null, t('checkFix')))))
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* 调样式面板:选中的元素的字号、字重、颜色、背景、圆角、内边距、间距。
|
|
2456
|
+
* 拖的时候页面上先看效果(还没写进文件);设计变量列成小块,点一个就用 var(--x)。
|
|
2457
|
+
* write:{ ok, tip } 能不能写回文件(开发服务器、模型在干活时不能)
|
|
2458
|
+
*/
|
|
2459
|
+
function StylePanel({ t, sty, tokens, write, onSet, onReset, onAsk, onSave, onClose }) {
|
|
2460
|
+
const head = h('header', null, h(I.sliders), h('b', null, t('style')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
|
|
2461
|
+
const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
|
|
2462
|
+
if (!sty) return box(h('p', { className: 'tip' }, t('styleNone')))
|
|
2463
|
+
const groups = {}
|
|
2464
|
+
for (const tok of tokens || []) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
2465
|
+
const cur = (k) => (k in sty.live ? sty.live[k] : sty.base[k])
|
|
2466
|
+
// 现在用的是哪个变量:改过的看改成了什么,没改的看样式表里怎么写的
|
|
2467
|
+
const varOf = (k) => { const m = VAR_ONLY.exec(String(cur(k) || '').trim()); return m ? m[1] : k in sty.live ? '' : (sty.vars || {})[k] || '' }
|
|
2468
|
+
const set = (props, v) => onSet(Object.fromEntries(props.map((p) => [p, v])))
|
|
2469
|
+
const chips = (kind, props) => (groups[kind] || []).slice(0, 10).map((tok) => h('button', {
|
|
2470
|
+
key: tok.name,
|
|
2471
|
+
className: 'chip' + (kind === 'color' ? ' c' : '') + (varOf(props[0]) === tok.name ? ' on' : ''),
|
|
2472
|
+
title: `${tok.name}: ${tok.value}`,
|
|
2473
|
+
onClick: () => set(props, `var(${tok.name})`),
|
|
2474
|
+
}, kind === 'color' ? h('i', { style: { background: tok.value } }) : tok.name.replace(/^--/, '')))
|
|
2475
|
+
const shown = (k, text) => (varOf(k) ? h('em', { className: 'v', title: t('styleVar', { name: varOf(k) }) }, varOf(k)) : h('em', null, text))
|
|
2476
|
+
const slider = (key, props, max, kind) => {
|
|
2477
|
+
const v = Math.round(pxOf(cur(props[0]), tokens) ?? 0)
|
|
2478
|
+
const c = kind ? chips(kind, props) : []
|
|
2479
|
+
return h('div', { className: 'row', key },
|
|
2480
|
+
h('label', null, h('span', null, t(key)), shown(props[0], `${v}px`)),
|
|
2481
|
+
h('input', { type: 'range', min: 0, max: Math.max(max, v), step: 1, value: v, 'aria-label': t(key), onChange: (e) => set(props, `${e.target.value}px`) }),
|
|
2482
|
+
c.length ? h('div', { className: 'chips' }, ...c) : null)
|
|
2483
|
+
}
|
|
2484
|
+
const color = (key, prop, note) => h('div', { className: 'row', key },
|
|
2485
|
+
h('label', null, h('span', null, t(key)), shown(prop, showVal(prop, cur(prop), t))),
|
|
2486
|
+
h('div', { className: 'chips' },
|
|
2487
|
+
h('input', { type: 'color', value: colorHex(cur(prop), tokens), 'aria-label': t(key), onChange: (e) => set([prop], e.target.value) }),
|
|
2488
|
+
...chips('color', [prop])),
|
|
2489
|
+
note ? h('div', { className: 'note' }, note) : null)
|
|
2490
|
+
const w = Math.min(700, Math.max(400, Math.round(parseInt(cur('font-weight'), 10) / 100) * 100 || 400))
|
|
2491
|
+
const n = Object.keys(sty.live).length
|
|
2492
|
+
return box(
|
|
2493
|
+
h('p', { className: 'who', title: sty.selector }, h('code', null, sty.name), sty.text ? ` · ${sty.text}` : ''),
|
|
2494
|
+
h('div', { className: 'bd' },
|
|
2495
|
+
slider('styleFont', ['font-size'], 72, 'size'),
|
|
2496
|
+
h('div', { className: 'row', key: 'w' },
|
|
2497
|
+
h('label', null, h('span', null, t('styleWeight')), h('em', null, String(cur('font-weight')))),
|
|
2498
|
+
h('div', { className: 'seg' }, ...[400, 500, 600, 700].map((x) => h('button', { key: x, className: x === w ? 'on' : '', style: { fontWeight: x }, onClick: () => set(['font-weight'], String(x)) }, t('styleW' + x))))),
|
|
2499
|
+
color('styleColor', 'color'),
|
|
2500
|
+
color('styleBg', 'background', sty.gradient && !('background' in sty.live) ? t('styleGradient') : ''),
|
|
2501
|
+
slider('styleRadius', ['border-radius'], 40, 'radius'),
|
|
2502
|
+
slider('stylePadY', ['padding-top', 'padding-bottom'], 48, 'space'),
|
|
2503
|
+
slider('stylePadX', ['padding-left', 'padding-right'], 48, 'space'),
|
|
2504
|
+
sty.layout ? slider('styleGap', ['gap'], 48, 'space') : null),
|
|
2505
|
+
n ? h('div', { className: 'cnt' }, t('styleN', { n: styleChanges(t, sty).length })) : null,
|
|
2506
|
+
h('footer', { className: 'row' },
|
|
2507
|
+
h('button', { onClick: onReset, disabled: !n }, h('span', null, t('styleReset'))),
|
|
2508
|
+
h('button', { onClick: onAsk, disabled: !n, title: t('styleAskTip') }, h('span', null, t('styleAsk'))),
|
|
2509
|
+
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
/** 一排风格卡:每张是个小样(底色、标题字、卡片、主色按钮),点了交给 onPick */
|
|
2513
|
+
function StyleCards({ t, onPick }) {
|
|
2514
|
+
const en = t('lang') === 'en'
|
|
2515
|
+
return h('div', { className: 'ddp-styles' }, ...STYLES.map((x) => h('button', { key: x.id, className: 'ddp-sc', title: en ? x.den : x.dzh, onClick: () => onPick(x) },
|
|
2516
|
+
h('span', { className: 'pv', style: { background: x.c.bg, backgroundImage: x.bgImage || 'none', color: x.c.text } },
|
|
2517
|
+
h('b', { style: { fontFamily: x.head, fontWeight: x.w } }, en ? 'Aa' : '字'),
|
|
2518
|
+
h('i', { className: 'bt', style: { background: x.c.primary, borderRadius: Math.min(x.radius, 6), boxShadow: x.id === 'playful' ? `1.5px 1.5px 0 ${x.c.line}` : 'none', outline: x.id === 'playful' ? `1px solid ${x.c.line}` : 'none' } }),
|
|
2519
|
+
h('i', { className: 'cd', style: { background: x.id === 'glass' ? 'rgba(255,255,255,.6)' : x.c.surface, borderRadius: Math.min(x.radius, 8) / 2 + 2,
|
|
2520
|
+
boxShadow: x.id === 'soft' ? '2px 2px 5px #C8CED6, -2px -2px 5px #FFFFFF' : x.id === 'playful' ? `2px 2px 0 ${x.c.line}` : x.id === 'editorial' ? `inset 0 1px 0 ${x.c.line}` : `0 0 0 .5px ${x.c.line}` } },
|
|
2521
|
+
h('i', { className: 'ln', style: { background: x.c.text } }), h('i', { className: 'ln s', style: { background: x.c.muted } }))),
|
|
2522
|
+
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/** 换风格面板(设计已经打开时):点一张卡让模型整套换成那个风格 */
|
|
2526
|
+
function StylesPanel({ t, onPick, onClose }) {
|
|
2527
|
+
return h('div', { className: 'ddp-tokens', onPointerDown: (e) => e.stopPropagation() },
|
|
2528
|
+
h('header', null, h(I.palette), h('b', null, t('stylesTitle')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
|
|
2529
|
+
h('p', { className: 'tip' }, t('stylesSwap')),
|
|
2530
|
+
h('div', { className: 'bd' }, h(StyleCards, { t, onPick })))
|
|
1747
2531
|
}
|
|
1748
2532
|
|
|
1749
2533
|
/** 菜单开着时:点外面、按 Esc 关掉 */
|
|
@@ -1770,6 +2554,7 @@ window.__ModuleLoader__.load({
|
|
|
1770
2554
|
items.push(['raw', I.image, t('exRaw')])
|
|
1771
2555
|
if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong')]) // 海报、幻灯片画幅是定的,没有「整页」
|
|
1772
2556
|
if (count > 1) items.push(['show', I.canvas, t('exShow')])
|
|
2557
|
+
items.push(['pdf', I.pdf, t('exPdf')], ['pptx', I.deck, t('exPptx')])
|
|
1773
2558
|
return h('div', { className: 'ddp-kind', ref },
|
|
1774
2559
|
h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('exportT'), disabled, onClick: () => setOpen(!open) }, h(I.download)),
|
|
1775
2560
|
open ? h('div', { className: 'ddp-menu', role: 'menu' },
|
|
@@ -1900,6 +2685,29 @@ window.__ModuleLoader__.load({
|
|
|
1900
2685
|
}
|
|
1901
2686
|
}, [layoutKey])
|
|
1902
2687
|
|
|
2688
|
+
// 让某一屏露出来(体检里点了一条):整屏不在看得见的地方就把镜头挪过去;右边 inset 像素被面板挡着
|
|
2689
|
+
const rv = props.reveal
|
|
2690
|
+
useEffect(() => {
|
|
2691
|
+
const c = camRef.current
|
|
2692
|
+
if (!rv || !L || !c) return
|
|
2693
|
+
const i = screens.findIndex((x) => x.key === rv.key)
|
|
2694
|
+
if (i < 0) return
|
|
2695
|
+
const [cw, ch] = boxes ? boxes[i] : [dw, dh]
|
|
2696
|
+
const vw = Math.max(120, box.w - (props.inset || 0))
|
|
2697
|
+
const x0 = c.x + L.pos[i].x * c.z
|
|
2698
|
+
const y0 = c.y + L.pos[i].y * c.z
|
|
2699
|
+
const w = cw * c.z
|
|
2700
|
+
const hh = ch * c.z
|
|
2701
|
+
let x = c.x
|
|
2702
|
+
let y = c.y
|
|
2703
|
+
if (x0 < 8 || x0 + w > vw - 8) x += (w > vw - 16 ? 8 : (vw - w) / 2) - x0
|
|
2704
|
+
if (y0 < LAB || y0 + hh > box.h - 8) y += (hh > box.h - LAB - 8 ? LAB : (box.h - hh) / 2) - y0
|
|
2705
|
+
if (x === c.x && y === c.y) return
|
|
2706
|
+
auto.current = false
|
|
2707
|
+
setAnim(true)
|
|
2708
|
+
setCam({ ...c, x, y })
|
|
2709
|
+
}, [rv && rv.n])
|
|
2710
|
+
|
|
1903
2711
|
const zoomAt = (factor, px, py, smooth) => {
|
|
1904
2712
|
const c = camRef.current
|
|
1905
2713
|
if (!c) return
|
|
@@ -2037,6 +2845,8 @@ window.__ModuleLoader__.load({
|
|
|
2037
2845
|
useEffect(() => { const tm = setTimeout(() => setWaited(true), 2500); return () => clearTimeout(tm) }, [])
|
|
2038
2846
|
const [picking, setPicking] = useState(false)
|
|
2039
2847
|
const [textEdit, setTextEdit] = useState(false) // 改字:点页面里的文字直接改
|
|
2848
|
+
const [styleEdit, setStyleEdit] = useState(false) // 调样式:点元素、拖滑块,写回它的 style 属性
|
|
2849
|
+
const [sty, setSty] = useState(null) // 调样式选中的元素:{ key, url, name, selector, text, loc, base, vars, gradient, layout, live }
|
|
2040
2850
|
const [editing, setEditing] = useState(false)
|
|
2041
2851
|
const [menu, setMenu] = useState(false)
|
|
2042
2852
|
const [toast, setToast] = useState('')
|
|
@@ -2045,6 +2855,7 @@ window.__ModuleLoader__.load({
|
|
|
2045
2855
|
const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
|
|
2046
2856
|
const [sel, setSel] = useState('')
|
|
2047
2857
|
const [panel, setPanel] = useState(false)
|
|
2858
|
+
const [styles, setStyles] = useState(false) // 换风格面板
|
|
2048
2859
|
const [dropping, setDropping] = useState(false) // 正拖着图片进来
|
|
2049
2860
|
const refInput = useRef(null)
|
|
2050
2861
|
const [exMenu, setExMenu] = useState(false)
|
|
@@ -2058,6 +2869,9 @@ window.__ModuleLoader__.load({
|
|
|
2058
2869
|
const [dark, setDark] = useState(false)
|
|
2059
2870
|
const [phone, setPhone] = useState(false) // 手机上看的面板
|
|
2060
2871
|
const [multi, setMulti] = useState(false) // 多尺寸并排
|
|
2872
|
+
const [ck, setCk] = useState(null) // 设计体检:null = 面板关着;{ running, list: [{ key, label, url, issues, error }] }
|
|
2873
|
+
const [ckCount, setCkCount] = useState(0) // 最近一次体检(模型干完活会悄悄查一次)有几个问题:面板关着时标在按钮上
|
|
2874
|
+
const [reveal, setReveal] = useState(null) // 画布上要露出来的那一屏 { key, n }(体检里点了一条)
|
|
2061
2875
|
const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
|
|
2062
2876
|
const apis = useRef(new Map()) // 屏 → 截图用的接口
|
|
2063
2877
|
const cmpRef = useRef(null)
|
|
@@ -2118,6 +2932,8 @@ window.__ModuleLoader__.load({
|
|
|
2118
2932
|
const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
|
|
2119
2933
|
const anyDark = Object.keys(info).some((k) => k.startsWith(site + '|') && info[k].dark)
|
|
2120
2934
|
const fileTarget = !!(st && st.target && st.target.kind === 'file')
|
|
2935
|
+
// 远程看(手机走外网等):本地文件的预览由服务端打包传过来(见 Screen),目录里改了哪些文件也一起带过去
|
|
2936
|
+
const remote = fileTarget && sid ? { sid, at: st.changedAt || 0, files: st.changedFiles || [], changes: st.changes || [] } : null
|
|
2121
2937
|
|
|
2122
2938
|
// 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
|
|
2123
2939
|
const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
|
|
@@ -2206,7 +3022,7 @@ window.__ModuleLoader__.load({
|
|
|
2206
3022
|
return () => ro.disconnect()
|
|
2207
3023
|
}, [!!url])
|
|
2208
3024
|
|
|
2209
|
-
useEffect(() => { setPicking(false); setTextEdit(false); setSel(''); setVer(null); setCmp(false) }, [site])
|
|
3025
|
+
useEffect(() => { setPicking(false); setTextEdit(false); setStyleEdit(false); setSel(''); setVer(null); setCmp(false) }, [site])
|
|
2210
3026
|
|
|
2211
3027
|
// 版本列表:面板开着时拉一次,存了新版本(vrev 变了)再拉
|
|
2212
3028
|
const vrev = st ? st.vrev : 0
|
|
@@ -2244,6 +3060,7 @@ window.__ModuleLoader__.load({
|
|
|
2244
3060
|
const togglePick = () => {
|
|
2245
3061
|
if (picking) { setPicking(false); return }
|
|
2246
3062
|
setTextEdit(false)
|
|
3063
|
+
setStyleEdit(false)
|
|
2247
3064
|
setPicking(true)
|
|
2248
3065
|
if (!canvas) {
|
|
2249
3066
|
// 单页:把焦点给页面,Esc 在页面里也管用
|
|
@@ -2255,20 +3072,76 @@ window.__ModuleLoader__.load({
|
|
|
2255
3072
|
const toggleText = () => {
|
|
2256
3073
|
if (textEdit) { setTextEdit(false); return }
|
|
2257
3074
|
setPicking(false)
|
|
3075
|
+
setStyleEdit(false)
|
|
2258
3076
|
setTextEdit(true)
|
|
2259
3077
|
}
|
|
2260
3078
|
|
|
2261
|
-
const askText = (sc, d) => insert(t('textAsk', { before: d.before, after: d.after, where: whereOf(sc.url, stRef.current) }), 'insertedSend')
|
|
3079
|
+
const askText = (sc, d) => insert(t('textAsk', { before: d.before, after: d.after, where: whereOf(pageOf(sc, d.href).url, stRef.current) }), 'insertedSend')
|
|
2262
3080
|
|
|
2263
3081
|
const onEdited = (sc, d) => {
|
|
2264
3082
|
if (!fileTarget) return askText(sc, d)
|
|
2265
|
-
post('edit', { session: sid, key: sc.key, before: d.before, after: d.after, nth: d.nth })
|
|
3083
|
+
post('edit', { session: sid, key: pageOf(sc, d.href).key, before: d.before, after: d.after, nth: d.nth })
|
|
2266
3084
|
.then((r) => (r && r.ok ? say(t('textSaved', { file: r.file })) : askText(sc, d)), () => askText(sc, d))
|
|
2267
3085
|
}
|
|
2268
3086
|
|
|
2269
3087
|
// 模型在干活时不能改字(它可能正整份重写文件,改了会被盖掉)
|
|
2270
3088
|
useEffect(() => { if (ver || demo || busy) setTextEdit(false) }, [!!ver, !!demo, busy])
|
|
2271
3089
|
|
|
3090
|
+
// ── 调样式:点一个元素,面板里拖滑块先在页面上看效果;写回文件 = 并进它在源码里的 style 属性 ──
|
|
3091
|
+
const toggleStyle = () => {
|
|
3092
|
+
if (styleEdit) { setStyleEdit(false); return }
|
|
3093
|
+
setPicking(false)
|
|
3094
|
+
setTextEdit(false)
|
|
3095
|
+
setPanel(false)
|
|
3096
|
+
setStyles(false)
|
|
3097
|
+
setHist(false)
|
|
3098
|
+
setPhone(false)
|
|
3099
|
+
closeCheck()
|
|
3100
|
+
setStyleEdit(true)
|
|
3101
|
+
}
|
|
3102
|
+
useEffect(() => { if (!styleEdit) setSty(null) }, [styleEdit])
|
|
3103
|
+
const onStylePick = (sc, p) => {
|
|
3104
|
+
if (canvas) setSel(sc.key)
|
|
3105
|
+
const pg = pageOf(sc, p.href)
|
|
3106
|
+
setSty({ key: pg.key, url: pg.url, name: p.name, selector: p.selector, text: p.text, loc: p.loc, base: p.css || {}, vars: p.vars || {}, rules: p.rules && typeof p.rules === 'object' ? p.rules : {}, gradient: !!p.gradient, layout: !!p.layout, live: {} })
|
|
3107
|
+
}
|
|
3108
|
+
const setStyleProp = (patch) => setSty((c) => (c ? { ...c, live: { ...c.live, ...patch } } : c))
|
|
3109
|
+
// 交给模型:先问服务端那几条规则在源码第几行(本地文件才有;问不到就只写文件名)
|
|
3110
|
+
const askStyle = async () => {
|
|
3111
|
+
const s0 = sty
|
|
3112
|
+
if (!s0) return
|
|
3113
|
+
let located = null
|
|
3114
|
+
const items = [...new Map(Object.keys(s0.live || {}).map((k) => s0.rules && s0.rules[k]).filter((r) => r && r.sel && r.href).map((r) => [`${r.href}|${r.sel}|${r.n}`, r])).values()]
|
|
3115
|
+
if (fileTarget && items.length) {
|
|
3116
|
+
try {
|
|
3117
|
+
const r = await post('locate', { session: sid, items: items.map((x) => ({ href: x.href, sel: x.sel, n: x.n })) })
|
|
3118
|
+
located = new Map(items.map((x, i) => [`${x.href}|${x.sel}|${x.n}`, r && Array.isArray(r.items) ? r.items[i] : null]))
|
|
3119
|
+
} catch { /* 找不到行号就只写文件名 */ }
|
|
3120
|
+
}
|
|
3121
|
+
insert(formatStyleAsk(t, s0, stRef.current, located), 'insertedSend')
|
|
3122
|
+
}
|
|
3123
|
+
const saveStyle = () => {
|
|
3124
|
+
const s0 = sty
|
|
3125
|
+
if (!s0 || !Object.keys(s0.live).length) return
|
|
3126
|
+
if (!fileTarget) return askStyle()
|
|
3127
|
+
const saved = s0.live
|
|
3128
|
+
post('style', { session: sid, key: s0.key, loc: s0.loc, css: saved, name: s0.name }).then((r) => {
|
|
3129
|
+
if (r && r.ok) {
|
|
3130
|
+
say(t('styleSaved', { file: r.file }))
|
|
3131
|
+
// 写进去的算「原样」了;写的时候又拖过的留着
|
|
3132
|
+
setSty((c) => (c && c.key === s0.key && c.selector === s0.selector
|
|
3133
|
+
? { ...c, base: { ...c.base, ...saved }, live: Object.fromEntries(Object.entries(c.live).filter(([k, v]) => saved[k] !== v)) } : c))
|
|
3134
|
+
} else if (r && r.reason === 'busy') say(t('styleBusyTip'))
|
|
3135
|
+
else { say(t('styleMiss')); askStyle() }
|
|
3136
|
+
}, () => askStyle())
|
|
3137
|
+
}
|
|
3138
|
+
useEffect(() => {
|
|
3139
|
+
if (!styleEdit) return undefined
|
|
3140
|
+
const onKey = (e) => { if (e.key === 'Escape') setStyleEdit(false) }
|
|
3141
|
+
window.addEventListener('keydown', onKey)
|
|
3142
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
3143
|
+
}, [styleEdit])
|
|
3144
|
+
|
|
2272
3145
|
useEffect(() => {
|
|
2273
3146
|
if (!dropping) return undefined
|
|
2274
3147
|
const off = () => setDropping(false)
|
|
@@ -2355,13 +3228,18 @@ window.__ModuleLoader__.load({
|
|
|
2355
3228
|
}
|
|
2356
3229
|
|
|
2357
3230
|
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc) },
|
|
2358
|
-
h(Screen, {
|
|
3231
|
+
h(Screen, { t, remote,
|
|
2359
3232
|
src: sc.url,
|
|
2360
3233
|
label: labelOf(sc),
|
|
2361
3234
|
busy: busy && !sc.v,
|
|
2362
3235
|
born: !sc.v && isBorn(sc.key),
|
|
2363
3236
|
picking: picking && !sc.v,
|
|
2364
3237
|
textEdit: textEdit && !sc.v,
|
|
3238
|
+
styleEdit: styleEdit && !sc.v,
|
|
3239
|
+
styleSel: sty && sty.key === sc.key ? sty.selector : '',
|
|
3240
|
+
styleCss: sty && sty.key === sc.key ? sty.live : null,
|
|
3241
|
+
onStylePick: (p) => onStylePick(sc, p),
|
|
3242
|
+
onStyleCancel: () => setStyleEdit(false),
|
|
2365
3243
|
dark,
|
|
2366
3244
|
apis: apis.current,
|
|
2367
3245
|
apiKey: ik(sc),
|
|
@@ -2379,16 +3257,23 @@ window.__ModuleLoader__.load({
|
|
|
2379
3257
|
}))
|
|
2380
3258
|
|
|
2381
3259
|
// ── 导出图片 ──
|
|
2382
|
-
|
|
3260
|
+
// 截一屏:图片和它的 CSS 尺寸(PDF / PPT 按这个定页面大小)
|
|
3261
|
+
// 那一屏的接口(截图、体检);刚载入的等它就绪
|
|
3262
|
+
const apiOf = async (sc) => {
|
|
2383
3263
|
let api = apis.current.get(ik(sc))
|
|
2384
3264
|
for (let i = 0; i < 40 && !(api && api.ready()); i++) {
|
|
2385
3265
|
await new Promise((r) => setTimeout(r, 150))
|
|
2386
3266
|
api = apis.current.get(ik(sc))
|
|
2387
3267
|
}
|
|
2388
3268
|
if (!api) throw new Error('not loaded')
|
|
3269
|
+
return api
|
|
3270
|
+
}
|
|
3271
|
+
const captureOf = async (sc, full) => {
|
|
3272
|
+
const api = await apiOf(sc)
|
|
2389
3273
|
const r = await api.capture({ full: !!full, scale: 2 })
|
|
2390
|
-
return loadImg(r.png)
|
|
3274
|
+
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [] }
|
|
2391
3275
|
}
|
|
3276
|
+
const shotOf = async (sc, full) => (await captureOf(sc, full)).img
|
|
2392
3277
|
|
|
2393
3278
|
const saveImage = async (name, blob) => {
|
|
2394
3279
|
const png = await blobB64(blob)
|
|
@@ -2404,6 +3289,31 @@ window.__ModuleLoader__.load({
|
|
|
2404
3289
|
try {
|
|
2405
3290
|
const prefix = ver ? `v${ver.n}-` : ''
|
|
2406
3291
|
const suffix = dark ? '-dark' : ''
|
|
3292
|
+
if (mode === 'pdf' || mode === 'pptx') {
|
|
3293
|
+
// 每屏一页。PDF 里网页、邮件放整页长图(页面多高就多高);PPT 一张幻灯片一个画面
|
|
3294
|
+
const long = mode === 'pdf' && (kind === 'web' || kind === 'email')
|
|
3295
|
+
const pages = []
|
|
3296
|
+
const keys = []
|
|
3297
|
+
for (const sc of shown) {
|
|
3298
|
+
try {
|
|
3299
|
+
const r = await captureOf(sc, long)
|
|
3300
|
+
pages.push({ jpg: await jpegB64(r.img), w: r.w, h: r.h, title: labelOf(sc), links: r.links })
|
|
3301
|
+
keys.push(sc.key)
|
|
3302
|
+
} catch (e) { console.warn('[dsh-design-studio] 截图失败', sc.key, e) }
|
|
3303
|
+
}
|
|
3304
|
+
if (!pages.length) throw new Error('no screens')
|
|
3305
|
+
// 原型里的跳转:路径换成第几页,PDF / PPT 里点了能跳过去
|
|
3306
|
+
for (const p of pages) {
|
|
3307
|
+
p.links = p.links.map((l) => ({ x: l.x, y: l.y, w: l.w, h: l.h, to: keys.indexOf(keyOfPath(l.path, fileTarget)) })).filter((l) => l.to >= 0)
|
|
3308
|
+
}
|
|
3309
|
+
const sp = splitTitles(pages.map((x) => x.title))
|
|
3310
|
+
const folder = st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : ''
|
|
3311
|
+
const title = sp.brand || folder || st.label || ''
|
|
3312
|
+
const r = await post('doc', { session: sid, format: mode, name: prefix + (title || 'design') + suffix, title, bg: hexOf(pickColors(tokens).bg), pages })
|
|
3313
|
+
const file = String(r.path || '').split(/[\\/]/).pop()
|
|
3314
|
+
say(t('exportedDoc', { name: file, n: pages.length }), { label: t('reveal'), run: () => post('reveal', { path: r.path }).catch(() => {}) })
|
|
3315
|
+
return
|
|
3316
|
+
}
|
|
2407
3317
|
if (mode === 'show') {
|
|
2408
3318
|
const shots = []
|
|
2409
3319
|
for (const sc of shown) {
|
|
@@ -2459,7 +3369,17 @@ window.__ModuleLoader__.load({
|
|
|
2459
3369
|
insert(t('variantText', { name: labelOf(sc), b, where: whereOf(sc.url, stRef.current) }), 'insertedSend')
|
|
2460
3370
|
}
|
|
2461
3371
|
|
|
3372
|
+
// 同一屏的几个版本(开局的三个方向 home-a / b / c,或「再出两版」的 login / login-b / login-c):挑一个
|
|
3373
|
+
const pickDirection = (sc) => {
|
|
3374
|
+
const g = takesOf(shown.map((x) => x.key), sc.key)
|
|
3375
|
+
if (!g) return
|
|
3376
|
+
const others = g.others.join('、')
|
|
3377
|
+
const where = whereOf(sc.url, stRef.current)
|
|
3378
|
+
insert(g.letter ? t('pickText', { name: labelOf(sc), where, others, base: g.stem }) : t('pickKeep', { name: labelOf(sc), where, others }), 'insertedSend')
|
|
3379
|
+
}
|
|
3380
|
+
|
|
2462
3381
|
const actions = (sc) => [
|
|
3382
|
+
!sc.v && takesOf(shown.map((x) => x.key), sc.key) ? h('button', { key: 'p', className: 'ddp-btn', title: t('pickThis'), onClick: () => pickDirection(sc) }, h(I.check)) : null,
|
|
2463
3383
|
h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download)),
|
|
2464
3384
|
sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle)),
|
|
2465
3385
|
h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single)),
|
|
@@ -2495,9 +3415,95 @@ window.__ModuleLoader__.load({
|
|
|
2495
3415
|
const hintTop = (picking || textEdit) && !canvas && !inMulti && !ver && !demo && kind !== 'web'
|
|
2496
3416
|
const flows = canvas && !inMulti && !picking && !textEdit ? flowsOf(shown, (sc) => (infoOf(ik(sc)) || {}).links, fileTarget) : []
|
|
2497
3417
|
|
|
3418
|
+
// ── 设计体检:画布上量所有屏,单页量这一屏;问题标在页面上,列在面板里 ──
|
|
3419
|
+
const closeCheck = () => {
|
|
3420
|
+
if (ck) for (const sc of ck.list || []) { const a = apis.current.get(ik(sc)); if (a) a.checkClear() }
|
|
3421
|
+
setCk(null)
|
|
3422
|
+
}
|
|
3423
|
+
const checkList = () => (canvas ? shown : [shown.find((x) => x.key === focus) || { key: focus, url, label: st.label }])
|
|
3424
|
+
// 几屏一起量(各量各的 iframe),一屏卡住不拖累别的
|
|
3425
|
+
const measure = (list, quiet) => Promise.all(list.map(async (sc) => {
|
|
3426
|
+
const row = { key: sc.key, v: sc.v, label: labelOf(sc), url: sc.url, issues: [] }
|
|
3427
|
+
try {
|
|
3428
|
+
const r = await (await apiOf(sc)).check(kind, quiet)
|
|
3429
|
+
row.issues = Array.isArray(r.issues) ? r.issues : []
|
|
3430
|
+
if (r.metrics && typeof r.metrics === 'object') row.metrics = r.metrics
|
|
3431
|
+
} catch (e) { row.error = e.message }
|
|
3432
|
+
return row
|
|
3433
|
+
}))
|
|
3434
|
+
const total = (list) => list.reduce((m, x) => m + x.issues.length, 0)
|
|
3435
|
+
const runCheck = async () => {
|
|
3436
|
+
setPanel(false); setHist(false); setPhone(false)
|
|
3437
|
+
setCk({ running: true, list: [] })
|
|
3438
|
+
const out = await measure(checkList(), false)
|
|
3439
|
+
setCk({ running: false, list: out })
|
|
3440
|
+
setCkCount(total(out))
|
|
3441
|
+
// 有问题的第一屏被面板挡着、在画布外面:挪过来
|
|
3442
|
+
const first = out.find((x) => x.issues.length)
|
|
3443
|
+
if (canvas && first) setReveal((r) => ({ key: first.key, n: ((r && r.n) || 0) + 1 }))
|
|
3444
|
+
return out
|
|
3445
|
+
}
|
|
3446
|
+
const focusIssue = (key, i) => {
|
|
3447
|
+
const sc = (ck && ck.list.find((x) => x.key === key)) || null
|
|
3448
|
+
if (!sc) return
|
|
3449
|
+
if (canvas) { setSel(key); setReveal((r) => ({ key, n: ((r && r.n) || 0) + 1 })) }
|
|
3450
|
+
const a = apis.current.get(ik(sc))
|
|
3451
|
+
if (a) a.checkFocus(i)
|
|
3452
|
+
}
|
|
3453
|
+
// 打开一套设计、模型又干完一轮(turns 变了):面板开着就再查一次(改没改好一眼看到);关着就悄悄量一下,问题数标在按钮上。
|
|
3454
|
+
// 模型干着活时不量;等一会儿再量:刚写完的屏还在刷新、播生成动画
|
|
3455
|
+
const autoCk = useRef(null)
|
|
3456
|
+
const ckKey = url && !busy ? `${site}|${(st && st.turns) || 0}` : ''
|
|
3457
|
+
const seenCk = useRef('')
|
|
3458
|
+
useEffect(() => {
|
|
3459
|
+
if (!ckKey || ckKey === seenCk.current) return undefined
|
|
3460
|
+
seenCk.current = ckKey
|
|
3461
|
+
const timer = setTimeout(async () => {
|
|
3462
|
+
const a = autoCk.current
|
|
3463
|
+
if (a.off) return
|
|
3464
|
+
if (a.ck && !a.ck.running) return a.run()
|
|
3465
|
+
if (a.ck) return
|
|
3466
|
+
const out = await a.measure(a.list(), true)
|
|
3467
|
+
if (!autoCk.current.ck) setCkCount(total(out))
|
|
3468
|
+
}, 2500)
|
|
3469
|
+
return () => clearTimeout(timer)
|
|
3470
|
+
}, [ckKey])
|
|
3471
|
+
useEffect(() => { setCkCount(0) }, [site])
|
|
3472
|
+
// 模型调了 check_design(轮询里出现新的 checkReq):量完把清单交回去;面板开着就顺手刷新面板,关着就只更新按钮上的数。
|
|
3473
|
+
// 稍等一下再量:模型刚写完的屏还在刷新
|
|
3474
|
+
const ckReq = (st && st.checkReq) || ''
|
|
3475
|
+
const doneReq = useRef('')
|
|
3476
|
+
useEffect(() => {
|
|
3477
|
+
if (!ckReq || ckReq === doneReq.current) return undefined
|
|
3478
|
+
doneReq.current = ckReq
|
|
3479
|
+
const timer = setTimeout(async () => {
|
|
3480
|
+
const a = autoCk.current
|
|
3481
|
+
let body
|
|
3482
|
+
if (a.off) body = { error: t('ckToolOff') }
|
|
3483
|
+
else {
|
|
3484
|
+
const out = a.ck && !a.ck.running ? await a.run() : await a.measure(a.list(), true)
|
|
3485
|
+
if (!autoCk.current.ck) setCkCount(total(out))
|
|
3486
|
+
// measured:真量到的屏数。一屏都没量到(这台设备上的预览不回话)时服务端先不收,等别的设备上的右侧栏
|
|
3487
|
+
body = { text: formatCheckTool(t, out, stRef.current), measured: out.filter((x) => !x.error).length }
|
|
3488
|
+
}
|
|
3489
|
+
post('check', { session: sid, id: ckReq, ...body }).catch(() => {})
|
|
3490
|
+
}, 1500)
|
|
3491
|
+
return () => clearTimeout(timer)
|
|
3492
|
+
}, [ckReq])
|
|
3493
|
+
const fixIssues = () => {
|
|
3494
|
+
if (!ck) return
|
|
3495
|
+
insert(formatCheck(t, ck.list, stRef.current), 'insertedSend')
|
|
3496
|
+
closeCheck()
|
|
3497
|
+
}
|
|
3498
|
+
const checkOff = !url || !!ver || !!demo || inMulti
|
|
3499
|
+
const styleOff = !url || !!ver || !!demo || inMulti || busy
|
|
3500
|
+
useEffect(() => { if (styleOff) setStyleEdit(false) }, [styleOff])
|
|
3501
|
+
autoCk.current = { ck, off: checkOff, run: runCheck, list: checkList, measure }
|
|
3502
|
+
useEffect(() => { if (checkOff && ck) closeCheck() }, [checkOff])
|
|
3503
|
+
|
|
2498
3504
|
const toggleDemo = () => (demo ? setDemo(null) : startDemo())
|
|
2499
|
-
const toggleHist = () => { setHist(!hist); setPanel(false); setPhone(false) }
|
|
2500
|
-
const togglePhone = () => { setPhone(!phone); setPanel(false); setHist(false) }
|
|
3505
|
+
const toggleHist = () => { setHist(!hist); setPanel(false); setStyles(false); setPhone(false); closeCheck(); setStyleEdit(false) }
|
|
3506
|
+
const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
|
|
2501
3507
|
const reload = () => setReloadSeq((x) => x + 1)
|
|
2502
3508
|
|
|
2503
3509
|
const bar = h('div', { className: 'ddp-bar' },
|
|
@@ -2509,6 +3515,7 @@ window.__ModuleLoader__.load({
|
|
|
2509
3515
|
h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo, onClick: togglePick },
|
|
2510
3516
|
h(I.pick), h('span', null, picking ? t('picking') : t('pick'))),
|
|
2511
3517
|
h('button', { className: 'ddp-btn' + (textEdit ? ' on' : ''), title: t('textEdit'), disabled: !url || !!ver || !!demo || busy, onClick: toggleText }, h(I.text)),
|
|
3518
|
+
h('button', { className: 'ddp-btn' + (styleEdit ? ' on' : ''), title: t('styleEdit'), disabled: styleOff, onClick: toggleStyle }, h(I.sliders)),
|
|
2512
3519
|
h('span', { className: 'ddp-sep' }),
|
|
2513
3520
|
h('div', { className: 'ddp-seg' },
|
|
2514
3521
|
h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
|
|
@@ -2516,15 +3523,18 @@ window.__ModuleLoader__.load({
|
|
|
2516
3523
|
h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
|
|
2517
3524
|
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver, onClick: toggleDemo }, h(I.play)),
|
|
2518
3525
|
h(KindMenu, { t, kind, open: menu, setOpen: setMenu, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
|
|
2519
|
-
h('button', { className: 'ddp-btn' + (panel ? ' sel' : '') + (tokens.length && !panel ? ' has' : ''), title: t('tokens'), onClick: () => { setPanel(!panel); setHist(false); setPhone(false) } }, h(I.palette)),
|
|
3526
|
+
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)),
|
|
3527
|
+
h('button', { className: 'ddp-btn o2' + (ck ? ' sel' : ''), title: ckCount && !ck ? t('checkCount', { n: ckCount }) : t('checkTip'), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
|
|
3528
|
+
h(I.stetho), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
|
|
2520
3529
|
h('button', { className: 'ddp-btn o2' + (hist || ver ? ' sel' : ''), title: t('history'), disabled: !fileTarget, onClick: toggleHist }, h(I.history)),
|
|
2521
3530
|
h(ExportMenu, { t, kind, count: shown.length, open: exMenu, setOpen: setExMenu, onPick: (m) => doExport(m), disabled: !url }),
|
|
2522
3531
|
anyDark || dark ? h('button', { className: 'ddp-btn o1' + (dark ? ' sel' : ''), title: t('dark'), onClick: () => setDark(!dark) }, h(I.moon)) : null,
|
|
2523
3532
|
h('button', { className: 'ddp-btn o1' + (phone ? ' sel' : '') + (phoneOn && !phone ? ' has' : ''), title: t('phone'), disabled: !url, onClick: togglePhone }, h(I.scan)),
|
|
2524
3533
|
h('button', { className: 'ddp-btn o1', title: t('refresh'), onClick: reload }, h(I.refresh)),
|
|
2525
3534
|
h('a', { className: 'ddp-btn o1', title: t('external'), href: url, target: '_blank', rel: 'noopener noreferrer' }, h(I.external)),
|
|
2526
|
-
h(MoreMenu, { t, open: more, setOpen: setMore, has: phoneOn && !phone, items: [
|
|
3535
|
+
h(MoreMenu, { t, open: more, setOpen: setMore, has: (phoneOn && !phone) || (!!ckCount && !ck && !checkOff), items: [
|
|
2527
3536
|
['o2', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo],
|
|
3537
|
+
['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
|
|
2528
3538
|
['o2', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
|
|
2529
3539
|
anyDark || dark ? ['o1', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
|
|
2530
3540
|
['o1', I.scan, t('phone'), phone, !url, togglePhone],
|
|
@@ -2532,8 +3542,11 @@ window.__ModuleLoader__.load({
|
|
|
2532
3542
|
['o1', I.external, t('external'), false, !url, url],
|
|
2533
3543
|
] }))
|
|
2534
3544
|
|
|
2535
|
-
|
|
2536
|
-
|
|
3545
|
+
// 模型用提问卡问你、在等回答:不转圈,换成一个橙点
|
|
3546
|
+
const busyChip = (cls) => st && st.asking
|
|
3547
|
+
? h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'q' }), h('span', null, t('asking')))
|
|
3548
|
+
: h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'sp' }),
|
|
3549
|
+
h('span', { className: 'tx' }, st && st.writing ? t('writing', { file: st.writing }) : t('working')))
|
|
2537
3550
|
|
|
2538
3551
|
// ── 参考图:拖进来 / 粘贴 / 从设计规范面板选;存进设计目录的 refs/,尺寸和主要颜色放进输入框 ──
|
|
2539
3552
|
const addRef = async (file) => {
|
|
@@ -2602,6 +3615,7 @@ window.__ModuleLoader__.load({
|
|
|
2602
3615
|
onClick: () => post('open', { session: sid, target: st.hint }).then((s) => { if (poll) poll.set(s) }, (e) => say(t('openFailed', { msg: e.message }))),
|
|
2603
3616
|
}, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
|
|
2604
3617
|
h('ol', { className: 'ddp-steps' }, h('li', null, t('step1')), h('li', null, t('step2')), h('li', null, t('step3'))),
|
|
3618
|
+
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)) })),
|
|
2605
3619
|
h('p', null, t('emptyHint')),
|
|
2606
3620
|
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
2607
3621
|
h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose')))),
|
|
@@ -2622,7 +3636,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
3636
|
h('div', { className: 'ddp-single' },
|
|
2623
3637
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2624
3638
|
h(Device, { kind, w: size[0], h: size[1], fill, label },
|
|
2625
|
-
h(Screen, {
|
|
3639
|
+
h(Screen, { t, remote,
|
|
2626
3640
|
src: urlOfKey(l.key),
|
|
2627
3641
|
label,
|
|
2628
3642
|
busy: false,
|
|
@@ -2656,7 +3670,7 @@ window.__ModuleLoader__.load({
|
|
|
2656
3670
|
sel: '',
|
|
2657
3671
|
labelOf: (sc) => sc.label,
|
|
2658
3672
|
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base) },
|
|
2659
|
-
h(Screen, {
|
|
3673
|
+
h(Screen, { t, remote,
|
|
2660
3674
|
src: base.url,
|
|
2661
3675
|
label: labelOf(base),
|
|
2662
3676
|
busy,
|
|
@@ -2685,7 +3699,7 @@ window.__ModuleLoader__.load({
|
|
|
2685
3699
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
2686
3700
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
2687
3701
|
const tagAt = (side) => ({ top: kind === 'app' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
2688
|
-
const screenOf = (sc) => h(Screen, { src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
3702
|
+
const screenOf = (sc) => h(Screen, { t, remote, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
2689
3703
|
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk) })
|
|
2690
3704
|
body = h('div', { className: 'ddp-single' },
|
|
2691
3705
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
@@ -2707,9 +3721,11 @@ window.__ModuleLoader__.load({
|
|
|
2707
3721
|
kind,
|
|
2708
3722
|
size,
|
|
2709
3723
|
ghost,
|
|
2710
|
-
picking: picking || textEdit,
|
|
3724
|
+
picking: picking || textEdit || styleEdit,
|
|
2711
3725
|
sel: sel || focus,
|
|
2712
3726
|
labelOf,
|
|
3727
|
+
reveal,
|
|
3728
|
+
inset: ck || styleEdit || panel || styles ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
2713
3729
|
render: (sc, z) => renderScreen(sc, z, false),
|
|
2714
3730
|
flows,
|
|
2715
3731
|
onSelect: setSel,
|
|
@@ -2718,16 +3734,18 @@ window.__ModuleLoader__.load({
|
|
|
2718
3734
|
} else {
|
|
2719
3735
|
const fill = kind === 'web'
|
|
2720
3736
|
const [dw, dh] = deviceBox(kind, size)
|
|
2721
|
-
|
|
3737
|
+
// 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
|
|
3738
|
+
const aside = !fill && (ck || styleEdit || panel || styles) && box.w - 296 >= 240 ? 296 : 0
|
|
3739
|
+
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - aside - 32) / dw, (box.h - 32 - (ver || hintTop ? 46 : 0)) / dh))
|
|
2722
3740
|
const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
|
|
2723
|
-
body = h('div', { className: 'ddp-single' },
|
|
3741
|
+
body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
|
|
2724
3742
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2725
3743
|
renderScreen(one, z, fill)))
|
|
2726
3744
|
}
|
|
2727
3745
|
|
|
2728
3746
|
let chip = null
|
|
2729
3747
|
const side = canvas || inMulti ? ' left' : ''
|
|
2730
|
-
const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : ''
|
|
3748
|
+
const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : styleEdit && !sty ? t(canvas ? 'styleAny' : 'styleOne') : ''
|
|
2731
3749
|
if (flash) chip = h('div', { className: 'ddp-chip ok' + side, key: 'ok' }, h(I.check), h('span', null, flash))
|
|
2732
3750
|
else if (busy) chip = busyChip(side)
|
|
2733
3751
|
|
|
@@ -2737,8 +3755,23 @@ window.__ModuleLoader__.load({
|
|
|
2737
3755
|
h('div', { className: 'ddp-body' + (ver || demo || hintTop ? ' vb' : '') }, body),
|
|
2738
3756
|
busy ? h('div', { className: 'ddp-glow' }) : null,
|
|
2739
3757
|
busy ? h('div', { className: 'ddp-ring' }) : null,
|
|
2740
|
-
|
|
2741
|
-
|
|
3758
|
+
// 右边开着面板时提示条居中在面板左边那块,别被面板盖住一半
|
|
3759
|
+
hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
|
|
3760
|
+
ck ? h(CheckPanel, { t, ck, onFocus: focusIssue, onFix: fixIssues, onAgain: runCheck, onClose: closeCheck }) : null,
|
|
3761
|
+
styleEdit ? h(StylePanel, {
|
|
3762
|
+
t,
|
|
3763
|
+
sty,
|
|
3764
|
+
tokens: (sty && (infoOf(sty.key) || {}).tokens) || tokens,
|
|
3765
|
+
write: !fileTarget ? { ok: false, tip: t('styleNoWrite') } : busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('styleSaveTip') },
|
|
3766
|
+
onSet: setStyleProp,
|
|
3767
|
+
onReset: () => setSty((c) => (c ? { ...c, live: {} } : c)),
|
|
3768
|
+
onAsk: askStyle,
|
|
3769
|
+
onSave: saveStyle,
|
|
3770
|
+
onClose: () => setStyleEdit(false),
|
|
3771
|
+
}) : null,
|
|
3772
|
+
panel ? h(TokensPanel, { t, tokens, onPick: (tok) => insert(formatToken(tok, stRef.current)), onAsk: () => insert(t('tokensAskText') + '\n', 'insertedSend'), onRef: pickRef,
|
|
3773
|
+
onStyles: () => { setPanel(false); setStyles(true) }, onClose: () => setPanel(false) }) : null,
|
|
3774
|
+
styles ? h(StylesPanel, { t, onPick: (x) => { setStyles(false); insert(formatStyle(t, x, true), 'insertedSend') }, onClose: () => setStyles(false) }) : null,
|
|
2742
3775
|
phone ? h(PhonePanel, {
|
|
2743
3776
|
t,
|
|
2744
3777
|
sid,
|
|
@@ -2837,7 +3870,7 @@ window.__ModuleLoader__.load({
|
|
|
2837
3870
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
2838
3871
|
exports.inject = INJECT
|
|
2839
3872
|
// 测试用
|
|
2840
|
-
exports._test = { poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
3873
|
+
exports._test = { STYLES, formatStyle, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
2841
3874
|
return module.exports
|
|
2842
3875
|
},
|
|
2843
3876
|
})
|