dsh-design-studio 0.4.1 → 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 +1067 -46
- 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/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,9 +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: '在手机上看',
|
|
147
|
-
away: '
|
|
264
|
+
away: '这里看不到预览:开发服务器跑在电脑本机上,从手机或别的电脑打开 DSH 时连不过去(本地 HTML 文件的设计能看)。回到那台电脑上看;想在手机上看,就在电脑上点「在手机上看」扫码(同一个 Wi-Fi)。',
|
|
265
|
+
remoteFail: '这一屏没能传过来(连接断了,或图片太大)。点「刷新」再试。',
|
|
148
266
|
phoneStarting: '正在打开…',
|
|
149
267
|
phoneHow: '用手机相机扫一扫',
|
|
150
268
|
phoneWifi: '手机和电脑要连同一个 Wi-Fi',
|
|
@@ -170,6 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
170
288
|
demoShort: '演示',
|
|
171
289
|
editAddr: '换一个地址',
|
|
172
290
|
working: '模型正在工作',
|
|
291
|
+
asking: '模型在等你回答(在对话里)',
|
|
173
292
|
writing: '正在写 {file}',
|
|
174
293
|
born: '已生成 {name}',
|
|
175
294
|
updated: '已更新{file}',
|
|
@@ -245,12 +364,75 @@ window.__ModuleLoader__.load({
|
|
|
245
364
|
gRadius: 'Radii',
|
|
246
365
|
gShadow: 'Shadows',
|
|
247
366
|
gOther: 'Other',
|
|
248
|
-
exportT: 'Export
|
|
367
|
+
exportT: 'Export',
|
|
249
368
|
exFrame: 'This screen · in frame',
|
|
250
369
|
exRaw: 'This screen · plain',
|
|
251
370
|
exLong: 'This screen · full page',
|
|
252
371
|
exShow: 'Showcase of all screens',
|
|
253
|
-
|
|
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}"',
|
|
254
436
|
exporting: 'Exporting…',
|
|
255
437
|
exported: 'Exported {name}',
|
|
256
438
|
exportedCopied: 'Exported {name} and copied it',
|
|
@@ -278,6 +460,9 @@ window.__ModuleLoader__.load({
|
|
|
278
460
|
cmpNow: 'Now',
|
|
279
461
|
dark: 'Dark mode',
|
|
280
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',
|
|
281
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',
|
|
282
467
|
exThis: 'Export this screen',
|
|
283
468
|
demo: 'Play: tap buttons in the page to move between screens',
|
|
@@ -288,9 +473,61 @@ window.__ModuleLoader__.load({
|
|
|
288
473
|
textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
|
|
289
474
|
textSaved: 'Saved to {file}',
|
|
290
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',
|
|
291
527
|
demoNone: 'This screen has no links to other screens yet. Ask the model to connect screens with <a href="detail.html">',
|
|
292
528
|
phone: 'View on phone',
|
|
293
|
-
away: 'The
|
|
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.',
|
|
294
531
|
phoneStarting: 'Starting…',
|
|
295
532
|
phoneHow: 'Scan with your phone camera',
|
|
296
533
|
phoneWifi: 'Phone and computer must be on the same Wi-Fi',
|
|
@@ -316,6 +553,7 @@ window.__ModuleLoader__.load({
|
|
|
316
553
|
demoShort: 'Play',
|
|
317
554
|
editAddr: 'Change address',
|
|
318
555
|
working: 'The model is working',
|
|
556
|
+
asking: 'The model is waiting for your answer (in the chat)',
|
|
319
557
|
writing: 'Writing {file}',
|
|
320
558
|
born: 'Generated {name}',
|
|
321
559
|
updated: 'Updated{file}',
|
|
@@ -434,6 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
434
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;
|
|
435
673
|
background:rgba(18,18,24,.84);color:#ececf0;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
436
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)}
|
|
437
676
|
@property --ddp-a{syntax:'<angle>';initial-value:0deg;inherits:false}
|
|
438
677
|
.ddp-ring{position:absolute;inset:0;z-index:4;pointer-events:none;padding:2.5px;
|
|
439
678
|
background:conic-gradient(from var(--ddp-a),transparent 0deg 140deg,${ACCENT} 240deg,#a78bfa 300deg,#5eead4 340deg,transparent 360deg);
|
|
@@ -451,6 +690,7 @@ window.__ModuleLoader__.load({
|
|
|
451
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}
|
|
452
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}
|
|
453
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)}
|
|
454
694
|
@keyframes ddp-rot{to{transform:rotate(360deg)}}
|
|
455
695
|
@keyframes ddp-txt{from{background-position:100% 0}to{background-position:-160% 0}}
|
|
456
696
|
@keyframes ddp-blink{50%{opacity:.25}}
|
|
@@ -528,6 +768,54 @@ window.__ModuleLoader__.load({
|
|
|
528
768
|
.ddp-empty .ddp-refline{margin-top:0}
|
|
529
769
|
.ddp-empty .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
|
|
530
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)}
|
|
531
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}
|
|
532
820
|
.ddp-tok:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
533
821
|
.ddp-tok .pv{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
|
@@ -542,6 +830,7 @@ window.__ModuleLoader__.load({
|
|
|
542
830
|
.ddp-tok .vl{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}
|
|
543
831
|
.ddp-btn.has::after{content:"";position:absolute;right:4px;top:4px;width:5px;height:5px;border-radius:50%;background:${ACCENT}}
|
|
544
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}
|
|
545
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}
|
|
546
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}
|
|
547
836
|
.ddp-ver:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
@@ -635,6 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
635
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' }),
|
|
636
925
|
h('rect', { key: 3, x: 16.5, y: 8, width: 5, height: 10.5, rx: 1.2 })]),
|
|
637
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 })]),
|
|
638
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' })]),
|
|
639
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 })]),
|
|
640
930
|
play: () => svg([h('path', { key: 1, d: 'M8 5.5v13l10.5-6.5z', strokeLinejoin: 'round' })]),
|
|
@@ -647,6 +937,9 @@ window.__ModuleLoader__.load({
|
|
|
647
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' })]),
|
|
648
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' })]),
|
|
649
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' })]),
|
|
650
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),
|
|
651
944
|
}
|
|
652
945
|
// 引导页上的大图标
|
|
@@ -719,6 +1012,19 @@ window.__ModuleLoader__.load({
|
|
|
719
1012
|
// ── 选中内容 → 输入框里的一段字 ──────────────────────────────────────────
|
|
720
1013
|
|
|
721
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
|
+
|
|
722
1028
|
function whereOf(href, st) {
|
|
723
1029
|
try {
|
|
724
1030
|
const u = new URL(href)
|
|
@@ -803,6 +1109,231 @@ window.__ModuleLoader__.load({
|
|
|
803
1109
|
return `〔设计变量〕${tok.name}: ${tok.value}\n${whereOf(tok.href, st)}\n`
|
|
804
1110
|
}
|
|
805
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
|
+
|
|
806
1337
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
807
1338
|
|
|
808
1339
|
const SHOW_COLS = { app: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
@@ -1140,6 +1671,32 @@ window.__ModuleLoader__.load({
|
|
|
1140
1671
|
return canvasBlob(c)
|
|
1141
1672
|
}
|
|
1142
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
|
+
|
|
1143
1700
|
function blobB64(blob) {
|
|
1144
1701
|
return new Promise((resolve, reject) => {
|
|
1145
1702
|
const r = new FileReader()
|
|
@@ -1476,15 +2033,32 @@ window.__ModuleLoader__.load({
|
|
|
1476
2033
|
function away(src) {
|
|
1477
2034
|
try { return !!src && LOCAL_HOST.test(new URL(src).hostname) && /^https?:$/.test(location.protocol) && !LOCAL_HOST.test(location.hostname) } catch { return false }
|
|
1478
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
|
+
}
|
|
1479
2051
|
|
|
1480
2052
|
function Screen(props) {
|
|
1481
2053
|
const src = props.src
|
|
1482
2054
|
const origin = src ? new URL(src).origin : ''
|
|
1483
2055
|
const [frames, setFrames] = useState([])
|
|
1484
2056
|
const [cur, setCur] = useState(0)
|
|
2057
|
+
const [failed, setFailed] = useState(false) // 远程看:打包没取回来
|
|
2058
|
+
const rem = remOf(props)
|
|
1485
2059
|
const wins = useRef(new Map())
|
|
1486
2060
|
const life = useRef(null)
|
|
1487
|
-
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 }
|
|
1488
2062
|
const curRef = useRef(0)
|
|
1489
2063
|
const P = useRef(props)
|
|
1490
2064
|
P.current = props
|
|
@@ -1503,6 +2077,16 @@ window.__ModuleLoader__.load({
|
|
|
1503
2077
|
postTo(curRef.current, { type: 'capture', id, ...opts })
|
|
1504
2078
|
setTimeout(() => { if (caps.current.has(id)) { caps.current.delete(id); reject(new Error('timeout')) } }, 30000)
|
|
1505
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' }) },
|
|
1506
2090
|
}
|
|
1507
2091
|
}
|
|
1508
2092
|
// 登记到右侧栏:导出图片时按屏找到它
|
|
@@ -1516,7 +2100,10 @@ window.__ModuleLoader__.load({
|
|
|
1516
2100
|
|
|
1517
2101
|
const postTo = (id, msg) => {
|
|
1518
2102
|
const f = wins.current.get(id)
|
|
1519
|
-
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)
|
|
1520
2107
|
}
|
|
1521
2108
|
|
|
1522
2109
|
// 在后台载一帧新的;正在载就记一笔,等它换上来再载
|
|
@@ -1525,13 +2112,44 @@ window.__ModuleLoader__.load({
|
|
|
1525
2112
|
if (L.loading) { L.again = true; return }
|
|
1526
2113
|
const id = L.next++
|
|
1527
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
|
+
}
|
|
1528
2141
|
let url = P.current.src
|
|
1529
2142
|
try { if (seed && seed.href && new URL(seed.href).origin === origin) url = seed.href } catch { /* 用原地址 */ }
|
|
1530
|
-
if (seed) L.seedOf.set(id, seed)
|
|
1531
2143
|
setFrames((fs) => [...fs, { id, src: url }])
|
|
1532
2144
|
}
|
|
1533
2145
|
|
|
1534
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
|
+
}
|
|
1535
2153
|
|
|
1536
2154
|
// 一帧好了:淡入换上来
|
|
1537
2155
|
const swap = (id, changes) => {
|
|
@@ -1548,6 +2166,7 @@ window.__ModuleLoader__.load({
|
|
|
1548
2166
|
if (p.picking) postTo(id, pickMsg())
|
|
1549
2167
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1550
2168
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2169
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1551
2170
|
if (first && p.onShown) p.onShown()
|
|
1552
2171
|
if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
|
|
1553
2172
|
L.file = ''
|
|
@@ -1565,16 +2184,21 @@ window.__ModuleLoader__.load({
|
|
|
1565
2184
|
L.again = false
|
|
1566
2185
|
L.seeds.clear()
|
|
1567
2186
|
L.seedOf.clear()
|
|
2187
|
+
L.gen++
|
|
2188
|
+
L.at = ''
|
|
2189
|
+
L.deps = null
|
|
2190
|
+
L.fails = 0
|
|
1568
2191
|
curRef.current = 0
|
|
1569
2192
|
setCur(0)
|
|
1570
2193
|
setFrames([])
|
|
2194
|
+
setFailed(false)
|
|
1571
2195
|
if (src) setTimeout(() => load(null), 0)
|
|
1572
|
-
}, [src])
|
|
2196
|
+
}, [src, !!rem])
|
|
1573
2197
|
|
|
1574
2198
|
// 页面发来的消息(几屏共用一个源,按 iframe 认是不是自己的)
|
|
1575
2199
|
useEffect(() => {
|
|
1576
2200
|
const on = (e) => {
|
|
1577
|
-
if (!origin || e.origin !== origin) return
|
|
2201
|
+
if (rem ? e.origin !== 'null' : !origin || e.origin !== origin) return
|
|
1578
2202
|
let id = 0
|
|
1579
2203
|
for (const [k, f] of wins.current) if (f && f.contentWindow === e.source) { id = k; break }
|
|
1580
2204
|
const d = e.data
|
|
@@ -1603,6 +2227,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
2227
|
if (p.picking) postTo(id, pickMsg())
|
|
1604
2228
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1605
2229
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2230
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1606
2231
|
if (d.changes && d.changes.count && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1607
2232
|
} else if (id > curRef.current) swap(id, d.changes)
|
|
1608
2233
|
break
|
|
@@ -1622,14 +2247,20 @@ window.__ModuleLoader__.load({
|
|
|
1622
2247
|
if (isCur && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1623
2248
|
break
|
|
1624
2249
|
case 'cancelled':
|
|
1625
|
-
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)
|
|
1626
2255
|
break
|
|
1627
2256
|
// 点选、改字只在用户开着对应模式时收(页面自己的脚本也能发这种消息,不能随时往输入框里塞字、往文件里写)
|
|
1628
2257
|
case 'picked':
|
|
1629
2258
|
if (isCur && p.picking && p.onPicked) p.onPicked(d.payload)
|
|
1630
2259
|
break
|
|
1631
2260
|
case 'nav':
|
|
1632
|
-
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 自己跳过去一样)
|
|
1633
2264
|
break
|
|
1634
2265
|
case 'edited':
|
|
1635
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)
|
|
@@ -1637,6 +2268,7 @@ window.__ModuleLoader__.load({
|
|
|
1637
2268
|
case 'editDone':
|
|
1638
2269
|
if (isCur && p.onEditDone) p.onEditDone()
|
|
1639
2270
|
break
|
|
2271
|
+
case 'checked':
|
|
1640
2272
|
case 'captured': {
|
|
1641
2273
|
const c = caps.current.get(d.id)
|
|
1642
2274
|
if (c) {
|
|
@@ -1651,7 +2283,7 @@ window.__ModuleLoader__.load({
|
|
|
1651
2283
|
}
|
|
1652
2284
|
window.addEventListener('message', on)
|
|
1653
2285
|
return () => window.removeEventListener('message', on)
|
|
1654
|
-
}, [origin, src])
|
|
2286
|
+
}, [origin, src, !!rem])
|
|
1655
2287
|
|
|
1656
2288
|
// 模型在不在干活、画布缩放比例、选取开关:告诉页面
|
|
1657
2289
|
useEffect(() => { if (cur) postTo(cur, { type: 'busy', on: !!props.busy }) }, [!!props.busy, cur])
|
|
@@ -1659,6 +2291,10 @@ window.__ModuleLoader__.load({
|
|
|
1659
2291
|
useEffect(() => { if (cur) postTo(cur, { type: 'zoom', z: zq / 100 }) }, [zq])
|
|
1660
2292
|
useEffect(() => { if (cur) postTo(cur, props.picking ? pickMsg() : { type: 'cancel' }) }, [!!props.picking])
|
|
1661
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])
|
|
1662
2298
|
useEffect(() => { if (cur) postTo(cur, { type: 'demo', on: !!props.demo }) }, [!!props.demo])
|
|
1663
2299
|
useEffect(() => {
|
|
1664
2300
|
// 刚退出改字(点按钮、Esc、模型开始干活):页面会把正在改的那一处交上来,这一小会儿还收
|
|
@@ -1668,17 +2304,46 @@ window.__ModuleLoader__.load({
|
|
|
1668
2304
|
}, [!!props.textEdit])
|
|
1669
2305
|
|
|
1670
2306
|
// 刷新:先问当前这帧要滚动位置和指纹,问不到就直接载
|
|
1671
|
-
const
|
|
1672
|
-
useEffect(() => {
|
|
1673
|
-
if (seq.current === props.reloadSeq) return
|
|
1674
|
-
seq.current = props.reloadSeq
|
|
2307
|
+
const refresh = () => {
|
|
1675
2308
|
const L = life.current
|
|
1676
2309
|
if (!curRef.current) { load(null); return }
|
|
1677
2310
|
clearTimeout(L.snapTimer)
|
|
1678
2311
|
L.snapTimer = setTimeout(() => load(null), 400)
|
|
1679
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()
|
|
1680
2319
|
}, [props.reloadSeq])
|
|
1681
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
|
+
|
|
1682
2347
|
// 页面没装上脚本(极少数,比如不是 HTML):载完等一会儿也换上来
|
|
1683
2348
|
const onFrameLoad = (id) => {
|
|
1684
2349
|
setTimeout(() => {
|
|
@@ -1687,15 +2352,17 @@ window.__ModuleLoader__.load({
|
|
|
1687
2352
|
}, 1600)
|
|
1688
2353
|
}
|
|
1689
2354
|
|
|
1690
|
-
// 预览服务器只听电脑本机(127.0.0.1)。从手机(dsh-mobile)或别的电脑打开 DSH
|
|
1691
|
-
//
|
|
1692
|
-
|
|
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')
|
|
1693
2360
|
return h('div', { className: 'ddp-scr' + (props.dark ? ' dk' : '') },
|
|
1694
2361
|
...frames.map((f) => h('iframe', {
|
|
1695
2362
|
key: f.id,
|
|
1696
2363
|
ref: (el) => { if (el) wins.current.set(f.id, el); else wins.current.delete(f.id) },
|
|
1697
2364
|
className: 'ddp-frame' + (f.id === cur ? ' cur' : f.id < cur ? ' old' : ''),
|
|
1698
|
-
src: f.src,
|
|
2365
|
+
...(typeof f.doc === 'string' ? { srcDoc: f.doc, sandbox: 'allow-scripts allow-forms', referrerPolicy: 'no-referrer' } : { src: f.src }),
|
|
1699
2366
|
title: props.label || '',
|
|
1700
2367
|
tabIndex: f.id === cur ? 0 : -1,
|
|
1701
2368
|
'aria-hidden': f.id === cur ? undefined : true,
|
|
@@ -1729,7 +2396,7 @@ window.__ModuleLoader__.load({
|
|
|
1729
2396
|
}
|
|
1730
2397
|
|
|
1731
2398
|
/** 设计规范面板:按组列出变量,点一个放进输入框 */
|
|
1732
|
-
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onClose }) {
|
|
2399
|
+
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onClose }) {
|
|
1733
2400
|
const groups = {}
|
|
1734
2401
|
for (const tok of tokens) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
1735
2402
|
const px = (v) => Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
|
|
@@ -1755,7 +2422,112 @@ window.__ModuleLoader__.load({
|
|
|
1755
2422
|
h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, preview(g, tok.value)),
|
|
1756
2423
|
h('span', { className: 'tx' }, h('span', { className: 'nm' }, tok.name), h('span', { className: 'vl' }, tok.value))))))
|
|
1757
2424
|
: h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onAsk }, t('tokensAsk'))))),
|
|
1758
|
-
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 })))
|
|
1759
2531
|
}
|
|
1760
2532
|
|
|
1761
2533
|
/** 菜单开着时:点外面、按 Esc 关掉 */
|
|
@@ -1782,6 +2554,7 @@ window.__ModuleLoader__.load({
|
|
|
1782
2554
|
items.push(['raw', I.image, t('exRaw')])
|
|
1783
2555
|
if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong')]) // 海报、幻灯片画幅是定的,没有「整页」
|
|
1784
2556
|
if (count > 1) items.push(['show', I.canvas, t('exShow')])
|
|
2557
|
+
items.push(['pdf', I.pdf, t('exPdf')], ['pptx', I.deck, t('exPptx')])
|
|
1785
2558
|
return h('div', { className: 'ddp-kind', ref },
|
|
1786
2559
|
h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('exportT'), disabled, onClick: () => setOpen(!open) }, h(I.download)),
|
|
1787
2560
|
open ? h('div', { className: 'ddp-menu', role: 'menu' },
|
|
@@ -1912,6 +2685,29 @@ window.__ModuleLoader__.load({
|
|
|
1912
2685
|
}
|
|
1913
2686
|
}, [layoutKey])
|
|
1914
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
|
+
|
|
1915
2711
|
const zoomAt = (factor, px, py, smooth) => {
|
|
1916
2712
|
const c = camRef.current
|
|
1917
2713
|
if (!c) return
|
|
@@ -2049,6 +2845,8 @@ window.__ModuleLoader__.load({
|
|
|
2049
2845
|
useEffect(() => { const tm = setTimeout(() => setWaited(true), 2500); return () => clearTimeout(tm) }, [])
|
|
2050
2846
|
const [picking, setPicking] = useState(false)
|
|
2051
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 }
|
|
2052
2850
|
const [editing, setEditing] = useState(false)
|
|
2053
2851
|
const [menu, setMenu] = useState(false)
|
|
2054
2852
|
const [toast, setToast] = useState('')
|
|
@@ -2057,6 +2855,7 @@ window.__ModuleLoader__.load({
|
|
|
2057
2855
|
const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
|
|
2058
2856
|
const [sel, setSel] = useState('')
|
|
2059
2857
|
const [panel, setPanel] = useState(false)
|
|
2858
|
+
const [styles, setStyles] = useState(false) // 换风格面板
|
|
2060
2859
|
const [dropping, setDropping] = useState(false) // 正拖着图片进来
|
|
2061
2860
|
const refInput = useRef(null)
|
|
2062
2861
|
const [exMenu, setExMenu] = useState(false)
|
|
@@ -2070,6 +2869,9 @@ window.__ModuleLoader__.load({
|
|
|
2070
2869
|
const [dark, setDark] = useState(false)
|
|
2071
2870
|
const [phone, setPhone] = useState(false) // 手机上看的面板
|
|
2072
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 }(体检里点了一条)
|
|
2073
2875
|
const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
|
|
2074
2876
|
const apis = useRef(new Map()) // 屏 → 截图用的接口
|
|
2075
2877
|
const cmpRef = useRef(null)
|
|
@@ -2130,6 +2932,8 @@ window.__ModuleLoader__.load({
|
|
|
2130
2932
|
const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
|
|
2131
2933
|
const anyDark = Object.keys(info).some((k) => k.startsWith(site + '|') && info[k].dark)
|
|
2132
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
|
|
2133
2937
|
|
|
2134
2938
|
// 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
|
|
2135
2939
|
const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
|
|
@@ -2218,7 +3022,7 @@ window.__ModuleLoader__.load({
|
|
|
2218
3022
|
return () => ro.disconnect()
|
|
2219
3023
|
}, [!!url])
|
|
2220
3024
|
|
|
2221
|
-
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])
|
|
2222
3026
|
|
|
2223
3027
|
// 版本列表:面板开着时拉一次,存了新版本(vrev 变了)再拉
|
|
2224
3028
|
const vrev = st ? st.vrev : 0
|
|
@@ -2256,6 +3060,7 @@ window.__ModuleLoader__.load({
|
|
|
2256
3060
|
const togglePick = () => {
|
|
2257
3061
|
if (picking) { setPicking(false); return }
|
|
2258
3062
|
setTextEdit(false)
|
|
3063
|
+
setStyleEdit(false)
|
|
2259
3064
|
setPicking(true)
|
|
2260
3065
|
if (!canvas) {
|
|
2261
3066
|
// 单页:把焦点给页面,Esc 在页面里也管用
|
|
@@ -2267,20 +3072,76 @@ window.__ModuleLoader__.load({
|
|
|
2267
3072
|
const toggleText = () => {
|
|
2268
3073
|
if (textEdit) { setTextEdit(false); return }
|
|
2269
3074
|
setPicking(false)
|
|
3075
|
+
setStyleEdit(false)
|
|
2270
3076
|
setTextEdit(true)
|
|
2271
3077
|
}
|
|
2272
3078
|
|
|
2273
|
-
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')
|
|
2274
3080
|
|
|
2275
3081
|
const onEdited = (sc, d) => {
|
|
2276
3082
|
if (!fileTarget) return askText(sc, d)
|
|
2277
|
-
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 })
|
|
2278
3084
|
.then((r) => (r && r.ok ? say(t('textSaved', { file: r.file })) : askText(sc, d)), () => askText(sc, d))
|
|
2279
3085
|
}
|
|
2280
3086
|
|
|
2281
3087
|
// 模型在干活时不能改字(它可能正整份重写文件,改了会被盖掉)
|
|
2282
3088
|
useEffect(() => { if (ver || demo || busy) setTextEdit(false) }, [!!ver, !!demo, busy])
|
|
2283
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
|
+
|
|
2284
3145
|
useEffect(() => {
|
|
2285
3146
|
if (!dropping) return undefined
|
|
2286
3147
|
const off = () => setDropping(false)
|
|
@@ -2367,13 +3228,18 @@ window.__ModuleLoader__.load({
|
|
|
2367
3228
|
}
|
|
2368
3229
|
|
|
2369
3230
|
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc) },
|
|
2370
|
-
h(Screen, { t,
|
|
3231
|
+
h(Screen, { t, remote,
|
|
2371
3232
|
src: sc.url,
|
|
2372
3233
|
label: labelOf(sc),
|
|
2373
3234
|
busy: busy && !sc.v,
|
|
2374
3235
|
born: !sc.v && isBorn(sc.key),
|
|
2375
3236
|
picking: picking && !sc.v,
|
|
2376
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),
|
|
2377
3243
|
dark,
|
|
2378
3244
|
apis: apis.current,
|
|
2379
3245
|
apiKey: ik(sc),
|
|
@@ -2391,16 +3257,23 @@ window.__ModuleLoader__.load({
|
|
|
2391
3257
|
}))
|
|
2392
3258
|
|
|
2393
3259
|
// ── 导出图片 ──
|
|
2394
|
-
|
|
3260
|
+
// 截一屏:图片和它的 CSS 尺寸(PDF / PPT 按这个定页面大小)
|
|
3261
|
+
// 那一屏的接口(截图、体检);刚载入的等它就绪
|
|
3262
|
+
const apiOf = async (sc) => {
|
|
2395
3263
|
let api = apis.current.get(ik(sc))
|
|
2396
3264
|
for (let i = 0; i < 40 && !(api && api.ready()); i++) {
|
|
2397
3265
|
await new Promise((r) => setTimeout(r, 150))
|
|
2398
3266
|
api = apis.current.get(ik(sc))
|
|
2399
3267
|
}
|
|
2400
3268
|
if (!api) throw new Error('not loaded')
|
|
3269
|
+
return api
|
|
3270
|
+
}
|
|
3271
|
+
const captureOf = async (sc, full) => {
|
|
3272
|
+
const api = await apiOf(sc)
|
|
2401
3273
|
const r = await api.capture({ full: !!full, scale: 2 })
|
|
2402
|
-
return loadImg(r.png)
|
|
3274
|
+
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [] }
|
|
2403
3275
|
}
|
|
3276
|
+
const shotOf = async (sc, full) => (await captureOf(sc, full)).img
|
|
2404
3277
|
|
|
2405
3278
|
const saveImage = async (name, blob) => {
|
|
2406
3279
|
const png = await blobB64(blob)
|
|
@@ -2416,6 +3289,31 @@ window.__ModuleLoader__.load({
|
|
|
2416
3289
|
try {
|
|
2417
3290
|
const prefix = ver ? `v${ver.n}-` : ''
|
|
2418
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
|
+
}
|
|
2419
3317
|
if (mode === 'show') {
|
|
2420
3318
|
const shots = []
|
|
2421
3319
|
for (const sc of shown) {
|
|
@@ -2471,7 +3369,17 @@ window.__ModuleLoader__.load({
|
|
|
2471
3369
|
insert(t('variantText', { name: labelOf(sc), b, where: whereOf(sc.url, stRef.current) }), 'insertedSend')
|
|
2472
3370
|
}
|
|
2473
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
|
+
|
|
2474
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,
|
|
2475
3383
|
h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download)),
|
|
2476
3384
|
sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle)),
|
|
2477
3385
|
h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single)),
|
|
@@ -2507,9 +3415,95 @@ window.__ModuleLoader__.load({
|
|
|
2507
3415
|
const hintTop = (picking || textEdit) && !canvas && !inMulti && !ver && !demo && kind !== 'web'
|
|
2508
3416
|
const flows = canvas && !inMulti && !picking && !textEdit ? flowsOf(shown, (sc) => (infoOf(ik(sc)) || {}).links, fileTarget) : []
|
|
2509
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
|
+
|
|
2510
3504
|
const toggleDemo = () => (demo ? setDemo(null) : startDemo())
|
|
2511
|
-
const toggleHist = () => { setHist(!hist); setPanel(false); setPhone(false) }
|
|
2512
|
-
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) }
|
|
2513
3507
|
const reload = () => setReloadSeq((x) => x + 1)
|
|
2514
3508
|
|
|
2515
3509
|
const bar = h('div', { className: 'ddp-bar' },
|
|
@@ -2521,6 +3515,7 @@ window.__ModuleLoader__.load({
|
|
|
2521
3515
|
h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo, onClick: togglePick },
|
|
2522
3516
|
h(I.pick), h('span', null, picking ? t('picking') : t('pick'))),
|
|
2523
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)),
|
|
2524
3519
|
h('span', { className: 'ddp-sep' }),
|
|
2525
3520
|
h('div', { className: 'ddp-seg' },
|
|
2526
3521
|
h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
|
|
@@ -2528,15 +3523,18 @@ window.__ModuleLoader__.load({
|
|
|
2528
3523
|
h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
|
|
2529
3524
|
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver, onClick: toggleDemo }, h(I.play)),
|
|
2530
3525
|
h(KindMenu, { t, kind, open: menu, setOpen: setMenu, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
|
|
2531
|
-
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),
|
|
2532
3529
|
h('button', { className: 'ddp-btn o2' + (hist || ver ? ' sel' : ''), title: t('history'), disabled: !fileTarget, onClick: toggleHist }, h(I.history)),
|
|
2533
3530
|
h(ExportMenu, { t, kind, count: shown.length, open: exMenu, setOpen: setExMenu, onPick: (m) => doExport(m), disabled: !url }),
|
|
2534
3531
|
anyDark || dark ? h('button', { className: 'ddp-btn o1' + (dark ? ' sel' : ''), title: t('dark'), onClick: () => setDark(!dark) }, h(I.moon)) : null,
|
|
2535
3532
|
h('button', { className: 'ddp-btn o1' + (phone ? ' sel' : '') + (phoneOn && !phone ? ' has' : ''), title: t('phone'), disabled: !url, onClick: togglePhone }, h(I.scan)),
|
|
2536
3533
|
h('button', { className: 'ddp-btn o1', title: t('refresh'), onClick: reload }, h(I.refresh)),
|
|
2537
3534
|
h('a', { className: 'ddp-btn o1', title: t('external'), href: url, target: '_blank', rel: 'noopener noreferrer' }, h(I.external)),
|
|
2538
|
-
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: [
|
|
2539
3536
|
['o2', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo],
|
|
3537
|
+
['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
|
|
2540
3538
|
['o2', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
|
|
2541
3539
|
anyDark || dark ? ['o1', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
|
|
2542
3540
|
['o1', I.scan, t('phone'), phone, !url, togglePhone],
|
|
@@ -2544,8 +3542,11 @@ window.__ModuleLoader__.load({
|
|
|
2544
3542
|
['o1', I.external, t('external'), false, !url, url],
|
|
2545
3543
|
] }))
|
|
2546
3544
|
|
|
2547
|
-
|
|
2548
|
-
|
|
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')))
|
|
2549
3550
|
|
|
2550
3551
|
// ── 参考图:拖进来 / 粘贴 / 从设计规范面板选;存进设计目录的 refs/,尺寸和主要颜色放进输入框 ──
|
|
2551
3552
|
const addRef = async (file) => {
|
|
@@ -2614,6 +3615,7 @@ window.__ModuleLoader__.load({
|
|
|
2614
3615
|
onClick: () => post('open', { session: sid, target: st.hint }).then((s) => { if (poll) poll.set(s) }, (e) => say(t('openFailed', { msg: e.message }))),
|
|
2615
3616
|
}, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
|
|
2616
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)) })),
|
|
2617
3619
|
h('p', null, t('emptyHint')),
|
|
2618
3620
|
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
2619
3621
|
h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose')))),
|
|
@@ -2634,7 +3636,7 @@ window.__ModuleLoader__.load({
|
|
|
2634
3636
|
h('div', { className: 'ddp-single' },
|
|
2635
3637
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2636
3638
|
h(Device, { kind, w: size[0], h: size[1], fill, label },
|
|
2637
|
-
h(Screen, { t,
|
|
3639
|
+
h(Screen, { t, remote,
|
|
2638
3640
|
src: urlOfKey(l.key),
|
|
2639
3641
|
label,
|
|
2640
3642
|
busy: false,
|
|
@@ -2668,7 +3670,7 @@ window.__ModuleLoader__.load({
|
|
|
2668
3670
|
sel: '',
|
|
2669
3671
|
labelOf: (sc) => sc.label,
|
|
2670
3672
|
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base) },
|
|
2671
|
-
h(Screen, { t,
|
|
3673
|
+
h(Screen, { t, remote,
|
|
2672
3674
|
src: base.url,
|
|
2673
3675
|
label: labelOf(base),
|
|
2674
3676
|
busy,
|
|
@@ -2697,7 +3699,7 @@ window.__ModuleLoader__.load({
|
|
|
2697
3699
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
2698
3700
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
2699
3701
|
const tagAt = (side) => ({ top: kind === 'app' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
2700
|
-
const screenOf = (sc) => h(Screen, { t, 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,
|
|
2701
3703
|
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk) })
|
|
2702
3704
|
body = h('div', { className: 'ddp-single' },
|
|
2703
3705
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
@@ -2719,9 +3721,11 @@ window.__ModuleLoader__.load({
|
|
|
2719
3721
|
kind,
|
|
2720
3722
|
size,
|
|
2721
3723
|
ghost,
|
|
2722
|
-
picking: picking || textEdit,
|
|
3724
|
+
picking: picking || textEdit || styleEdit,
|
|
2723
3725
|
sel: sel || focus,
|
|
2724
3726
|
labelOf,
|
|
3727
|
+
reveal,
|
|
3728
|
+
inset: ck || styleEdit || panel || styles ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
2725
3729
|
render: (sc, z) => renderScreen(sc, z, false),
|
|
2726
3730
|
flows,
|
|
2727
3731
|
onSelect: setSel,
|
|
@@ -2730,16 +3734,18 @@ window.__ModuleLoader__.load({
|
|
|
2730
3734
|
} else {
|
|
2731
3735
|
const fill = kind === 'web'
|
|
2732
3736
|
const [dw, dh] = deviceBox(kind, size)
|
|
2733
|
-
|
|
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))
|
|
2734
3740
|
const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
|
|
2735
|
-
body = h('div', { className: 'ddp-single' },
|
|
3741
|
+
body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
|
|
2736
3742
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2737
3743
|
renderScreen(one, z, fill)))
|
|
2738
3744
|
}
|
|
2739
3745
|
|
|
2740
3746
|
let chip = null
|
|
2741
3747
|
const side = canvas || inMulti ? ' left' : ''
|
|
2742
|
-
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') : ''
|
|
2743
3749
|
if (flash) chip = h('div', { className: 'ddp-chip ok' + side, key: 'ok' }, h(I.check), h('span', null, flash))
|
|
2744
3750
|
else if (busy) chip = busyChip(side)
|
|
2745
3751
|
|
|
@@ -2749,8 +3755,23 @@ window.__ModuleLoader__.load({
|
|
|
2749
3755
|
h('div', { className: 'ddp-body' + (ver || demo || hintTop ? ' vb' : '') }, body),
|
|
2750
3756
|
busy ? h('div', { className: 'ddp-glow' }) : null,
|
|
2751
3757
|
busy ? h('div', { className: 'ddp-ring' }) : null,
|
|
2752
|
-
|
|
2753
|
-
|
|
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,
|
|
2754
3775
|
phone ? h(PhonePanel, {
|
|
2755
3776
|
t,
|
|
2756
3777
|
sid,
|
|
@@ -2849,7 +3870,7 @@ window.__ModuleLoader__.load({
|
|
|
2849
3870
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
2850
3871
|
exports.inject = INJECT
|
|
2851
3872
|
// 测试用
|
|
2852
|
-
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 }
|
|
2853
3874
|
return module.exports
|
|
2854
3875
|
},
|
|
2855
3876
|
})
|