dsh-design-studio 0.4.1 → 0.6.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 +19 -7
- package/README.md +46 -11
- package/cordis.patch.yml +1 -1
- package/lib/browser.js +273 -0
- package/lib/bundle.js +228 -0
- package/lib/client.js +1442 -47
- package/lib/core.js +263 -25
- package/lib/doc.js +356 -0
- package/lib/handoff.js +555 -0
- package/lib/history.js +1 -1
- package/lib/index.js +683 -24
- package/lib/look.js +550 -0
- package/lib/picker.js +1153 -24
- package/lib/serve.js +25 -8
- package/lib/system.js +1360 -0
- package/lib/tool.js +9 -3
- package/lib/tweaks.js +362 -0
- 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 +67 -3
- 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}',
|
|
@@ -184,6 +303,49 @@ window.__ModuleLoader__.load({
|
|
|
184
303
|
failed: '没放进去:{msg}',
|
|
185
304
|
openFailed: '打不开:{msg}',
|
|
186
305
|
headerTip: '设计工作台',
|
|
306
|
+
tweaks: '调节滑块',
|
|
307
|
+
twTip: '模型给这套设计做的几个维度:拖的时候所有屏一起变,还没写进文件。',
|
|
308
|
+
twEmpty: '这套设计还没有专属滑块。让模型挑几个最值得试的维度(留白、圆角、主色、排法……),做成滑块放在这里。',
|
|
309
|
+
twMake: '让模型做几个',
|
|
310
|
+
twFix: '让模型修好',
|
|
311
|
+
twMakeText: '〔调节滑块〕给这套设计做 3–5 个调节滑块:挑最值得试的维度(比如留白、圆角、主色、卡片排法),照 ui-design 技能「调节滑块」一节写进设计目录的 tweaks.json,把样式接上,做完用 check_design 查一遍。',
|
|
312
|
+
twFixText: '〔调节滑块〕tweaks.json 有问题:{errs}。照 ui-design 技能「调节滑块」一节改好。',
|
|
313
|
+
twAskHead: '〔调节〕我在右侧调节滑块里调成了下面这样,照这个改设计(改样式源文件,tweaks.json 里对应的 value 也改成新值):',
|
|
314
|
+
twAskTip: '让模型照这几个值改设计(可以顺手调整相关的地方)',
|
|
315
|
+
twSaveTip: 'CSS 变量写进 tokens.css 的默认值,属性写到每屏的 <html> 上(改前改后各存一版)',
|
|
316
|
+
twNoWrite: '开发服务器的页面不能直接写回,交给模型改',
|
|
317
|
+
twMoved: '设计已经换了,没写回;再拖一次',
|
|
318
|
+
twSaved: '已写回 {n} 项',
|
|
319
|
+
twMissed: '{names} 在样式里没找到默认值,没写回:交给模型改',
|
|
320
|
+
systems: '品牌设计系统',
|
|
321
|
+
sysOpen: '品牌设计系统…',
|
|
322
|
+
sysEmptyLine: '有自己的品牌?',
|
|
323
|
+
sysTip: '从你的网站或项目里提取的颜色、字体、圆角和组件。设为默认后,做新设计都照它来。',
|
|
324
|
+
sysEmpty: '还没有。在下面填你的网站地址或项目文件夹,让模型提一套。',
|
|
325
|
+
sysDefault: '默认',
|
|
326
|
+
sysSet: '设为默认',
|
|
327
|
+
sysUnset: '不再默认',
|
|
328
|
+
sysApply: '套用到当前设计',
|
|
329
|
+
sysPh: '网站地址或项目文件夹',
|
|
330
|
+
sysExtract: '提取',
|
|
331
|
+
sysExtractText: '〔提取设计系统〕从 {src} 提取我的品牌设计系统(调 extract_design_system),设为默认,以后做新设计都照它来。提完对照截图核对一遍,再打开样张给我看。',
|
|
332
|
+
sysApplyText: '〔套用设计系统〕把右侧这套设计换成设计系统「{name}」({dir}):复制它的 tokens.css、components.css 进设计目录,每屏都 link 上,按钮、输入框、卡片换成里面的类,颜色、字体、圆角都照它;版式和内容不变。',
|
|
333
|
+
sysSetOk: '之后做新设计都照「{name}」',
|
|
334
|
+
sysUnsetOk: '已取消默认',
|
|
335
|
+
handoff: '转成代码',
|
|
336
|
+
hoTip: '把设计做成真项目的代码:先整理交接说明和截图,再一屏一屏写,每屏写完和设计稿逐像素比,对上了再做下一屏。',
|
|
337
|
+
hoFw: '用什么做',
|
|
338
|
+
hoSame: '跟现有项目一样({label})',
|
|
339
|
+
hoHtml: '纯 HTML / CSS',
|
|
340
|
+
hoDest: '代码放在',
|
|
341
|
+
hoScope: '范围',
|
|
342
|
+
hoAll: '全部 {n} 屏',
|
|
343
|
+
hoOne: '只转这一屏',
|
|
344
|
+
hoScopeOne: '只做 {name}',
|
|
345
|
+
hoGo: '开始转成代码',
|
|
346
|
+
hoProxy: '现在预览的是开发服务器上的页面,它已经是代码了。要转的是用本地 HTML 做的设计。',
|
|
347
|
+
hoLoading: '正在看会话目录…',
|
|
348
|
+
hoText: '〔转成代码〕把右侧的设计做成真项目的代码。\n- 用:{fw}\n- 放在:{dest}\n- 范围:{scope}\n先调 handoff_design(framework: {key},dest: {dest}{only})拿交接说明,照 HANDOFF.md 一屏一屏做;每屏做完把开发服务器跑起来,用 compare_design 和设计稿比,差异降到 3% 以下再做下一屏。设计目录里的文件别改。',
|
|
187
349
|
}
|
|
188
350
|
const EN = {
|
|
189
351
|
title: 'Design Studio',
|
|
@@ -245,12 +407,75 @@ window.__ModuleLoader__.load({
|
|
|
245
407
|
gRadius: 'Radii',
|
|
246
408
|
gShadow: 'Shadows',
|
|
247
409
|
gOther: 'Other',
|
|
248
|
-
exportT: 'Export
|
|
410
|
+
exportT: 'Export',
|
|
249
411
|
exFrame: 'This screen · in frame',
|
|
250
412
|
exRaw: 'This screen · plain',
|
|
251
413
|
exLong: 'This screen · full page',
|
|
252
414
|
exShow: 'Showcase of all screens',
|
|
253
|
-
|
|
415
|
+
exPdf: 'PDF · one page per screen',
|
|
416
|
+
exPptx: 'PowerPoint · one slide per screen (as images)',
|
|
417
|
+
exWhere: 'Saved to exports/ in the design folder; images are also copied to the clipboard',
|
|
418
|
+
exportedDoc: 'Exported {name} ({n} pages)',
|
|
419
|
+
check: 'Design check',
|
|
420
|
+
checkTip: 'Design check: measure overflowing or overlapping text, contrast, tap targets and type sizes',
|
|
421
|
+
checking: 'Checking…',
|
|
422
|
+
checkNone: 'No problems found',
|
|
423
|
+
checkSum: '{s} screens · {n} problems',
|
|
424
|
+
checkFix: 'Ask the model to fix',
|
|
425
|
+
checkAgain: 'Check again',
|
|
426
|
+
checkFail: 'Check failed: {msg}',
|
|
427
|
+
checkNote: 'Only measurable layout problems; whether it looks good is still up to you',
|
|
428
|
+
checkCount: 'Design check: {n} problems last time. Click to see them',
|
|
429
|
+
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.',
|
|
430
|
+
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:',
|
|
431
|
+
ckToolOk: 'Measured {s} screens ({labels}). No problems found.',
|
|
432
|
+
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.',
|
|
433
|
+
ckToolErr: '{label}: could not measure ({msg})',
|
|
434
|
+
ck_hscroll: 'Wider than the screen',
|
|
435
|
+
ck_clipped: 'Text cut off',
|
|
436
|
+
ck_overflow: 'Text spills out of its box',
|
|
437
|
+
ck_overlap: 'Overlapping text',
|
|
438
|
+
ck_contrast: 'Low contrast',
|
|
439
|
+
ck_tap: 'Tap target too small',
|
|
440
|
+
ck_tiny: 'Text too small',
|
|
441
|
+
ck_img: 'Image failed to load',
|
|
442
|
+
ck_fonts: 'Too many type sizes',
|
|
443
|
+
ck_distort: 'Stretched image',
|
|
444
|
+
ck_long: 'Lines too long',
|
|
445
|
+
ck_wrap: 'Short text wraps',
|
|
446
|
+
ck_covered: 'Hidden under a fixed bar',
|
|
447
|
+
ck_tight: 'Too close to a fixed bar',
|
|
448
|
+
ck_jserr: 'Script error',
|
|
449
|
+
ck_missing: 'File failed to load',
|
|
450
|
+
cd_hscroll: 'sticks out {px}px',
|
|
451
|
+
cd_clipped: '{px}px cut off',
|
|
452
|
+
cd_overflow: 'sticks out {px}px',
|
|
453
|
+
cd_overlap: 'overlaps {other} "{otherText}"',
|
|
454
|
+
cd_contrast: '{fg} on {bg}, {ratio}:1 (needs ≥ {need}:1)',
|
|
455
|
+
cd_tap: '{w}×{h} (at least {min}×{min})',
|
|
456
|
+
cd_tiny: '{size}px (at least {min}px)',
|
|
457
|
+
cd_img: '{src}',
|
|
458
|
+
cd_fonts: '{n} type sizes on one screen: {sizes}',
|
|
459
|
+
cd_distort: '{nw}×{nh} shown at {w}×{h} (use object-fit: cover or fix the size)',
|
|
460
|
+
cd_long: 'about {chars} characters per line (keep it ≤ {max})',
|
|
461
|
+
cd_wrap: 'breaks onto {lines} lines (keep short labels and prices on one line: more width, white-space: nowrap, or shorter text)',
|
|
462
|
+
cd_covered: '{when}{px}px hidden under the {edgeName} {bar} (leave room for the bar)',
|
|
463
|
+
cd_tight: 'only {px}px from the {edgeName} {bar} (leave 16px or more)',
|
|
464
|
+
cd_jserr: '{msg}{at}',
|
|
465
|
+
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
466
|
+
ckEdge_top: 'top bar',
|
|
467
|
+
ckEdge_bottom: 'bottom bar',
|
|
468
|
+
ckWhen_top: 'before scrolling, ',
|
|
469
|
+
ckWhen_bottom: 'even scrolled to the end, ',
|
|
470
|
+
ckWhat_css: 'stylesheet',
|
|
471
|
+
ckWhat_script: 'script',
|
|
472
|
+
ckWhat_font: 'font',
|
|
473
|
+
ckWhat_other: 'file',
|
|
474
|
+
ckSize: '{w}×{h}, page height {page}',
|
|
475
|
+
ckSizeBar: ', {edgeName} {bar} {gap}px from the content',
|
|
476
|
+
ckScreen: '{label} ({where})',
|
|
477
|
+
ckItem: '{i}. {type}: {where}{text} — {detail}',
|
|
478
|
+
ckQuote: ' "{text}"',
|
|
254
479
|
exporting: 'Exporting…',
|
|
255
480
|
exported: 'Exported {name}',
|
|
256
481
|
exportedCopied: 'Exported {name} and copied it',
|
|
@@ -278,6 +503,9 @@ window.__ModuleLoader__.load({
|
|
|
278
503
|
cmpNow: 'Now',
|
|
279
504
|
dark: 'Dark mode',
|
|
280
505
|
variants: 'Two more takes',
|
|
506
|
+
pickThis: 'Use this one: move the other takes to drafts/ and build the rest',
|
|
507
|
+
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',
|
|
508
|
+
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
509
|
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
510
|
exThis: 'Export this screen',
|
|
283
511
|
demo: 'Play: tap buttons in the page to move between screens',
|
|
@@ -288,9 +516,61 @@ window.__ModuleLoader__.load({
|
|
|
288
516
|
textEdit: 'Edit text: click any text in the page, Enter to save (Esc to stop)',
|
|
289
517
|
textSaved: 'Saved to {file}',
|
|
290
518
|
textAsk: 'Change the text "{before}" to "{after}" on the page (only this text, nothing else).\n{where}\n',
|
|
519
|
+
styleEdit: 'Style: click an element, then drag sliders for size, colors, radius and spacing (Esc to stop)',
|
|
520
|
+
styleAny: 'Click an element in any screen to style it · Esc to stop',
|
|
521
|
+
styleOne: 'Click an element in the page to style it · Esc to stop',
|
|
522
|
+
style: 'Style',
|
|
523
|
+
styleNone: 'Click an element in the page. Changes show in the preview first; save them to the file when you like them.',
|
|
524
|
+
styleFont: 'Font size',
|
|
525
|
+
styleWeight: 'Weight',
|
|
526
|
+
styleColor: 'Text',
|
|
527
|
+
styleBg: 'Background',
|
|
528
|
+
styleClear: 'transparent',
|
|
529
|
+
styleRadius: 'Radius',
|
|
530
|
+
stylePadY: 'Padding (top / bottom)',
|
|
531
|
+
stylePadX: 'Padding (left / right)',
|
|
532
|
+
styleGap: 'Gap',
|
|
533
|
+
styleW400: 'Regular',
|
|
534
|
+
styleW500: 'Medium',
|
|
535
|
+
styleW600: 'Semi',
|
|
536
|
+
styleW700: 'Bold',
|
|
537
|
+
styleVar: 'Token {name}',
|
|
538
|
+
styleGradient: 'It is a gradient now; picking a color makes it solid',
|
|
539
|
+
styleReset: 'Reset',
|
|
540
|
+
styleAsk: 'Ask the model',
|
|
541
|
+
styleSave: 'Save to file',
|
|
542
|
+
styleSaveTip: 'Writes into this element\'s style attribute in the source (a version is kept before and after)',
|
|
543
|
+
styleAskTip: 'Let the model move it into the stylesheet (using tokens where it can)',
|
|
544
|
+
styleNoWrite: 'Dev-server pages are not written here; ask the model',
|
|
545
|
+
styleBusyTip: 'The model is working; save when it stops',
|
|
546
|
+
styleSaved: 'Style saved to {file}',
|
|
547
|
+
styleMiss: 'Could not find this element in the source (maybe a script made it); asking the model instead',
|
|
548
|
+
styleN: '{n} changed',
|
|
549
|
+
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',
|
|
550
|
+
ruleFrom: '- {props}: currently set by {sel} ({at})',
|
|
551
|
+
ruleMaybe: '- {props}: probably set by {sel} ({at}; it sits in @layer / @container and was not fully resolved, check before editing)',
|
|
552
|
+
ruleInline: '- {props}: currently in the element\'s inline style{over}',
|
|
553
|
+
ruleOver: ', overriding {sel} ({at})',
|
|
554
|
+
ruleAt: '{file} line {line}',
|
|
555
|
+
styleArrow: '{prop} {from} → {to}',
|
|
556
|
+
styleJoin: '; ',
|
|
557
|
+
lang: 'en',
|
|
558
|
+
stylesHead: 'Start from a style',
|
|
559
|
+
stylesTip: 'Pick one to put its colors, fonts and shapes in the chat box, then say what to make',
|
|
560
|
+
stylesOpen: 'Try another style…',
|
|
561
|
+
stylesTitle: 'Change the style',
|
|
562
|
+
stylesSwap: 'Pick one and the model restyles the current design (tokens.css first; layout and copy stay)',
|
|
563
|
+
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
564
|
+
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}',
|
|
565
|
+
styleBgImage: '; on the background: {v}',
|
|
566
|
+
styleFonts: 'headings {head}; body {body}',
|
|
567
|
+
styleShape: 'radius {r}px; shadow {s}',
|
|
568
|
+
styleStart: 'Design in this style (put these into tokens.css first; no need to ask about the feel again):',
|
|
569
|
+
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
570
|
demoNone: 'This screen has no links to other screens yet. Ask the model to connect screens with <a href="detail.html">',
|
|
292
571
|
phone: 'View on phone',
|
|
293
|
-
away: 'The
|
|
572
|
+
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).',
|
|
573
|
+
remoteFail: 'This screen couldn\'t be sent over (connection dropped, or the images are too large). Click Reload to try again.',
|
|
294
574
|
phoneStarting: 'Starting…',
|
|
295
575
|
phoneHow: 'Scan with your phone camera',
|
|
296
576
|
phoneWifi: 'Phone and computer must be on the same Wi-Fi',
|
|
@@ -316,6 +596,7 @@ window.__ModuleLoader__.load({
|
|
|
316
596
|
demoShort: 'Play',
|
|
317
597
|
editAddr: 'Change address',
|
|
318
598
|
working: 'The model is working',
|
|
599
|
+
asking: 'The model is waiting for your answer (in the chat)',
|
|
319
600
|
writing: 'Writing {file}',
|
|
320
601
|
born: 'Generated {name}',
|
|
321
602
|
updated: 'Updated{file}',
|
|
@@ -330,6 +611,49 @@ window.__ModuleLoader__.load({
|
|
|
330
611
|
failed: 'Could not add it: {msg}',
|
|
331
612
|
openFailed: 'Could not open: {msg}',
|
|
332
613
|
headerTip: 'Design Studio',
|
|
614
|
+
tweaks: 'Tweaks',
|
|
615
|
+
twTip: 'Knobs the model made for this design. Every screen follows as you drag; nothing is written yet.',
|
|
616
|
+
twEmpty: 'This design has no tweaks yet. Ask the model to pick the dimensions most worth trying (spacing, radius, brand color, layout…) and put them here as knobs.',
|
|
617
|
+
twMake: 'Ask the model',
|
|
618
|
+
twFix: 'Ask the model to fix it',
|
|
619
|
+
twMakeText: '[Tweaks] Make 3–5 tweaks for this design: pick the dimensions most worth trying (spacing, radius, brand color, card layout…), write them to tweaks.json in the design folder as the ui-design skill describes under "Tweaks", wire up the styles, then run check_design.',
|
|
620
|
+
twFixText: '[Tweaks] tweaks.json has problems: {errs}. Fix it as the ui-design skill describes under "Tweaks".',
|
|
621
|
+
twAskHead: '[Tweaks] I set the tweaks in the sidebar to these values. Change the design to match (edit the style sources and update the matching values in tweaks.json):',
|
|
622
|
+
twAskTip: 'Let the model apply these values (and adjust related details)',
|
|
623
|
+
twSaveTip: 'CSS variables go into the defaults in tokens.css, attributes onto each screen\'s <html> (a version is kept before and after)',
|
|
624
|
+
twNoWrite: 'Dev-server pages cannot be written back. Ask the model instead',
|
|
625
|
+
twMoved: 'The design changed, so nothing was written. Try again',
|
|
626
|
+
twSaved: 'Wrote back {n}',
|
|
627
|
+
twMissed: 'No default found in the styles for {names}, not written. Ask the model instead',
|
|
628
|
+
systems: 'Brand design systems',
|
|
629
|
+
sysOpen: 'Brand design systems…',
|
|
630
|
+
sysEmptyLine: 'Have your own brand?',
|
|
631
|
+
sysTip: 'Colors, fonts, radii and components pulled from your site or project. The default one is used for every new design.',
|
|
632
|
+
sysEmpty: 'None yet. Enter your website or project folder below and let the model extract one.',
|
|
633
|
+
sysDefault: 'Default',
|
|
634
|
+
sysSet: 'Make default',
|
|
635
|
+
sysUnset: 'Stop using',
|
|
636
|
+
sysApply: 'Apply to this design',
|
|
637
|
+
sysPh: 'Website or project folder',
|
|
638
|
+
sysExtract: 'Extract',
|
|
639
|
+
sysExtractText: '[Design system] Extract my brand design system from {src} (call extract_design_system) and make it the default for new designs. Check it against the screenshots, then open the specimen for me.',
|
|
640
|
+
sysApplyText: '[Design system] Switch the design in the sidebar to the design system "{name}" ({dir}): copy its tokens.css and components.css into the design folder, link them on every screen, use its button, input and card classes, and follow its colors, fonts and radii. Keep the layout and content.',
|
|
641
|
+
sysSetOk: 'New designs will follow "{name}"',
|
|
642
|
+
sysUnsetOk: 'No default design system now',
|
|
643
|
+
handoff: 'Build as code',
|
|
644
|
+
hoTip: 'Turn the design into a real project: a handoff note and screenshots first, then one screen at a time, each pixel-compared with the design before moving on.',
|
|
645
|
+
hoFw: 'Build with',
|
|
646
|
+
hoSame: 'Same as the project ({label})',
|
|
647
|
+
hoHtml: 'Plain HTML / CSS',
|
|
648
|
+
hoDest: 'Put the code in',
|
|
649
|
+
hoScope: 'Scope',
|
|
650
|
+
hoAll: 'All {n} screens',
|
|
651
|
+
hoOne: 'This screen only',
|
|
652
|
+
hoScopeOne: 'only {name}',
|
|
653
|
+
hoGo: 'Start building',
|
|
654
|
+
hoProxy: 'This preview is a dev-server page, which is already code. Building works on designs made as local HTML.',
|
|
655
|
+
hoLoading: 'Looking at the session folder…',
|
|
656
|
+
hoText: '[Build as code] Build the design in the sidebar as a real project.\n- With: {fw}\n- In: {dest}\n- Scope: {scope}\nCall handoff_design (framework: {key}, dest: {dest}{only}) for the handoff note and follow HANDOFF.md one screen at a time. After each screen, run the dev server and call compare_design until the difference is under 3%, then move on. Do not edit the design folder.',
|
|
333
657
|
}
|
|
334
658
|
|
|
335
659
|
function makeT(ctx) {
|
|
@@ -434,6 +758,7 @@ window.__ModuleLoader__.load({
|
|
|
434
758
|
.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
759
|
background:rgba(18,18,24,.84);color:#ececf0;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
436
760
|
box-shadow:0 6px 18px rgba(0,0,0,.22);backdrop-filter:blur(8px);pointer-events:none;animation:ddp-in .2s ease}
|
|
761
|
+
.ddp-hint.side{left:calc((100% - 296px) / 2);max-width:calc(100% - 320px)}
|
|
437
762
|
@property --ddp-a{syntax:'<angle>';initial-value:0deg;inherits:false}
|
|
438
763
|
.ddp-ring{position:absolute;inset:0;z-index:4;pointer-events:none;padding:2.5px;
|
|
439
764
|
background:conic-gradient(from var(--ddp-a),transparent 0deg 140deg,${ACCENT} 240deg,#a78bfa 300deg,#5eead4 340deg,transparent 360deg);
|
|
@@ -451,6 +776,7 @@ window.__ModuleLoader__.load({
|
|
|
451
776
|
.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
777
|
.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
778
|
.ddp-chip.ok svg{width:14px;height:14px;flex:none;color:#4ade80}
|
|
779
|
+
.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
780
|
@keyframes ddp-rot{to{transform:rotate(360deg)}}
|
|
455
781
|
@keyframes ddp-txt{from{background-position:100% 0}to{background-position:-160% 0}}
|
|
456
782
|
@keyframes ddp-blink{50%{opacity:.25}}
|
|
@@ -528,6 +854,54 @@ window.__ModuleLoader__.load({
|
|
|
528
854
|
.ddp-empty .ddp-refline{margin-top:0}
|
|
529
855
|
.ddp-empty .lk{border:0;padding:0;background:transparent;color:${ACCENT};font:inherit;cursor:pointer}
|
|
530
856
|
.ddp-empty .lk:hover{text-decoration:underline;text-underline-offset:2px}
|
|
857
|
+
.ddp-ck{align-items:flex-start}
|
|
858
|
+
.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}
|
|
859
|
+
.ddp-ck .no.hi{background:#e5484d}
|
|
860
|
+
.ddp-tok.ddp-ck .nm{font-family:inherit;font-size:12px;line-height:17px}
|
|
861
|
+
.ddp-tok.ddp-ck .vl{white-space:normal;overflow-wrap:anywhere}
|
|
862
|
+
.ddp-check .ok{display:flex;align-items:center;gap:8px;padding:14px 6px;color:var(--dsw-alias-label-secondary)}
|
|
863
|
+
.ddp-check .ok svg{width:16px;height:16px;flex:none;color:#30a46c}
|
|
864
|
+
.ddp-check footer.row,.ddp-style footer.row{display:flex;gap:6px}
|
|
865
|
+
.ddp-check footer.row button,.ddp-style footer.row button{flex:1;justify-content:center;padding:0 6px}
|
|
866
|
+
.ddp-check footer .go,.ddp-style footer .go{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
|
|
867
|
+
.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}
|
|
868
|
+
.ddp-check footer button:disabled,.ddp-style footer button:disabled{opacity:.45;cursor:default}
|
|
869
|
+
.ddp-style .bd{padding:0 14px 10px}
|
|
870
|
+
.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}
|
|
871
|
+
.ddp-style .who code{font:11px/17px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary)}
|
|
872
|
+
.ddp-style .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
|
|
873
|
+
.ddp-style label{display:flex;align-items:baseline;gap:8px;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
874
|
+
.ddp-style label span{flex:1}
|
|
875
|
+
.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}
|
|
876
|
+
.ddp-style label em.v{color:${ACCENT}}
|
|
877
|
+
.ddp-style input[type=range]{display:block;width:100%;margin:0;accent-color:${ACCENT};cursor:pointer}
|
|
878
|
+
.ddp-style .chips{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin-top:7px}
|
|
879
|
+
.ddp-style .row>.chips:first-of-type{margin-top:0}
|
|
880
|
+
.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}
|
|
881
|
+
.ddp-style .chip:hover{color:var(--dsw-alias-label-primary)}
|
|
882
|
+
.ddp-style .chip.on{box-shadow:inset 0 0 0 1.5px ${ACCENT};color:var(--dsw-alias-label-primary)}
|
|
883
|
+
.ddp-style .chip.c{width:22px;padding:2px}
|
|
884
|
+
.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)}
|
|
885
|
+
.ddp-style input[type=color]{width:30px;height:22px;padding:0;border:0;border-radius:6px;background:none;cursor:pointer}
|
|
886
|
+
.ddp-style .seg{display:flex;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1))}
|
|
887
|
+
.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}
|
|
888
|
+
.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)}
|
|
889
|
+
.ddp-style .note{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
890
|
+
.ddp-style .cnt{padding:0 14px 6px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
891
|
+
.ddp-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px 8px}
|
|
892
|
+
.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}
|
|
893
|
+
.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}
|
|
894
|
+
.ddp-sc:hover .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
895
|
+
.ddp-sc:hover{color:var(--dsw-alias-label-primary)}
|
|
896
|
+
.ddp-sc .pv b{position:absolute;left:8px;top:6px;font-size:17px;line-height:1}
|
|
897
|
+
.ddp-sc .pv .bt{position:absolute;right:8px;top:8px;width:20px;height:9px}
|
|
898
|
+
.ddp-sc .pv .cd{position:absolute;left:8px;right:8px;bottom:7px;height:20px}
|
|
899
|
+
.ddp-sc .pv .ln{position:absolute;left:6px;top:6px;width:46%;height:3px;border-radius:2px;opacity:.85}
|
|
900
|
+
.ddp-sc .pv .ln.s{top:12px;width:30%;opacity:.6}
|
|
901
|
+
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
902
|
+
.ddp-stylepick{width:100%;max-width:440px;margin:4px 0 6px;text-align:left}
|
|
903
|
+
.ddp-stylepick>b{display:block;margin-bottom:2px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
904
|
+
.ddp-stylepick>p{margin:0 0 10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
531
905
|
.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
906
|
.ddp-tok:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
533
907
|
.ddp-tok .pv{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
|
@@ -542,6 +916,7 @@ window.__ModuleLoader__.load({
|
|
|
542
916
|
.ddp-tok .vl{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}
|
|
543
917
|
.ddp-btn.has::after{content:"";position:absolute;right:4px;top:4px;width:5px;height:5px;border-radius:50%;background:${ACCENT}}
|
|
544
918
|
.ddp-btn{position:relative}
|
|
919
|
+
.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
920
|
.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
921
|
.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
922
|
.ddp-ver:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
@@ -594,6 +969,39 @@ window.__ModuleLoader__.load({
|
|
|
594
969
|
.ddp-act .ddp-btn{height:22px;min-width:22px;padding:0 4px;color:${ACCENT}}
|
|
595
970
|
.ddp-act .ddp-btn svg{width:13px;height:13px}
|
|
596
971
|
@container (max-width:500px){.ddp-btn.pick span,.ddp-kind>.ddp-btn span{display:none}}
|
|
972
|
+
.ddp-tw .row:first-child{border-top:0}
|
|
973
|
+
.ddp-sys .bd{display:flex;flex-direction:column;gap:8px;padding:2px 10px 10px}
|
|
974
|
+
.ddp-sysc{display:flex;flex-direction:column;gap:5px;padding:8px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}
|
|
975
|
+
.ddp-sysc.def{box-shadow:inset 0 0 0 1.5px ${ACCENT}}
|
|
976
|
+
.ddp-sysc img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;object-position:top;border-radius:6px;background:var(--dsw-alias-bg-layer-1)}
|
|
977
|
+
.ddp-sysc .nm{display:flex;align-items:center;gap:6px;font-size:13px}
|
|
978
|
+
.ddp-sysc .nm b{flex:1;min-width:0;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
979
|
+
.ddp-sysc .nm span{flex:none;height:18px;padding:0 6px;border-radius:9px;background:${ACCENT};color:#fff;font-size:11px;line-height:18px}
|
|
980
|
+
.ddp-sysc .sw{display:flex;align-items:center;gap:3px;min-width:0}
|
|
981
|
+
.ddp-sysc .sw i{width:14px;height:14px;flex:none;border-radius:4px;box-shadow:inset 0 0 0 .5px rgba(127,127,127,.45)}
|
|
982
|
+
.ddp-sysc .sw em{margin-left:4px;font-size:11px;font-style:normal;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
983
|
+
.ddp-sysc small{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
984
|
+
.ddp-sysc .acts{display:flex;gap:4px}
|
|
985
|
+
.ddp-sysc .acts button{flex:1;height:26px;padding:0 6px;border:0;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:12px/26px inherit;cursor:pointer;white-space:nowrap}
|
|
986
|
+
.ddp-sysc .acts button:hover{color:var(--dsw-alias-label-primary)}
|
|
987
|
+
.ddp-sys form{display:flex;gap:6px}
|
|
988
|
+
.ddp-sys form input,.ddp-ho input[type=text]{flex:1;min-width:0;height:28px;padding:0 9px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px inherit;outline:none}
|
|
989
|
+
.ddp-sys form input:focus,.ddp-ho input[type=text]:focus{border-color:${ACCENT}}
|
|
990
|
+
.ddp-tokens.ddp-sys footer form button{width:auto;height:28px;flex:none;justify-content:center;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border-radius:999px;padding:0 12px}
|
|
991
|
+
.ddp-tokens.ddp-sys footer form button:disabled{opacity:.45;cursor:default}
|
|
992
|
+
.ddp-ho .bd{padding:0 14px 10px}
|
|
993
|
+
.ddp-ho .row{padding:9px 0 10px;border-top:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.16))}
|
|
994
|
+
.ddp-ho .row:first-child{border-top:0}
|
|
995
|
+
.ddp-ho label{display:block;margin-bottom:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
996
|
+
.ddp-ho input[type=text]{width:100%;box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
|
|
997
|
+
.ddp-ho .opt{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;padding:4px 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/18px inherit;text-align:left;cursor:pointer}
|
|
998
|
+
.ddp-ho .opt:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
999
|
+
.ddp-ho .opt i{width:12px;height:12px;flex:none;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--dsw-alias-label-tertiary)}
|
|
1000
|
+
.ddp-ho .opt.on{color:var(--dsw-alias-label-primary)}
|
|
1001
|
+
.ddp-ho .opt.on i{box-shadow:inset 0 0 0 4px ${ACCENT}}
|
|
1002
|
+
.ddp-ho footer .go{justify-content:center;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1)}
|
|
1003
|
+
.ddp-ho footer .go:hover{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);opacity:.88}
|
|
1004
|
+
.ddp-ho footer .go:disabled{opacity:.45;cursor:default}
|
|
597
1005
|
.ddp-more{display:none}
|
|
598
1006
|
.ddp-menu .o1,.ddp-menu .o2{display:none}
|
|
599
1007
|
@container (max-width:700px){.ddp-bar>.o1{display:none}.ddp-bar>.ddp-more{display:block}.ddp-menu .o1{display:flex}}
|
|
@@ -635,6 +1043,7 @@ window.__ModuleLoader__.load({
|
|
|
635
1043
|
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
1044
|
h('rect', { key: 3, x: 16.5, y: 8, width: 5, height: 10.5, rx: 1.2 })]),
|
|
637
1045
|
text: () => svg([h('path', { key: 1, d: 'M5 7V5h11v2M10.5 5v14M8 19h5' }), h('path', { key: 2, d: 'M17 12v8M15.5 12h3M15.5 20h3' })]),
|
|
1046
|
+
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
1047
|
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
1048
|
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
1049
|
play: () => svg([h('path', { key: 1, d: 'M8 5.5v13l10.5-6.5z', strokeLinejoin: 'round' })]),
|
|
@@ -647,6 +1056,12 @@ window.__ModuleLoader__.load({
|
|
|
647
1056
|
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
1057
|
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
1058
|
check: () => svg([h('path', { key: 1, d: 'M5 12.5l4.5 4.5L19 7.5', strokeWidth: 2.4 })]),
|
|
1059
|
+
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 })]),
|
|
1060
|
+
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' })]),
|
|
1061
|
+
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' })]),
|
|
1062
|
+
dial: () => svg([h('circle', { key: 1, cx: 12, cy: 13.5, r: 6.5 }), h('path', { key: 2, d: 'M12 13.5l3.2-3.2' }), h('path', { key: 3, d: 'M12 3.5v2M4.6 7.6l1.4 1.1M19.4 7.6L18 8.7' })]),
|
|
1063
|
+
code: () => svg([h('path', { key: 1, d: 'M8.5 7.5L4 12l4.5 4.5M15.5 7.5L20 12l-4.5 4.5' }), h('path', { key: 2, d: 'M13.5 5l-3 14' })]),
|
|
1064
|
+
library: () => svg([h('rect', { key: 1, x: 3.5, y: 4, width: 5, height: 16, rx: 1.2 }), h('rect', { key: 2, x: 10, y: 4, width: 5, height: 16, rx: 1.2 }), h('path', { key: 3, d: 'M16.6 5.4l3.7 13.9' })]),
|
|
650
1065
|
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
1066
|
}
|
|
652
1067
|
// 引导页上的大图标
|
|
@@ -719,6 +1134,19 @@ window.__ModuleLoader__.load({
|
|
|
719
1134
|
// ── 选中内容 → 输入框里的一段字 ──────────────────────────────────────────
|
|
720
1135
|
|
|
721
1136
|
/** 页面地址 → 用户认得的位置:本地文件给路径,开发服务器给 localhost 地址 */
|
|
1137
|
+
/**
|
|
1138
|
+
* 这一屏里现在显示的是哪页(在预览里点链接跳过页,就是跳到的那页)→ { key, url }。
|
|
1139
|
+
* 同一个预览服务器上的 .html 才算、旧版本不算;不然还是这一屏。写回文件按它找文件,别写到原来那页去。
|
|
1140
|
+
*/
|
|
1141
|
+
function pageOf(sc, href) {
|
|
1142
|
+
try {
|
|
1143
|
+
const u = new URL(href)
|
|
1144
|
+
const p = decodeURIComponent(u.pathname).replace(/^\/+/, '')
|
|
1145
|
+
if (u.origin === new URL(sc.url).origin && /\.html?$/i.test(p) && !p.startsWith('__dsh_design__/')) return { key: p, url: u.origin + u.pathname }
|
|
1146
|
+
} catch { /* 地址不对 */ }
|
|
1147
|
+
return { key: sc.key, url: sc.url }
|
|
1148
|
+
}
|
|
1149
|
+
|
|
722
1150
|
function whereOf(href, st) {
|
|
723
1151
|
try {
|
|
724
1152
|
const u = new URL(href)
|
|
@@ -803,6 +1231,231 @@ window.__ModuleLoader__.load({
|
|
|
803
1231
|
return `〔设计变量〕${tok.name}: ${tok.value}\n${whereOf(tok.href, st)}\n`
|
|
804
1232
|
}
|
|
805
1233
|
|
|
1234
|
+
// ── 风格卡:开局挑一个风格(或给已有的设计换风格),整套配色、字体、圆角、阴影、要点放进输入框 ──────
|
|
1235
|
+
// 配色都量过对比度:正文和次要文字对底色 ≥ 4.5:1,主色按钮上的字 ≥ 4.5:1
|
|
1236
|
+
|
|
1237
|
+
const SANS = '"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1238
|
+
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1239
|
+
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1240
|
+
const STYLES = [
|
|
1241
|
+
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1242
|
+
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
1243
|
+
head: SANS, w: 600, radius: 10, shadow: '0 1px 2px rgba(24,24,27,.06)',
|
|
1244
|
+
nzh: '靠字号和留白分层级,不靠色块;分隔用 1px 细线;蓝色只给链接和选中态;大标题字距收 -0.02em',
|
|
1245
|
+
nen: 'Hierarchy from type size and whitespace, not color blocks; 1px hairlines; blue only for links and selection; tighten big headings to -0.02em' },
|
|
1246
|
+
{ id: 'warm', zh: '温暖手作', en: 'Warm craft', dzh: '纸感底色、圆润、手绘感', den: 'Paper tones, soft and rounded, hand-made feel',
|
|
1247
|
+
c: { bg: '#F6EFE6', surface: '#FFF9F2', text: '#2B1D14', muted: '#6E5A4B', line: '#E7D9C8', primary: '#A9542A', accent: '#5E7A4A', on: '#FFFFFF', link: '#52703F' },
|
|
1248
|
+
head: SERIF, w: 600, radius: 16, shadow: '0 8px 24px rgba(120,72,30,.12)',
|
|
1249
|
+
nzh: '标题用衬线字,正文无衬线;插画用手绘线条和色块;阴影带暖色;中性色都偏暖,不用纯灰',
|
|
1250
|
+
nen: 'Serif headings, sans body; hand-drawn line illustrations and color blocks; warm-tinted shadows; warm neutrals, no pure gray' },
|
|
1251
|
+
{ id: 'editorial', zh: '杂志编辑', en: 'Editorial', dzh: '大标题、黑线栅格、红色点题', den: 'Big headlines, black-rule grid, a red accent',
|
|
1252
|
+
c: { bg: '#F4F1EA', surface: '#FFFFFF', text: '#111111', muted: '#555555', line: '#111111', primary: '#C8231B', accent: '#111111', on: '#FFFFFF', link: '#C8231B' },
|
|
1253
|
+
head: SERIF, w: 700, radius: 2, shadow: 'none',
|
|
1254
|
+
nzh: '标题很大、粗细对比强烈;用粗细两种黑线分栏;版式不对称、有留白;红色只在一两处;几乎没有圆角和阴影',
|
|
1255
|
+
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' },
|
|
1256
|
+
{ id: 'dark', zh: '深色高级', en: 'Dark luxe', dzh: '近黑底、香槟金点缀、大图', den: 'Near-black, champagne gold accents, big imagery',
|
|
1257
|
+
c: { bg: '#0E0E10', surface: '#18181B', text: '#F2EFE9', muted: '#A19C93', line: '#2C2C30', primary: '#C9A86A', accent: '#E8E2D6', on: '#17140E', link: '#C9A86A' },
|
|
1258
|
+
head: SERIF, w: 500, radius: 12, shadow: '0 12px 40px rgba(0,0,0,.45)',
|
|
1259
|
+
nzh: '金色只点在关键按钮和细线上;层级靠表面色深浅,不靠阴影;标题细一点、字距放开;留白要多',
|
|
1260
|
+
nen: 'Gold only on key buttons and fine lines; layers from surface shades, not shadows; lighter, tracked-out headings; generous space' },
|
|
1261
|
+
{ id: 'playful', zh: '活泼年轻', en: 'Playful', dzh: '粗描边、硬阴影、贴纸感', den: 'Thick outlines, hard shadows, sticker feel',
|
|
1262
|
+
c: { bg: '#FFF7E8', surface: '#FFFFFF', text: '#1D1B3A', muted: '#5D5A82', line: '#1D1B3A', primary: '#FF6A3D', accent: '#1F9E92', on: '#1D1B3A', link: '#137167' },
|
|
1263
|
+
head: ROUND, w: 800, radius: 20, shadow: '4px 4px 0 #1D1B3A',
|
|
1264
|
+
nzh: '卡片和按钮 2px 深色描边 + 不模糊的硬阴影;主色按钮上用深色字;元素可以略微倾斜、像贴纸;颜色最多三种',
|
|
1265
|
+
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' },
|
|
1266
|
+
{ id: 'fresh', zh: '清新自然', en: 'Fresh', dzh: '浅绿底、圆角卡片、轻插画', den: 'Pale green, rounded cards, light illustration',
|
|
1267
|
+
c: { bg: '#F3F7F2', surface: '#FFFFFF', text: '#1F2A22', muted: '#56665A', line: '#DCE6DA', primary: '#2F7D4F', accent: '#C9962A', on: '#FFFFFF', link: '#2F7D4F' },
|
|
1268
|
+
head: SANS, w: 600, radius: 14, shadow: '0 6px 18px rgba(47,125,79,.10)',
|
|
1269
|
+
nzh: '大面积浅色、卡片白底;插画用叶子、阳光这类自然元素;黄色只做小点缀;整体轻、透气',
|
|
1270
|
+
nen: 'Large light areas with white cards; nature motifs (leaves, sunlight) in illustrations; yellow only as a small accent; light and airy' },
|
|
1271
|
+
{ id: 'tech', zh: '专业科技', en: 'Pro / tech', dzh: '冷灰蓝、信息密、数字清楚', den: 'Cool gray-blue, dense info, crisp numbers',
|
|
1272
|
+
c: { bg: '#F5F7FB', surface: '#FFFFFF', text: '#0F172A', muted: '#55657D', line: '#E2E8F0', primary: '#2457F5', accent: '#0F8F8E', on: '#FFFFFF', link: '#2457F5' },
|
|
1273
|
+
head: SANS, w: 600, radius: 8, shadow: '0 1px 3px rgba(15,23,42,.08)',
|
|
1274
|
+
nzh: '信息密度高但对齐严格;数字用等宽(font-variant-numeric: tabular-nums);表格行高 44–48;状态用小色点和标签;主色只给主操作',
|
|
1275
|
+
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' },
|
|
1276
|
+
{ id: 'glass', zh: '玻璃质感', en: 'Glass', dzh: '柔光底、浮层毛玻璃', den: 'Soft glow background, frosted overlays',
|
|
1277
|
+
c: { bg: '#EAF0F6', surface: '#F7F9FC', text: '#10151C', muted: '#4F5B69', line: '#D5DEE8', primary: '#CC4520', accent: '#2B6CB0', on: '#FFFFFF', link: '#2B6CB0' },
|
|
1278
|
+
bgImage: 'radial-gradient(circle at 20% 15%, #FFD9C7 0, transparent 45%), radial-gradient(circle at 85% 70%, #C9E2FF 0, transparent 50%)',
|
|
1279
|
+
head: SANS, w: 600, radius: 18, shadow: '0 10px 30px rgba(16,21,28,.10)',
|
|
1280
|
+
nzh: '底色加两三团柔和的光斑(别用紫蓝渐变);只有浮层、卡片、标签栏用毛玻璃(半透明白 + backdrop-filter: blur(20px) + 1px 白色描边),正文区是实色;毛玻璃上的字照样要够深',
|
|
1281
|
+
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' },
|
|
1282
|
+
{ id: 'soft', zh: '柔和立体', en: 'Soft UI', dzh: '同色底、凸起凹陷的柔光阴影', den: 'Same-color surfaces with raised and pressed soft shadows',
|
|
1283
|
+
c: { bg: '#E9EDF2', surface: '#E9EDF2', text: '#2D3440', muted: '#556072', line: '#D3D9E1', primary: '#3E5BD8', accent: '#22A06B', on: '#FFFFFF', link: '#3E5BD8' },
|
|
1284
|
+
head: ROUND, w: 700, radius: 18, shadow: '6px 6px 14px #C8CED6, -6px -6px 14px #FFFFFF',
|
|
1285
|
+
nzh: '卡片和底同色,靠一亮一暗两道阴影凸起,按下用内阴影;容易看不清,所以主按钮一定用主色实底、文字够深;别整屏都是凸起',
|
|
1286
|
+
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' },
|
|
1287
|
+
]
|
|
1288
|
+
|
|
1289
|
+
/** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
|
|
1290
|
+
function formatStyle(t, pre, restyle) {
|
|
1291
|
+
const en = t('lang') === 'en'
|
|
1292
|
+
const c = pre.c
|
|
1293
|
+
const fonts = t('styleFonts', { head: `${pre.head} · ${pre.w}`, body: SANS })
|
|
1294
|
+
return t('styleText', {
|
|
1295
|
+
name: en ? pre.en : pre.zh,
|
|
1296
|
+
desc: en ? pre.den : pre.dzh,
|
|
1297
|
+
colors: t('styleColors', { ...c, bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1298
|
+
fonts,
|
|
1299
|
+
shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
|
|
1300
|
+
notes: en ? pre.nen : pre.nzh,
|
|
1301
|
+
ask: t(restyle ? 'styleRestyle' : 'styleStart'),
|
|
1302
|
+
})
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ── 调样式 ───────────────────────────────────────────────────────────────
|
|
1306
|
+
|
|
1307
|
+
const VAR_ONLY = /^var\(\s*(--[\w-]+)\s*\)$/
|
|
1308
|
+
/** 面板上能调的几项:属性 → [文案, 哪一行](上下 / 左右内边距各管两条) */
|
|
1309
|
+
const STYLE_LABEL = { 'font-size': 'styleFont', 'font-weight': 'styleWeight', color: 'styleColor', background: 'styleBg', 'border-radius': 'styleRadius',
|
|
1310
|
+
'padding-top': 'stylePadY', 'padding-bottom': 'stylePadY', 'padding-left': 'stylePadX', 'padding-right': 'stylePadX', gap: 'styleGap' }
|
|
1311
|
+
|
|
1312
|
+
/** 变量名 → 它现在的值(页面 :root 上的) */
|
|
1313
|
+
const tokenVal = (name, tokens) => { const tok = (tokens || []).find((x) => x.name === name); return tok ? String(tok.value).trim() : '' }
|
|
1314
|
+
|
|
1315
|
+
/** 值 → 像素数(var(--x) 先换成变量的值;rem / em 按 16);认不出返回 null */
|
|
1316
|
+
function pxOf(v, tokens) {
|
|
1317
|
+
let s = String(v || '').trim()
|
|
1318
|
+
const m = VAR_ONLY.exec(s)
|
|
1319
|
+
if (m) s = tokenVal(m[1], tokens)
|
|
1320
|
+
const n = parseFloat(s)
|
|
1321
|
+
if (!isFinite(n)) return null
|
|
1322
|
+
return /r?em$/.test(s) ? n * 16 : n
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** 透明的颜色(没设背景时 getComputedStyle 给 rgba(0, 0, 0, 0)) */
|
|
1326
|
+
const clearColor = (v) => /^(transparent|rgba\(.*,\s*0\)|hsla\(.*,\s*0%?\))$/i.test(String(v || '').trim())
|
|
1327
|
+
|
|
1328
|
+
/** 值 → '#rrggbb'(颜色框只认这个);透明的给白色,不然显示成黑的 */
|
|
1329
|
+
const colorHex = (v, tokens) => {
|
|
1330
|
+
const m = VAR_ONLY.exec(String(v || '').trim())
|
|
1331
|
+
const c = m ? tokenVal(m[1], tokens) : v
|
|
1332
|
+
return '#' + (clearColor(c) ? 'ffffff' : hexOf(c))
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/** 给人看的值:颜色换成 #RRGGBB,透明的说透明,别的原样 */
|
|
1336
|
+
const showVal = (prop, v, t) => {
|
|
1337
|
+
if (!/^(color|background)$/.test(prop)) return String(v)
|
|
1338
|
+
if (clearColor(v)) return t ? t('styleClear') : 'transparent'
|
|
1339
|
+
return /^(rgb|hsl)/i.test(String(v)) ? '#' + hexOf(v).toUpperCase() : String(v)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** 改了的几项 → 「字号 15px → 17px」这种;上下 / 左右内边距只说一次 */
|
|
1343
|
+
function styleChanges(t, sty) {
|
|
1344
|
+
const out = []
|
|
1345
|
+
const seen = new Set()
|
|
1346
|
+
for (const [k, v] of Object.entries((sty && sty.live) || {})) {
|
|
1347
|
+
const key = STYLE_LABEL[k]
|
|
1348
|
+
if (!key || seen.has(key)) continue
|
|
1349
|
+
seen.add(key)
|
|
1350
|
+
out.push(t('styleArrow', { prop: t(key), from: showVal(k, sty.base[k], t), to: showVal(k, v, t) }))
|
|
1351
|
+
}
|
|
1352
|
+
return out
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const ruleKey = (r) => `${r.href}|${r.sel}|${r.n}`
|
|
1356
|
+
/** 规则在哪:服务端找到了行号就是「app.css 第 42 行」,没找到就是文件名 */
|
|
1357
|
+
function ruleAt(t, r, located) {
|
|
1358
|
+
const hit = located && located.get(ruleKey(r))
|
|
1359
|
+
if (hit && hit.file) return hit.line ? t('ruleAt', { file: hit.file, line: hit.line }) : hit.file
|
|
1360
|
+
try { return decodeURIComponent(new URL(r.href).pathname.split('/').pop()) || r.href } catch { return String(r.href || '') }
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** 改了的这几项现在各由哪条规则决定(同一条规则的合成一行),模型照着去改那条,不用再 grep */
|
|
1364
|
+
function ruleLines(t, sty, located) {
|
|
1365
|
+
const groups = new Map()
|
|
1366
|
+
for (const k of Object.keys((sty && sty.live) || {})) {
|
|
1367
|
+
const r = sty.rules && sty.rules[k]
|
|
1368
|
+
const key = STYLE_LABEL[k]
|
|
1369
|
+
if (!r || !key) continue
|
|
1370
|
+
const g = r.inline ? `inline|${r.sel ? ruleKey(r) : ''}` : ruleKey(r)
|
|
1371
|
+
if (!groups.has(g)) groups.set(g, { r, props: [] })
|
|
1372
|
+
const label = t(key)
|
|
1373
|
+
if (!groups.get(g).props.includes(label)) groups.get(g).props.push(label)
|
|
1374
|
+
}
|
|
1375
|
+
const out = []
|
|
1376
|
+
for (const { r, props } of groups.values()) {
|
|
1377
|
+
const p = props.join('、')
|
|
1378
|
+
if (r.inline) out.push(t('ruleInline', { props: p, over: r.sel ? t('ruleOver', { sel: r.sel, at: ruleAt(t, r, located) }) : '' }))
|
|
1379
|
+
else out.push(t(r.maybe ? 'ruleMaybe' : 'ruleFrom', { props: p, sel: r.sel, at: ruleAt(t, r, located) }))
|
|
1380
|
+
}
|
|
1381
|
+
return out
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** 调好的样式交给模型:哪一屏、哪个元素、改了什么、现在由哪条规则决定(located:服务端找到的行号),让它改进样式表 */
|
|
1385
|
+
function formatStyleAsk(t, sty, st, located) {
|
|
1386
|
+
const rules = ruleLines(t, sty, located)
|
|
1387
|
+
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) })
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// ── 设计体检 ─────────────────────────────────────────────────────────────
|
|
1391
|
+
|
|
1392
|
+
/** 一条问题的具体数字(「#C8C8C8 配 #FFFFFF,1.7:1」这种) */
|
|
1393
|
+
function checkDetail(t, it) {
|
|
1394
|
+
const edge = it.edge === 'top' || it.edge === 'bottom' ? it.edge : ''
|
|
1395
|
+
return t('cd_' + it.type, {
|
|
1396
|
+
...it,
|
|
1397
|
+
n: it.sizes ? it.sizes.length : '',
|
|
1398
|
+
sizes: it.sizes ? it.sizes.join(' / ') : '',
|
|
1399
|
+
otherText: String(it.otherText || '').slice(0, 20),
|
|
1400
|
+
edgeName: edge ? t('ckEdge_' + edge) : '',
|
|
1401
|
+
when: edge ? t('ckWhen_' + edge) : '',
|
|
1402
|
+
what: it.type === 'missing' ? t('ckWhat_' + (['css', 'script', 'font'].includes(it.what) ? it.what : 'other')) : '',
|
|
1403
|
+
at: it.type === 'jserr' && it.src ? ` (${it.src}${it.line ? ':' + it.line : ''})` : '',
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** 量的时候这一屏多大、页面多高、固定栏离内容多远(让模型知道量的是什么) */
|
|
1408
|
+
function sizeLine(t, m) {
|
|
1409
|
+
if (!m || !m.w) return ''
|
|
1410
|
+
let out = t('ckSize', { w: m.w, h: m.h, page: m.page || m.h })
|
|
1411
|
+
for (const b of m.bars || []) if (b.gap != null) out += t('ckSizeBar', { edgeName: t('ckEdge_' + b.edge), bar: b.name, gap: b.gap })
|
|
1412
|
+
return out
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** 体检结果 → 输入框里的一段字:按屏分组,带文件位置、选择器和数字,模型照着改 */
|
|
1416
|
+
function formatCheck(t, list, st, head = 'checkAsk') {
|
|
1417
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
1418
|
+
const lines = [t(head, { n, s: list.length })]
|
|
1419
|
+
for (const sc of list) {
|
|
1420
|
+
if (sc.error && head !== 'checkAsk') lines.push('', t('ckToolErr', { label: sc.label, msg: sc.error }))
|
|
1421
|
+
if (!sc.issues.length) continue
|
|
1422
|
+
const sz = head === 'checkAsk' ? '' : sizeLine(t, sc.metrics)
|
|
1423
|
+
lines.push('', t('ckScreen', { label: sc.label, where: whereOf(sc.url, st) }) + (sz ? ` · ${sz}` : ''))
|
|
1424
|
+
sc.issues.forEach((it, i) => lines.push(t('ckItem', {
|
|
1425
|
+
i: i + 1,
|
|
1426
|
+
type: t('ck_' + it.type),
|
|
1427
|
+
where: it.sel || '',
|
|
1428
|
+
text: it.text ? t('ckQuote', { text: it.text }) : '',
|
|
1429
|
+
detail: checkDetail(t, it),
|
|
1430
|
+
})))
|
|
1431
|
+
}
|
|
1432
|
+
return lines.join('\n') + '\n'
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* 一屏是不是「同一屏的几个版本」之一:home-a / home-b / home-c,或 login / login-b / login-c(和 core.js 的 placeScreen 一个规则)。
|
|
1437
|
+
* 是就给出 { stem, letter, others }(others:同组的其他文件名),不是给 null
|
|
1438
|
+
*/
|
|
1439
|
+
function takesOf(keys, key) {
|
|
1440
|
+
const VAR = /^(.+)-([a-z])\.html?$/i
|
|
1441
|
+
const part = (x) => {
|
|
1442
|
+
const v = VAR.exec(x)
|
|
1443
|
+
return v ? { stem: v[1].toLowerCase(), letter: v[2].toLowerCase() } : { stem: x.toLowerCase().replace(/\.html?$/i, ''), letter: '' }
|
|
1444
|
+
}
|
|
1445
|
+
if (!/\.html?$/i.test(key)) return null
|
|
1446
|
+
const me = part(key)
|
|
1447
|
+
const others = keys.filter((x) => x !== key && /\.html?$/i.test(x) && part(x).stem === me.stem && (me.letter || part(x).letter))
|
|
1448
|
+
if (!others.length || (!me.letter && !others.some((x) => part(x).letter))) return null
|
|
1449
|
+
return { stem: VAR.exec(key) ? VAR.exec(key)[1] : key.replace(/\.html?$/i, ''), letter: me.letter, others }
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** 体检结果 → 交给 check_design 工具的一段字(模型自己调的,不带〔设计体检〕开头) */
|
|
1453
|
+
function formatCheckTool(t, list, st) {
|
|
1454
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
1455
|
+
if (n || list.some((x) => x.error)) return formatCheck(t, list, st, 'ckToolHead')
|
|
1456
|
+
return t('ckToolOk', { s: list.length, labels: list.map((x) => { const sz = sizeLine(t, x.metrics); return sz ? `${x.label} ${sz}` : x.label }).join(' / ') })
|
|
1457
|
+
}
|
|
1458
|
+
|
|
806
1459
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
807
1460
|
|
|
808
1461
|
const SHOW_COLS = { app: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
@@ -1140,6 +1793,32 @@ window.__ModuleLoader__.load({
|
|
|
1140
1793
|
return canvasBlob(c)
|
|
1141
1794
|
}
|
|
1142
1795
|
|
|
1796
|
+
/** CSS 颜色 → 'RRGGBB'(PPT 留边的底色);认不出就白色 */
|
|
1797
|
+
function hexOf(color) {
|
|
1798
|
+
try {
|
|
1799
|
+
const c = document.createElement('canvas')
|
|
1800
|
+
c.width = c.height = 1
|
|
1801
|
+
const ctx = c.getContext('2d')
|
|
1802
|
+
ctx.fillStyle = '#ffffff'
|
|
1803
|
+
ctx.fillStyle = color || '#ffffff'
|
|
1804
|
+
ctx.fillRect(0, 0, 1, 1)
|
|
1805
|
+
return [...ctx.getImageData(0, 0, 1, 1).data.slice(0, 3)].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
1806
|
+
} catch { return 'ffffff' }
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/** 截图 → JPEG(base64)。PDF / PPT 里一页一张,JPEG 小得多;透明的地方垫白 */
|
|
1810
|
+
async function jpegB64(img) {
|
|
1811
|
+
const c = document.createElement('canvas')
|
|
1812
|
+
c.width = img.width
|
|
1813
|
+
c.height = img.height
|
|
1814
|
+
const ctx = c.getContext('2d')
|
|
1815
|
+
ctx.fillStyle = '#ffffff'
|
|
1816
|
+
ctx.fillRect(0, 0, c.width, c.height)
|
|
1817
|
+
ctx.drawImage(img, 0, 0)
|
|
1818
|
+
const blob = await new Promise((resolve, reject) => c.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/jpeg', 0.9))
|
|
1819
|
+
return blobB64(blob)
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1143
1822
|
function blobB64(blob) {
|
|
1144
1823
|
return new Promise((resolve, reject) => {
|
|
1145
1824
|
const r = new FileReader()
|
|
@@ -1476,15 +2155,32 @@ window.__ModuleLoader__.load({
|
|
|
1476
2155
|
function away(src) {
|
|
1477
2156
|
try { return !!src && LOCAL_HOST.test(new URL(src).hostname) && /^https?:$/.test(location.protocol) && !LOCAL_HOST.test(location.hostname) } catch { return false }
|
|
1478
2157
|
}
|
|
2158
|
+
// 远程看(本地文件的预览):服务端把这一屏连同样式、图片打成一份 HTML,放进沙盒 iframe 的 srcdoc
|
|
2159
|
+
const remOf = (p) => (p.remote && away(p.src) ? p.remote : null)
|
|
2160
|
+
const OLD_VER = /^__dsh_design__\/v\/(\d+)\/(.*)$/
|
|
2161
|
+
/** 这一屏(预览服务器上的地址)打包后的取法 */
|
|
2162
|
+
function bundleOf(href, sid) {
|
|
2163
|
+
let p
|
|
2164
|
+
try { p = new URL(href).pathname } catch { return '' }
|
|
2165
|
+
try { p = decodeURIComponent(p) } catch { /* 原样 */ }
|
|
2166
|
+
p = p.replace(/^\/+/, '')
|
|
2167
|
+
let v = ''
|
|
2168
|
+
const m = OLD_VER.exec(p)
|
|
2169
|
+
if (m) { v = m[1]; p = m[2] }
|
|
2170
|
+
if (!p || p.endsWith('/')) p += 'index.html'
|
|
2171
|
+
return `${API}bundle?${new URLSearchParams({ session: sid, key: p, v, parent: location.origin })}`
|
|
2172
|
+
}
|
|
1479
2173
|
|
|
1480
2174
|
function Screen(props) {
|
|
1481
2175
|
const src = props.src
|
|
1482
2176
|
const origin = src ? new URL(src).origin : ''
|
|
1483
2177
|
const [frames, setFrames] = useState([])
|
|
1484
2178
|
const [cur, setCur] = useState(0)
|
|
2179
|
+
const [failed, setFailed] = useState(false) // 远程看:打包没取回来
|
|
2180
|
+
const rem = remOf(props)
|
|
1485
2181
|
const wins = useRef(new Map())
|
|
1486
2182
|
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 }
|
|
2183
|
+
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
2184
|
const curRef = useRef(0)
|
|
1489
2185
|
const P = useRef(props)
|
|
1490
2186
|
P.current = props
|
|
@@ -1503,6 +2199,16 @@ window.__ModuleLoader__.load({
|
|
|
1503
2199
|
postTo(curRef.current, { type: 'capture', id, ...opts })
|
|
1504
2200
|
setTimeout(() => { if (caps.current.has(id)) { caps.current.delete(id); reject(new Error('timeout')) } }, 30000)
|
|
1505
2201
|
}),
|
|
2202
|
+
// 设计体检:页面里量完回 { issues },问题同时标在页面上(quiet:只要结果,不标)
|
|
2203
|
+
check: (kind, quiet) => new Promise((resolve, reject) => {
|
|
2204
|
+
if (!curRef.current) { reject(new Error('not loaded')); return }
|
|
2205
|
+
const id = `${Date.now()}-${++seq}`
|
|
2206
|
+
caps.current.set(id, { resolve, reject })
|
|
2207
|
+
postTo(curRef.current, { type: 'check', id, kind, quiet: !!quiet })
|
|
2208
|
+
setTimeout(() => { if (caps.current.has(id)) { caps.current.delete(id); reject(new Error('timeout')) } }, 15000)
|
|
2209
|
+
}),
|
|
2210
|
+
checkFocus: (i) => { if (curRef.current) postTo(curRef.current, { type: 'checkFocus', i }) },
|
|
2211
|
+
checkClear: () => { if (curRef.current) postTo(curRef.current, { type: 'checkClear' }) },
|
|
1506
2212
|
}
|
|
1507
2213
|
}
|
|
1508
2214
|
// 登记到右侧栏:导出图片时按屏找到它
|
|
@@ -1516,7 +2222,10 @@ window.__ModuleLoader__.load({
|
|
|
1516
2222
|
|
|
1517
2223
|
const postTo = (id, msg) => {
|
|
1518
2224
|
const f = wins.current.get(id)
|
|
1519
|
-
if (f
|
|
2225
|
+
if (!f || !f.contentWindow) return
|
|
2226
|
+
// 远程看:沙盒里的页面是不透明的源,只能发给 *(发的都是界面指令,没有要保密的)
|
|
2227
|
+
if (f.hasAttribute('srcdoc')) f.contentWindow.postMessage({ dshDesign: 1, ...msg }, '*')
|
|
2228
|
+
else if (origin) f.contentWindow.postMessage({ dshDesign: 1, ...msg }, origin)
|
|
1520
2229
|
}
|
|
1521
2230
|
|
|
1522
2231
|
// 在后台载一帧新的;正在载就记一笔,等它换上来再载
|
|
@@ -1525,13 +2234,44 @@ window.__ModuleLoader__.load({
|
|
|
1525
2234
|
if (L.loading) { L.again = true; return }
|
|
1526
2235
|
const id = L.next++
|
|
1527
2236
|
L.loading = id
|
|
2237
|
+
if (seed) L.seedOf.set(id, seed)
|
|
2238
|
+
const r = remOf(P.current)
|
|
2239
|
+
if (r) {
|
|
2240
|
+
const gen = L.gen
|
|
2241
|
+
fetch(bundleOf(L.at || P.current.src, r.sid), { credentials: 'same-origin', cache: 'no-store' })
|
|
2242
|
+
.then((res) => res.json().then((d) => {
|
|
2243
|
+
if (!res.ok || !d || typeof d.html !== 'string') throw new Error((d && d.error) || `HTTP ${res.status}`)
|
|
2244
|
+
return d
|
|
2245
|
+
}))
|
|
2246
|
+
.then((d) => {
|
|
2247
|
+
if (L.gen !== gen || L.loading !== id) return
|
|
2248
|
+
L.deps = Array.isArray(d.deps) ? d.deps : null
|
|
2249
|
+
L.fails = 0
|
|
2250
|
+
setFailed(false)
|
|
2251
|
+
setFrames((fs) => [...fs, { id, doc: d.html }])
|
|
2252
|
+
})
|
|
2253
|
+
.catch(() => {
|
|
2254
|
+
if (L.gen !== gen || L.loading !== id) return
|
|
2255
|
+
L.loading = 0
|
|
2256
|
+
if (!curRef.current) setFailed(true)
|
|
2257
|
+
// 没取回来:那次改动还没显示出来,过一会儿再试(最多三回),别等下一次改动
|
|
2258
|
+
L.fails = (L.fails || 0) + 1
|
|
2259
|
+
if (L.again || L.fails <= 3) { L.again = false; setTimeout(() => { if (L.gen === gen) load(null) }, 1500 * Math.min(L.fails, 3)) }
|
|
2260
|
+
})
|
|
2261
|
+
return
|
|
2262
|
+
}
|
|
1528
2263
|
let url = P.current.src
|
|
1529
2264
|
try { if (seed && seed.href && new URL(seed.href).origin === origin) url = seed.href } catch { /* 用原地址 */ }
|
|
1530
|
-
if (seed) L.seedOf.set(id, seed)
|
|
1531
2265
|
setFrames((fs) => [...fs, { id, src: url }])
|
|
1532
2266
|
}
|
|
1533
2267
|
|
|
1534
2268
|
const pickMsg = () => ({ type: 'pick', hint: false }) // 提示放在右侧栏(页面顶上会被手机的灵动岛挡住)
|
|
2269
|
+
// 调样式:开模式、选中右侧栏记着的那个元素、把改到一半的样式再套上(新载的一帧要从头告诉它)
|
|
2270
|
+
const styleTo = (id, p) => {
|
|
2271
|
+
postTo(id, { type: 'styleMode', on: true })
|
|
2272
|
+
if (p.styleSel) postTo(id, { type: 'styleSel', selector: p.styleSel })
|
|
2273
|
+
if (p.styleCss) postTo(id, { type: 'style', css: p.styleCss })
|
|
2274
|
+
}
|
|
1535
2275
|
|
|
1536
2276
|
// 一帧好了:淡入换上来
|
|
1537
2277
|
const swap = (id, changes) => {
|
|
@@ -1548,6 +2288,7 @@ window.__ModuleLoader__.load({
|
|
|
1548
2288
|
if (p.picking) postTo(id, pickMsg())
|
|
1549
2289
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1550
2290
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2291
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1551
2292
|
if (first && p.onShown) p.onShown()
|
|
1552
2293
|
if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
|
|
1553
2294
|
L.file = ''
|
|
@@ -1565,16 +2306,21 @@ window.__ModuleLoader__.load({
|
|
|
1565
2306
|
L.again = false
|
|
1566
2307
|
L.seeds.clear()
|
|
1567
2308
|
L.seedOf.clear()
|
|
2309
|
+
L.gen++
|
|
2310
|
+
L.at = ''
|
|
2311
|
+
L.deps = null
|
|
2312
|
+
L.fails = 0
|
|
1568
2313
|
curRef.current = 0
|
|
1569
2314
|
setCur(0)
|
|
1570
2315
|
setFrames([])
|
|
2316
|
+
setFailed(false)
|
|
1571
2317
|
if (src) setTimeout(() => load(null), 0)
|
|
1572
|
-
}, [src])
|
|
2318
|
+
}, [src, !!rem])
|
|
1573
2319
|
|
|
1574
2320
|
// 页面发来的消息(几屏共用一个源,按 iframe 认是不是自己的)
|
|
1575
2321
|
useEffect(() => {
|
|
1576
2322
|
const on = (e) => {
|
|
1577
|
-
if (!origin || e.origin !== origin) return
|
|
2323
|
+
if (rem ? e.origin !== 'null' : !origin || e.origin !== origin) return
|
|
1578
2324
|
let id = 0
|
|
1579
2325
|
for (const [k, f] of wins.current) if (f && f.contentWindow === e.source) { id = k; break }
|
|
1580
2326
|
const d = e.data
|
|
@@ -1595,6 +2341,7 @@ window.__ModuleLoader__.load({
|
|
|
1595
2341
|
L.readied.add(id)
|
|
1596
2342
|
L.title = d.title || ''
|
|
1597
2343
|
if (p.dark) postTo(id, { type: 'theme', dark: true })
|
|
2344
|
+
if (p.tweak) postTo(id, { type: 'tweak', vars: p.tweak.vars, attrs: p.tweak.attrs })
|
|
1598
2345
|
if (p.onMeta) p.onMeta(d.meta || '', d.title || '', Array.isArray(d.tokens) ? d.tokens : [], !!d.dark, Array.isArray(d.links) ? d.links : [])
|
|
1599
2346
|
if (p.onHref && d.href) p.onHref(d.href)
|
|
1600
2347
|
if (isCur) {
|
|
@@ -1603,6 +2350,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
2350
|
if (p.picking) postTo(id, pickMsg())
|
|
1604
2351
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
1605
2352
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2353
|
+
if (p.styleEdit) styleTo(id, p)
|
|
1606
2354
|
if (d.changes && d.changes.count && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1607
2355
|
} else if (id > curRef.current) swap(id, d.changes)
|
|
1608
2356
|
break
|
|
@@ -1622,14 +2370,20 @@ window.__ModuleLoader__.load({
|
|
|
1622
2370
|
if (isCur && p.onNote) p.onNote({ changes: d.changes, file: '' })
|
|
1623
2371
|
break
|
|
1624
2372
|
case 'cancelled':
|
|
1625
|
-
if (isCur
|
|
2373
|
+
if (!isCur) break
|
|
2374
|
+
if (d.style) { if (p.onStyleCancel) p.onStyleCancel() } else if (p.onCancelled) p.onCancelled()
|
|
2375
|
+
break
|
|
2376
|
+
case 'stylePick':
|
|
2377
|
+
if (isCur && p.styleEdit && p.onStylePick && d.payload && d.payload.loc) p.onStylePick(d.payload)
|
|
1626
2378
|
break
|
|
1627
2379
|
// 点选、改字只在用户开着对应模式时收(页面自己的脚本也能发这种消息,不能随时往输入框里塞字、往文件里写)
|
|
1628
2380
|
case 'picked':
|
|
1629
2381
|
if (isCur && p.picking && p.onPicked) p.onPicked(d.payload)
|
|
1630
2382
|
break
|
|
1631
2383
|
case 'nav':
|
|
1632
|
-
if (isCur
|
|
2384
|
+
if (!isCur || typeof d.path !== 'string') break
|
|
2385
|
+
if (p.onNav) p.onNav(d.path)
|
|
2386
|
+
else if (remOf(p)) { L.at = origin + d.path; load(null) } // 远程看:页面里点了跳页,这一框换成那一页(和本机预览时 iframe 自己跳过去一样)
|
|
1633
2387
|
break
|
|
1634
2388
|
case 'edited':
|
|
1635
2389
|
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 +2391,7 @@ window.__ModuleLoader__.load({
|
|
|
1637
2391
|
case 'editDone':
|
|
1638
2392
|
if (isCur && p.onEditDone) p.onEditDone()
|
|
1639
2393
|
break
|
|
2394
|
+
case 'checked':
|
|
1640
2395
|
case 'captured': {
|
|
1641
2396
|
const c = caps.current.get(d.id)
|
|
1642
2397
|
if (c) {
|
|
@@ -1651,7 +2406,7 @@ window.__ModuleLoader__.load({
|
|
|
1651
2406
|
}
|
|
1652
2407
|
window.addEventListener('message', on)
|
|
1653
2408
|
return () => window.removeEventListener('message', on)
|
|
1654
|
-
}, [origin, src])
|
|
2409
|
+
}, [origin, src, !!rem])
|
|
1655
2410
|
|
|
1656
2411
|
// 模型在不在干活、画布缩放比例、选取开关:告诉页面
|
|
1657
2412
|
useEffect(() => { if (cur) postTo(cur, { type: 'busy', on: !!props.busy }) }, [!!props.busy, cur])
|
|
@@ -1659,7 +2414,14 @@ window.__ModuleLoader__.load({
|
|
|
1659
2414
|
useEffect(() => { if (cur) postTo(cur, { type: 'zoom', z: zq / 100 }) }, [zq])
|
|
1660
2415
|
useEffect(() => { if (cur) postTo(cur, props.picking ? pickMsg() : { type: 'cancel' }) }, [!!props.picking])
|
|
1661
2416
|
useEffect(() => { if (cur) postTo(cur, { type: 'theme', dark: !!props.dark }) }, [!!props.dark])
|
|
2417
|
+
useEffect(() => { if (cur) { if (props.styleEdit) styleTo(cur, props); else postTo(cur, { type: 'styleMode', on: false }) } }, [!!props.styleEdit])
|
|
2418
|
+
useEffect(() => { if (cur && props.styleEdit) postTo(cur, { type: 'styleSel', selector: props.styleSel || '' }) }, [props.styleSel || ''])
|
|
2419
|
+
const cssSig = JSON.stringify(props.styleCss || {})
|
|
2420
|
+
useEffect(() => { if (cur && props.styleEdit) postTo(cur, { type: 'style', css: props.styleCss || {} }) }, [cssSig])
|
|
1662
2421
|
useEffect(() => { if (cur) postTo(cur, { type: 'demo', on: !!props.demo }) }, [!!props.demo])
|
|
2422
|
+
// 调节滑块:拖的时候改页面上的 CSS 变量 / <html> 属性(新载的一帧在 ready 时套上)
|
|
2423
|
+
const twSig = JSON.stringify(props.tweak || null)
|
|
2424
|
+
useEffect(() => { if (cur) postTo(cur, { type: 'tweak', vars: (props.tweak || {}).vars || {}, attrs: (props.tweak || {}).attrs || {} }) }, [twSig])
|
|
1663
2425
|
useEffect(() => {
|
|
1664
2426
|
// 刚退出改字(点按钮、Esc、模型开始干活):页面会把正在改的那一处交上来,这一小会儿还收
|
|
1665
2427
|
if (editWas.current && !props.textEdit) editOffAt.current = Date.now()
|
|
@@ -1668,17 +2430,46 @@ window.__ModuleLoader__.load({
|
|
|
1668
2430
|
}, [!!props.textEdit])
|
|
1669
2431
|
|
|
1670
2432
|
// 刷新:先问当前这帧要滚动位置和指纹,问不到就直接载
|
|
1671
|
-
const
|
|
1672
|
-
useEffect(() => {
|
|
1673
|
-
if (seq.current === props.reloadSeq) return
|
|
1674
|
-
seq.current = props.reloadSeq
|
|
2433
|
+
const refresh = () => {
|
|
1675
2434
|
const L = life.current
|
|
1676
2435
|
if (!curRef.current) { load(null); return }
|
|
1677
2436
|
clearTimeout(L.snapTimer)
|
|
1678
2437
|
L.snapTimer = setTimeout(() => load(null), 400)
|
|
1679
2438
|
postTo(curRef.current, { type: 'snapshot' })
|
|
2439
|
+
}
|
|
2440
|
+
const seq = useRef(props.reloadSeq)
|
|
2441
|
+
useEffect(() => {
|
|
2442
|
+
if (seq.current === props.reloadSeq) return
|
|
2443
|
+
seq.current = props.reloadSeq
|
|
2444
|
+
refresh()
|
|
1680
2445
|
}, [props.reloadSeq])
|
|
1681
2446
|
|
|
2447
|
+
// 远程看:页面连不上预览服务器、收不到它的改动通知,跟着右侧栏轮询到的「目录里改了哪些文件」重新打包
|
|
2448
|
+
// (改的全是这一屏没用到的就不动;旧版本不会变)。两次轮询之间可能来了好几批:按序号取上次看到之后所有批的并集,
|
|
2449
|
+
// 记录断了档(中间的批已经挤掉了)就不知道改了啥,整屏重打包
|
|
2450
|
+
const changes = (rem && rem.changes) || []
|
|
2451
|
+
const chgN = changes.length ? changes[changes.length - 1].n : 0
|
|
2452
|
+
const chg = rem ? `${rem.at || 0}|${chgN}` : ''
|
|
2453
|
+
const chgWas = useRef(chg)
|
|
2454
|
+
const seenN = useRef(chgN)
|
|
2455
|
+
useEffect(() => {
|
|
2456
|
+
if (chgWas.current === chg) return
|
|
2457
|
+
chgWas.current = chg
|
|
2458
|
+
const from = seenN.current
|
|
2459
|
+
seenN.current = chgN
|
|
2460
|
+
if (!rem || !rem.at) return
|
|
2461
|
+
const L = life.current
|
|
2462
|
+
let path = ''
|
|
2463
|
+
try { path = new URL(L.at || src).pathname.replace(/^\/+/, '') } catch { /* 地址不对 */ }
|
|
2464
|
+
if (OLD_VER.test(path)) return
|
|
2465
|
+
const fresh = changes.filter((c) => c.n > from)
|
|
2466
|
+
const unknown = !changes.length || changes[0].n > from + 1 || fresh.some((c) => !(c.files || []).length)
|
|
2467
|
+
const files = [...new Set(fresh.flatMap((c) => c.files || []))]
|
|
2468
|
+
if (!unknown && L.deps && !files.some((f) => L.deps.includes(f))) return
|
|
2469
|
+
L.file = files[files.length - 1] || L.file
|
|
2470
|
+
refresh()
|
|
2471
|
+
}, [chg])
|
|
2472
|
+
|
|
1682
2473
|
// 页面没装上脚本(极少数,比如不是 HTML):载完等一会儿也换上来
|
|
1683
2474
|
const onFrameLoad = (id) => {
|
|
1684
2475
|
setTimeout(() => {
|
|
@@ -1687,15 +2478,17 @@ window.__ModuleLoader__.load({
|
|
|
1687
2478
|
}, 1600)
|
|
1688
2479
|
}
|
|
1689
2480
|
|
|
1690
|
-
// 预览服务器只听电脑本机(127.0.0.1)。从手机(dsh-mobile)或别的电脑打开 DSH
|
|
1691
|
-
//
|
|
1692
|
-
|
|
2481
|
+
// 预览服务器只听电脑本机(127.0.0.1)。从手机(dsh-mobile)或别的电脑打开 DSH 网页时,这个地址指的是它们自己:
|
|
2482
|
+
// 本地文件的预览打包传过来(上面);开发服务器的没法打包,框里会一直空白,说清楚
|
|
2483
|
+
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])) // 画布缩小了,字别跟着变小
|
|
2484
|
+
if (away(src) && !rem) return tell('away')
|
|
2485
|
+
if (rem && failed && !cur) return tell('remoteFail')
|
|
1693
2486
|
return h('div', { className: 'ddp-scr' + (props.dark ? ' dk' : '') },
|
|
1694
2487
|
...frames.map((f) => h('iframe', {
|
|
1695
2488
|
key: f.id,
|
|
1696
2489
|
ref: (el) => { if (el) wins.current.set(f.id, el); else wins.current.delete(f.id) },
|
|
1697
2490
|
className: 'ddp-frame' + (f.id === cur ? ' cur' : f.id < cur ? ' old' : ''),
|
|
1698
|
-
src: f.src,
|
|
2491
|
+
...(typeof f.doc === 'string' ? { srcDoc: f.doc, sandbox: 'allow-scripts allow-forms', referrerPolicy: 'no-referrer' } : { src: f.src }),
|
|
1699
2492
|
title: props.label || '',
|
|
1700
2493
|
tabIndex: f.id === cur ? 0 : -1,
|
|
1701
2494
|
'aria-hidden': f.id === cur ? undefined : true,
|
|
@@ -1729,7 +2522,7 @@ window.__ModuleLoader__.load({
|
|
|
1729
2522
|
}
|
|
1730
2523
|
|
|
1731
2524
|
/** 设计规范面板:按组列出变量,点一个放进输入框 */
|
|
1732
|
-
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onClose }) {
|
|
2525
|
+
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onSystems, onClose }) {
|
|
1733
2526
|
const groups = {}
|
|
1734
2527
|
for (const tok of tokens) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
1735
2528
|
const px = (v) => Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
|
|
@@ -1755,7 +2548,267 @@ window.__ModuleLoader__.load({
|
|
|
1755
2548
|
h('span', { className: g === 'shadow' ? 'pv lt' : 'pv' }, preview(g, tok.value)),
|
|
1756
2549
|
h('span', { className: 'tx' }, h('span', { className: 'nm' }, tok.name), h('span', { className: 'vl' }, tok.value))))))
|
|
1757
2550
|
: h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onAsk }, t('tokensAsk'))))),
|
|
1758
|
-
onRef ? h('footer', null,
|
|
2551
|
+
onRef || onStyles ? h('footer', null,
|
|
2552
|
+
onStyles ? h('button', { onClick: onStyles }, h(I.sparkle), h('span', null, t('stylesOpen'))) : null,
|
|
2553
|
+
onSystems ? h('button', { onClick: onSystems }, h(I.library), h('span', null, t('sysOpen'))) : null,
|
|
2554
|
+
onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('refPick'))) : null) : null)
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
/** 设计体检的结果:按屏列出问题(和页面上的编号一样),点一条在页面上滚过去、那个框闪一下 */
|
|
2558
|
+
function CheckPanel({ t, ck, onFocus, onFix, onAgain, onClose }) {
|
|
2559
|
+
const list = ck.list || []
|
|
2560
|
+
const n = list.reduce((m, x) => m + x.issues.length, 0)
|
|
2561
|
+
const multi = list.length > 1
|
|
2562
|
+
return h('div', { className: 'ddp-tokens ddp-check', onPointerDown: (e) => e.stopPropagation() },
|
|
2563
|
+
h('header', null, h(I.stetho), h('b', null, t('check')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
|
|
2564
|
+
h('p', { className: 'tip' }, ck.running ? t('checking') : n ? t('checkSum', { s: list.filter((x) => x.issues.length).length, n }) : t('checkNote')),
|
|
2565
|
+
h('div', { className: 'bd' },
|
|
2566
|
+
...list.filter((sc) => sc.issues.length || sc.error).map((sc) => h('div', { key: sc.key },
|
|
2567
|
+
multi ? h('h4', null, sc.label) : null,
|
|
2568
|
+
sc.error ? h('div', { className: 'none' }, t('checkFail', { msg: sc.error })) : null,
|
|
2569
|
+
...sc.issues.map((it, i) => h('button', { key: i, className: 'ddp-tok ddp-ck', title: it.sel || '', onClick: () => onFocus(sc.key, i) },
|
|
2570
|
+
h('span', { className: 'no' + (it.sev > 1 ? ' hi' : '') }, String(i + 1)),
|
|
2571
|
+
h('span', { className: 'tx' },
|
|
2572
|
+
h('span', { className: 'nm' }, t('ck_' + it.type) + (it.text ? ` · ${it.text}` : '')),
|
|
2573
|
+
h('span', { className: 'vl' }, checkDetail(t, it))))))),
|
|
2574
|
+
!ck.running && !n ? h('div', { className: 'ok' }, h(I.check), h('span', null, t('checkNone'))) : null),
|
|
2575
|
+
n ? h('p', { className: 'tip' }, t('checkNote')) : null,
|
|
2576
|
+
h('footer', { className: 'row' },
|
|
2577
|
+
h('button', { onClick: onAgain, disabled: ck.running }, h(I.refresh), h('span', null, t('checkAgain'))),
|
|
2578
|
+
h('button', { className: 'go', onClick: onFix, disabled: ck.running || !n }, h(I.sparkle), h('span', null, t('checkFix')))))
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* 调样式面板:选中的元素的字号、字重、颜色、背景、圆角、内边距、间距。
|
|
2583
|
+
* 拖的时候页面上先看效果(还没写进文件);设计变量列成小块,点一个就用 var(--x)。
|
|
2584
|
+
* write:{ ok, tip } 能不能写回文件(开发服务器、模型在干活时不能)
|
|
2585
|
+
*/
|
|
2586
|
+
function StylePanel({ t, sty, tokens, write, onSet, onReset, onAsk, onSave, onClose }) {
|
|
2587
|
+
const head = h('header', null, h(I.sliders), h('b', null, t('style')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
|
|
2588
|
+
const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
|
|
2589
|
+
if (!sty) return box(h('p', { className: 'tip' }, t('styleNone')))
|
|
2590
|
+
const groups = {}
|
|
2591
|
+
for (const tok of tokens || []) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
2592
|
+
const cur = (k) => (k in sty.live ? sty.live[k] : sty.base[k])
|
|
2593
|
+
// 现在用的是哪个变量:改过的看改成了什么,没改的看样式表里怎么写的
|
|
2594
|
+
const varOf = (k) => { const m = VAR_ONLY.exec(String(cur(k) || '').trim()); return m ? m[1] : k in sty.live ? '' : (sty.vars || {})[k] || '' }
|
|
2595
|
+
const set = (props, v) => onSet(Object.fromEntries(props.map((p) => [p, v])))
|
|
2596
|
+
const chips = (kind, props) => (groups[kind] || []).slice(0, 10).map((tok) => h('button', {
|
|
2597
|
+
key: tok.name,
|
|
2598
|
+
className: 'chip' + (kind === 'color' ? ' c' : '') + (varOf(props[0]) === tok.name ? ' on' : ''),
|
|
2599
|
+
title: `${tok.name}: ${tok.value}`,
|
|
2600
|
+
onClick: () => set(props, `var(${tok.name})`),
|
|
2601
|
+
}, kind === 'color' ? h('i', { style: { background: tok.value } }) : tok.name.replace(/^--/, '')))
|
|
2602
|
+
const shown = (k, text) => (varOf(k) ? h('em', { className: 'v', title: t('styleVar', { name: varOf(k) }) }, varOf(k)) : h('em', null, text))
|
|
2603
|
+
const slider = (key, props, max, kind) => {
|
|
2604
|
+
const v = Math.round(pxOf(cur(props[0]), tokens) ?? 0)
|
|
2605
|
+
const c = kind ? chips(kind, props) : []
|
|
2606
|
+
return h('div', { className: 'row', key },
|
|
2607
|
+
h('label', null, h('span', null, t(key)), shown(props[0], `${v}px`)),
|
|
2608
|
+
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`) }),
|
|
2609
|
+
c.length ? h('div', { className: 'chips' }, ...c) : null)
|
|
2610
|
+
}
|
|
2611
|
+
const color = (key, prop, note) => h('div', { className: 'row', key },
|
|
2612
|
+
h('label', null, h('span', null, t(key)), shown(prop, showVal(prop, cur(prop), t))),
|
|
2613
|
+
h('div', { className: 'chips' },
|
|
2614
|
+
h('input', { type: 'color', value: colorHex(cur(prop), tokens), 'aria-label': t(key), onChange: (e) => set([prop], e.target.value) }),
|
|
2615
|
+
...chips('color', [prop])),
|
|
2616
|
+
note ? h('div', { className: 'note' }, note) : null)
|
|
2617
|
+
const w = Math.min(700, Math.max(400, Math.round(parseInt(cur('font-weight'), 10) / 100) * 100 || 400))
|
|
2618
|
+
const n = Object.keys(sty.live).length
|
|
2619
|
+
return box(
|
|
2620
|
+
h('p', { className: 'who', title: sty.selector }, h('code', null, sty.name), sty.text ? ` · ${sty.text}` : ''),
|
|
2621
|
+
h('div', { className: 'bd' },
|
|
2622
|
+
slider('styleFont', ['font-size'], 72, 'size'),
|
|
2623
|
+
h('div', { className: 'row', key: 'w' },
|
|
2624
|
+
h('label', null, h('span', null, t('styleWeight')), h('em', null, String(cur('font-weight')))),
|
|
2625
|
+
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))))),
|
|
2626
|
+
color('styleColor', 'color'),
|
|
2627
|
+
color('styleBg', 'background', sty.gradient && !('background' in sty.live) ? t('styleGradient') : ''),
|
|
2628
|
+
slider('styleRadius', ['border-radius'], 40, 'radius'),
|
|
2629
|
+
slider('stylePadY', ['padding-top', 'padding-bottom'], 48, 'space'),
|
|
2630
|
+
slider('stylePadX', ['padding-left', 'padding-right'], 48, 'space'),
|
|
2631
|
+
sty.layout ? slider('styleGap', ['gap'], 48, 'space') : null),
|
|
2632
|
+
n ? h('div', { className: 'cnt' }, t('styleN', { n: styleChanges(t, sty).length })) : null,
|
|
2633
|
+
h('footer', { className: 'row' },
|
|
2634
|
+
h('button', { onClick: onReset, disabled: !n }, h('span', null, t('styleReset'))),
|
|
2635
|
+
h('button', { onClick: onAsk, disabled: !n, title: t('styleAskTip') }, h('span', null, t('styleAsk'))),
|
|
2636
|
+
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
/** 一排风格卡:每张是个小样(底色、标题字、卡片、主色按钮),点了交给 onPick */
|
|
2640
|
+
function StyleCards({ t, onPick }) {
|
|
2641
|
+
const en = t('lang') === 'en'
|
|
2642
|
+
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) },
|
|
2643
|
+
h('span', { className: 'pv', style: { background: x.c.bg, backgroundImage: x.bgImage || 'none', color: x.c.text } },
|
|
2644
|
+
h('b', { style: { fontFamily: x.head, fontWeight: x.w } }, en ? 'Aa' : '字'),
|
|
2645
|
+
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' } }),
|
|
2646
|
+
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,
|
|
2647
|
+
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}` } },
|
|
2648
|
+
h('i', { className: 'ln', style: { background: x.c.text } }), h('i', { className: 'ln s', style: { background: x.c.muted } }))),
|
|
2649
|
+
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/** 换风格面板(设计已经打开时):点一张卡让模型整套换成那个风格 */
|
|
2653
|
+
function StylesPanel({ t, onPick, onClose }) {
|
|
2654
|
+
return h('div', { className: 'ddp-tokens', onPointerDown: (e) => e.stopPropagation() },
|
|
2655
|
+
h('header', null, h(I.palette), h('b', null, t('stylesTitle')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
|
|
2656
|
+
h('p', { className: 'tip' }, t('stylesSwap')),
|
|
2657
|
+
h('div', { className: 'bd' }, h(StyleCards, { t, onPick })))
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// ── 调节滑块、品牌设计系统、转成代码 ─────────────────────────────────────
|
|
2661
|
+
|
|
2662
|
+
/** 滑块的值 → 写到页面上的样子(和服务端 tweaks.js 的 cssValue 一样) */
|
|
2663
|
+
const tweakCss = (k, v) => (k.kind === 'range' ? `${v}${k.unit || ''}` : String(v))
|
|
2664
|
+
const tweakShown = (k, v) => (k.kind === 'choice' ? ((k.options || []).find((o) => o.value === String(v)) || { label: v }).label : tweakCss(k, v))
|
|
2665
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
2666
|
+
|
|
2667
|
+
/** 拖过的滑块(id → 值)→ 发给页面的 { vars, attrs };没拖过的不发(页面用文件里的) */
|
|
2668
|
+
function tweakMsg(list, live) {
|
|
2669
|
+
const vars = {}
|
|
2670
|
+
const attrs = {}
|
|
2671
|
+
for (const k of list) {
|
|
2672
|
+
if (!hasOwn(live, k.id)) continue
|
|
2673
|
+
if (k.var) vars[k.var] = tweakCss(k, live[k.id])
|
|
2674
|
+
else if (k.attr) attrs[k.attr] = String(live[k.id])
|
|
2675
|
+
}
|
|
2676
|
+
return { vars, attrs }
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/** 拖过的滑块 → 交给模型的一段字 */
|
|
2680
|
+
function formatTweaks(t, list, live) {
|
|
2681
|
+
const rows = list.filter((k) => hasOwn(live, k.id)).map((k) => `- ${k.label}:${tweakShown(k, k.value)} → ${tweakShown(k, live[k.id])}(${k.var || k.attr})`)
|
|
2682
|
+
return t('twAskHead') + '\n' + rows.join('\n') + '\n'
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/** 调节滑块面板:模型给这套设计做的几个维度(tweaks.json),拖的时候所有屏一起变,定下来写回文件或交给模型 */
|
|
2686
|
+
function TweaksPanel({ t, tw, live, write, onSet, onReset, onAsk, onSave, onMake, onClose }) {
|
|
2687
|
+
const head = h('header', null, h(I.dial), h('b', null, t('tweaks')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
|
|
2688
|
+
const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style ddp-tw', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
|
|
2689
|
+
const list = (tw && tw.list) || []
|
|
2690
|
+
const errs = (tw && tw.errors) || []
|
|
2691
|
+
if (!list.length) {
|
|
2692
|
+
return box(h('div', { className: 'bd' }, h('div', { className: 'none' },
|
|
2693
|
+
errs.length ? errs.map((e, i) => h('div', { key: i }, e)) : t('twEmpty'),
|
|
2694
|
+
h('div', null, h('button', { className: 'ddp-go', onClick: () => onMake(errs) }, t(errs.length ? 'twFix' : 'twMake'))))))
|
|
2695
|
+
}
|
|
2696
|
+
const cur = (k) => (hasOwn(live, k.id) ? live[k.id] : k.value)
|
|
2697
|
+
const row = (k) => {
|
|
2698
|
+
const v = cur(k)
|
|
2699
|
+
const lab = h('label', null, h('span', { title: k.var || k.attr }, k.label), h('em', { className: hasOwn(live, k.id) ? 'v' : '' }, tweakShown(k, v)))
|
|
2700
|
+
const note = k.note ? h('div', { className: 'note' }, k.note) : null
|
|
2701
|
+
if (k.kind === 'range') {
|
|
2702
|
+
return h('div', { className: 'row', key: k.id }, lab,
|
|
2703
|
+
h('input', { type: 'range', min: k.min, max: k.max, step: k.step, value: v, 'aria-label': k.label, onChange: (e) => onSet(k, Number(e.target.value)) }), note)
|
|
2704
|
+
}
|
|
2705
|
+
if (k.kind === 'color') {
|
|
2706
|
+
return h('div', { className: 'row', key: k.id }, lab,
|
|
2707
|
+
h('div', { className: 'chips' }, h('input', { type: 'color', value: /^#[0-9a-f]{6}$/i.test(v) ? v : '#000000', 'aria-label': k.label, onChange: (e) => onSet(k, e.target.value) })), note)
|
|
2708
|
+
}
|
|
2709
|
+
return h('div', { className: 'row', key: k.id }, lab,
|
|
2710
|
+
h('div', { className: 'seg' }, ...(k.options || []).map((o) => h('button', { key: o.value, className: String(v) === o.value ? 'on' : '', onClick: () => onSet(k, o.value) }, o.label))), note)
|
|
2711
|
+
}
|
|
2712
|
+
const n = list.filter((k) => hasOwn(live, k.id)).length
|
|
2713
|
+
return box(
|
|
2714
|
+
h('p', { className: 'tip' }, t('twTip')),
|
|
2715
|
+
h('div', { className: 'bd' }, ...list.map(row), errs.length ? h('div', { className: 'note', key: 'err' }, errs.join(';')) : null),
|
|
2716
|
+
h('footer', { className: 'row' },
|
|
2717
|
+
h('button', { onClick: onReset, disabled: !n }, h('span', null, t('styleReset'))),
|
|
2718
|
+
h('button', { onClick: onAsk, disabled: !n, title: t('twAskTip') }, h('span', null, t('styleAsk'))),
|
|
2719
|
+
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
async function getJson(path) {
|
|
2723
|
+
const r = await fetch(API + path, { credentials: 'same-origin' })
|
|
2724
|
+
const data = await r.json().catch(() => ({}))
|
|
2725
|
+
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`)
|
|
2726
|
+
return data
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/** 品牌设计系统库:从用户的网站 / 项目提取的几套,默认那套以后做新设计都照它来。rev 变了(模型干完一轮)重新列 */
|
|
2730
|
+
function SystemsPanel({ t, rev, hasDesign, onExtract, onApply, onSay, onClose }) {
|
|
2731
|
+
const [data, setData] = useState(null)
|
|
2732
|
+
const [err, setErr] = useState('')
|
|
2733
|
+
const [src, setSrc] = useState('')
|
|
2734
|
+
useEffect(() => {
|
|
2735
|
+
let alive = true
|
|
2736
|
+
getJson('systems').then((d) => { if (alive) { setData(d); setErr('') } }, (e) => { if (alive) setErr(e.message) })
|
|
2737
|
+
return () => { alive = false }
|
|
2738
|
+
}, [rev])
|
|
2739
|
+
const setDef = (x, on) => post('systems', { id: x.id, default: on }).then((d) => { setData(d); onSay(on ? t('sysSetOk', { name: x.name }) : t('sysUnsetOk')) }, (e) => setErr(e.message))
|
|
2740
|
+
const list = (data && data.systems) || []
|
|
2741
|
+
const go = (e) => {
|
|
2742
|
+
e.preventDefault()
|
|
2743
|
+
const v = src.trim()
|
|
2744
|
+
if (!v) return
|
|
2745
|
+
onExtract(v)
|
|
2746
|
+
setSrc('')
|
|
2747
|
+
}
|
|
2748
|
+
return h('div', { className: 'ddp-tokens ddp-sys', onPointerDown: (e) => e.stopPropagation() },
|
|
2749
|
+
h('header', null, h(I.library), h('b', null, t('systems')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close))),
|
|
2750
|
+
h('p', { className: 'tip' }, t('sysTip')),
|
|
2751
|
+
h('div', { className: 'bd' },
|
|
2752
|
+
err ? h('div', { className: 'none' }, err) : null,
|
|
2753
|
+
data && !list.length ? h('div', { className: 'none' }, t('sysEmpty')) : null,
|
|
2754
|
+
...list.map((x) => h('div', { key: x.id, className: 'ddp-sysc' + (x.default ? ' def' : '') },
|
|
2755
|
+
x.preview ? h('img', { src: `${API}systhumb?id=${encodeURIComponent(x.id)}&v=${x.createdAt || 0}`, alt: '', loading: 'lazy' }) : null,
|
|
2756
|
+
h('div', { className: 'nm' }, h('b', { title: x.dir }, x.name), x.default ? h('span', null, t('sysDefault')) : null),
|
|
2757
|
+
h('div', { className: 'sw' }, ...Object.entries(x.colors || {}).slice(0, 7).map(([k, v]) => h('i', { key: k, title: `${k} ${v}`, style: { background: v } })), x.font ? h('em', null, x.font) : null),
|
|
2758
|
+
x.sources && x.sources.length ? h('small', { title: x.sources.join('\n') }, x.sources[0]) : null,
|
|
2759
|
+
h('div', { className: 'acts' },
|
|
2760
|
+
h('button', { onClick: () => setDef(x, !x.default) }, x.default ? t('sysUnset') : t('sysSet')),
|
|
2761
|
+
hasDesign ? h('button', { onClick: () => onApply(x) }, t('sysApply')) : null)))),
|
|
2762
|
+
h('footer', null,
|
|
2763
|
+
h('form', { onSubmit: go },
|
|
2764
|
+
h('input', { value: src, placeholder: t('sysPh'), 'aria-label': t('sysPh'), onChange: (e) => setSrc(e.target.value) }),
|
|
2765
|
+
h('button', { type: 'submit', disabled: !src.trim() }, t('sysExtract')))))
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
const FW_LIST = ['react', 'vue', 'next', 'html']
|
|
2769
|
+
const FW_LABEL = { react: 'React + Vite', vue: 'Vue 3 + Vite', next: 'Next.js' }
|
|
2770
|
+
|
|
2771
|
+
/** 转成代码:用什么做、放哪、转几屏;点了把一段话放进输入框,模型调 handoff_design 照交接包做,每屏做完 compare_design */
|
|
2772
|
+
function HandoffPanel({ t, sid, root, fileTarget, count, cur, onGo, onClose }) {
|
|
2773
|
+
const [info, setInfo] = useState(null)
|
|
2774
|
+
const [fw, setFw] = useState('')
|
|
2775
|
+
const [dest, setDest] = useState('')
|
|
2776
|
+
const [one, setOne] = useState(false)
|
|
2777
|
+
// 换了会话或设计目录就重新问:先清掉旧的(别把上一套设计的目标发出去)
|
|
2778
|
+
useEffect(() => {
|
|
2779
|
+
let alive = true
|
|
2780
|
+
setInfo(null)
|
|
2781
|
+
setDest('')
|
|
2782
|
+
if (!root) return () => { alive = false }
|
|
2783
|
+
getJson(`project?session=${encodeURIComponent(sid)}`).then((d) => {
|
|
2784
|
+
if (!alive) return
|
|
2785
|
+
setInfo(d)
|
|
2786
|
+
setFw(d.project && d.project.pkg && d.project.framework ? 'same' : 'react')
|
|
2787
|
+
setDest(d.root === root ? d.dest || '' : '')
|
|
2788
|
+
}, () => { if (alive) setInfo({}) })
|
|
2789
|
+
return () => { alive = false }
|
|
2790
|
+
}, [sid, root])
|
|
2791
|
+
const head = h('header', null, h(I.code), h('b', null, t('handoff')), h('button', { className: 'ddp-btn', title: '×', onClick: onClose }, h(I.close)))
|
|
2792
|
+
const box = (...kids) => h('div', { className: 'ddp-tokens ddp-style ddp-ho', onPointerDown: (e) => e.stopPropagation() }, head, ...kids)
|
|
2793
|
+
if (!fileTarget) return box(h('div', { className: 'bd' }, h('div', { className: 'none' }, t('hoProxy'))))
|
|
2794
|
+
if (!info) return box(h('p', { className: 'tip' }, t('hoLoading')))
|
|
2795
|
+
const p = info.project || {}
|
|
2796
|
+
const same = p.pkg && p.framework
|
|
2797
|
+
const opts = [...(same ? [['same', t('hoSame', { label: p.label })]] : []), ...FW_LIST.map((k) => [k, k === 'html' ? t('hoHtml') : FW_LABEL[k]])]
|
|
2798
|
+
const label = (opts.find(([k]) => k === fw) || ['', ''])[1]
|
|
2799
|
+
return box(
|
|
2800
|
+
h('p', { className: 'tip' }, t('hoTip')),
|
|
2801
|
+
h('div', { className: 'bd' },
|
|
2802
|
+
h('div', { className: 'row' }, h('label', null, t('hoFw')),
|
|
2803
|
+
...opts.map(([k, l]) => h('button', { key: k, className: 'opt' + (fw === k ? ' on' : ''), onClick: () => setFw(k) }, h('i'), h('span', null, l)))),
|
|
2804
|
+
h('div', { className: 'row' }, h('label', null, t('hoDest')),
|
|
2805
|
+
h('input', { type: 'text', value: dest, spellCheck: false, 'aria-label': t('hoDest'), onChange: (e) => setDest(e.target.value) })),
|
|
2806
|
+
count > 1 && cur ? h('div', { className: 'row' }, h('label', null, t('hoScope')),
|
|
2807
|
+
h('div', { className: 'seg' },
|
|
2808
|
+
h('button', { className: one ? '' : 'on', onClick: () => setOne(false) }, t('hoAll', { n: count })),
|
|
2809
|
+
h('button', { className: one ? 'on' : '', onClick: () => setOne(true) }, t('hoOne')))) : null),
|
|
2810
|
+
h('footer', null,
|
|
2811
|
+
h('button', { className: 'go', disabled: !fw || !dest.trim(), onClick: () => onGo({ fw, label, dest: dest.trim(), only: one && count > 1 ? cur : '' }) }, h(I.code), h('span', null, t('hoGo')))))
|
|
1759
2812
|
}
|
|
1760
2813
|
|
|
1761
2814
|
/** 菜单开着时:点外面、按 Esc 关掉 */
|
|
@@ -1782,6 +2835,7 @@ window.__ModuleLoader__.load({
|
|
|
1782
2835
|
items.push(['raw', I.image, t('exRaw')])
|
|
1783
2836
|
if (kind !== 'poster' && kind !== 'slides') items.push(['long', I.long, t('exLong')]) // 海报、幻灯片画幅是定的,没有「整页」
|
|
1784
2837
|
if (count > 1) items.push(['show', I.canvas, t('exShow')])
|
|
2838
|
+
items.push(['pdf', I.pdf, t('exPdf')], ['pptx', I.deck, t('exPptx')])
|
|
1785
2839
|
return h('div', { className: 'ddp-kind', ref },
|
|
1786
2840
|
h('button', { className: 'ddp-btn' + (open ? ' sel' : ''), title: t('exportT'), disabled, onClick: () => setOpen(!open) }, h(I.download)),
|
|
1787
2841
|
open ? h('div', { className: 'ddp-menu', role: 'menu' },
|
|
@@ -1912,6 +2966,29 @@ window.__ModuleLoader__.load({
|
|
|
1912
2966
|
}
|
|
1913
2967
|
}, [layoutKey])
|
|
1914
2968
|
|
|
2969
|
+
// 让某一屏露出来(体检里点了一条):整屏不在看得见的地方就把镜头挪过去;右边 inset 像素被面板挡着
|
|
2970
|
+
const rv = props.reveal
|
|
2971
|
+
useEffect(() => {
|
|
2972
|
+
const c = camRef.current
|
|
2973
|
+
if (!rv || !L || !c) return
|
|
2974
|
+
const i = screens.findIndex((x) => x.key === rv.key)
|
|
2975
|
+
if (i < 0) return
|
|
2976
|
+
const [cw, ch] = boxes ? boxes[i] : [dw, dh]
|
|
2977
|
+
const vw = Math.max(120, box.w - (props.inset || 0))
|
|
2978
|
+
const x0 = c.x + L.pos[i].x * c.z
|
|
2979
|
+
const y0 = c.y + L.pos[i].y * c.z
|
|
2980
|
+
const w = cw * c.z
|
|
2981
|
+
const hh = ch * c.z
|
|
2982
|
+
let x = c.x
|
|
2983
|
+
let y = c.y
|
|
2984
|
+
if (x0 < 8 || x0 + w > vw - 8) x += (w > vw - 16 ? 8 : (vw - w) / 2) - x0
|
|
2985
|
+
if (y0 < LAB || y0 + hh > box.h - 8) y += (hh > box.h - LAB - 8 ? LAB : (box.h - hh) / 2) - y0
|
|
2986
|
+
if (x === c.x && y === c.y) return
|
|
2987
|
+
auto.current = false
|
|
2988
|
+
setAnim(true)
|
|
2989
|
+
setCam({ ...c, x, y })
|
|
2990
|
+
}, [rv && rv.n])
|
|
2991
|
+
|
|
1915
2992
|
const zoomAt = (factor, px, py, smooth) => {
|
|
1916
2993
|
const c = camRef.current
|
|
1917
2994
|
if (!c) return
|
|
@@ -2049,6 +3126,8 @@ window.__ModuleLoader__.load({
|
|
|
2049
3126
|
useEffect(() => { const tm = setTimeout(() => setWaited(true), 2500); return () => clearTimeout(tm) }, [])
|
|
2050
3127
|
const [picking, setPicking] = useState(false)
|
|
2051
3128
|
const [textEdit, setTextEdit] = useState(false) // 改字:点页面里的文字直接改
|
|
3129
|
+
const [styleEdit, setStyleEdit] = useState(false) // 调样式:点元素、拖滑块,写回它的 style 属性
|
|
3130
|
+
const [sty, setSty] = useState(null) // 调样式选中的元素:{ key, url, name, selector, text, loc, base, vars, gradient, layout, live }
|
|
2052
3131
|
const [editing, setEditing] = useState(false)
|
|
2053
3132
|
const [menu, setMenu] = useState(false)
|
|
2054
3133
|
const [toast, setToast] = useState('')
|
|
@@ -2057,6 +3136,7 @@ window.__ModuleLoader__.load({
|
|
|
2057
3136
|
const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
|
|
2058
3137
|
const [sel, setSel] = useState('')
|
|
2059
3138
|
const [panel, setPanel] = useState(false)
|
|
3139
|
+
const [styles, setStyles] = useState(false) // 换风格面板
|
|
2060
3140
|
const [dropping, setDropping] = useState(false) // 正拖着图片进来
|
|
2061
3141
|
const refInput = useRef(null)
|
|
2062
3142
|
const [exMenu, setExMenu] = useState(false)
|
|
@@ -2070,7 +3150,12 @@ window.__ModuleLoader__.load({
|
|
|
2070
3150
|
const [dark, setDark] = useState(false)
|
|
2071
3151
|
const [phone, setPhone] = useState(false) // 手机上看的面板
|
|
2072
3152
|
const [multi, setMulti] = useState(false) // 多尺寸并排
|
|
3153
|
+
const [ck, setCk] = useState(null) // 设计体检:null = 面板关着;{ running, list: [{ key, label, url, issues, error }] }
|
|
3154
|
+
const [ckCount, setCkCount] = useState(0) // 最近一次体检(模型干完活会悄悄查一次)有几个问题:面板关着时标在按钮上
|
|
3155
|
+
const [reveal, setReveal] = useState(null) // 画布上要露出来的那一屏 { key, n }(体检里点了一条)
|
|
2073
3156
|
const [demo, setDemo] = useState(null) // 演示:{ stack: [屏…], layers: [{ id, key, dir, shown }] }
|
|
3157
|
+
const [pane, setPane] = useState('') // 右边的面板:tweaks 调节滑块、systems 品牌设计系统、handoff 转成代码
|
|
3158
|
+
const [tw, setTw] = useState({}) // 拖过的滑块:id → 值(还没写进文件)
|
|
2074
3159
|
const apis = useRef(new Map()) // 屏 → 截图用的接口
|
|
2075
3160
|
const cmpRef = useRef(null)
|
|
2076
3161
|
const [box, setBox] = useState({ w: 0, h: 0 })
|
|
@@ -2130,6 +3215,22 @@ window.__ModuleLoader__.load({
|
|
|
2130
3215
|
const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
|
|
2131
3216
|
const anyDark = Object.keys(info).some((k) => k.startsWith(site + '|') && info[k].dark)
|
|
2132
3217
|
const fileTarget = !!(st && st.target && st.target.kind === 'file')
|
|
3218
|
+
// 远程看(手机走外网等):本地文件的预览由服务端打包传过来(见 Screen),目录里改了哪些文件也一起带过去
|
|
3219
|
+
const remote = fileTarget && sid ? { sid, at: st.changedAt || 0, files: st.changedFiles || [], changes: st.changes || [] } : null
|
|
3220
|
+
|
|
3221
|
+
// 调节滑块:tweaks.json 改了(写回了、模型改了)就把和文件一样的值放掉;换了设计全放掉
|
|
3222
|
+
const tweaks = (fileTarget && st.tweaks) || null
|
|
3223
|
+
const tlist = (tweaks && tweaks.list) || []
|
|
3224
|
+
const tsig = JSON.stringify(tlist.map((k) => [k.id, k.value]))
|
|
3225
|
+
useEffect(() => {
|
|
3226
|
+
setTw((c) => {
|
|
3227
|
+
const out = {}
|
|
3228
|
+
for (const [id, v] of Object.entries(c)) { const k = tlist.find((x) => x.id === id); if (k && String(k.value) !== String(v)) out[id] = v }
|
|
3229
|
+
return Object.keys(out).length === Object.keys(c).length ? c : out
|
|
3230
|
+
})
|
|
3231
|
+
}, [tsig])
|
|
3232
|
+
useEffect(() => { setTw({}) }, [site])
|
|
3233
|
+
const tweak = Object.keys(tw).length ? tweakMsg(tlist, tw) : null
|
|
2133
3234
|
|
|
2134
3235
|
// 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
|
|
2135
3236
|
const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
|
|
@@ -2218,7 +3319,7 @@ window.__ModuleLoader__.load({
|
|
|
2218
3319
|
return () => ro.disconnect()
|
|
2219
3320
|
}, [!!url])
|
|
2220
3321
|
|
|
2221
|
-
useEffect(() => { setPicking(false); setTextEdit(false); setSel(''); setVer(null); setCmp(false) }, [site])
|
|
3322
|
+
useEffect(() => { setPicking(false); setTextEdit(false); setStyleEdit(false); setSel(''); setVer(null); setCmp(false) }, [site])
|
|
2222
3323
|
|
|
2223
3324
|
// 版本列表:面板开着时拉一次,存了新版本(vrev 变了)再拉
|
|
2224
3325
|
const vrev = st ? st.vrev : 0
|
|
@@ -2256,6 +3357,7 @@ window.__ModuleLoader__.load({
|
|
|
2256
3357
|
const togglePick = () => {
|
|
2257
3358
|
if (picking) { setPicking(false); return }
|
|
2258
3359
|
setTextEdit(false)
|
|
3360
|
+
setStyleEdit(false)
|
|
2259
3361
|
setPicking(true)
|
|
2260
3362
|
if (!canvas) {
|
|
2261
3363
|
// 单页:把焦点给页面,Esc 在页面里也管用
|
|
@@ -2267,20 +3369,76 @@ window.__ModuleLoader__.load({
|
|
|
2267
3369
|
const toggleText = () => {
|
|
2268
3370
|
if (textEdit) { setTextEdit(false); return }
|
|
2269
3371
|
setPicking(false)
|
|
3372
|
+
setStyleEdit(false)
|
|
2270
3373
|
setTextEdit(true)
|
|
2271
3374
|
}
|
|
2272
3375
|
|
|
2273
|
-
const askText = (sc, d) => insert(t('textAsk', { before: d.before, after: d.after, where: whereOf(sc.url, stRef.current) }), 'insertedSend')
|
|
3376
|
+
const askText = (sc, d) => insert(t('textAsk', { before: d.before, after: d.after, where: whereOf(pageOf(sc, d.href).url, stRef.current) }), 'insertedSend')
|
|
2274
3377
|
|
|
2275
3378
|
const onEdited = (sc, d) => {
|
|
2276
3379
|
if (!fileTarget) return askText(sc, d)
|
|
2277
|
-
post('edit', { session: sid, key: sc.key, before: d.before, after: d.after, nth: d.nth })
|
|
3380
|
+
post('edit', { session: sid, key: pageOf(sc, d.href).key, before: d.before, after: d.after, nth: d.nth })
|
|
2278
3381
|
.then((r) => (r && r.ok ? say(t('textSaved', { file: r.file })) : askText(sc, d)), () => askText(sc, d))
|
|
2279
3382
|
}
|
|
2280
3383
|
|
|
2281
3384
|
// 模型在干活时不能改字(它可能正整份重写文件,改了会被盖掉)
|
|
2282
3385
|
useEffect(() => { if (ver || demo || busy) setTextEdit(false) }, [!!ver, !!demo, busy])
|
|
2283
3386
|
|
|
3387
|
+
// ── 调样式:点一个元素,面板里拖滑块先在页面上看效果;写回文件 = 并进它在源码里的 style 属性 ──
|
|
3388
|
+
const toggleStyle = () => {
|
|
3389
|
+
if (styleEdit) { setStyleEdit(false); return }
|
|
3390
|
+
setPicking(false)
|
|
3391
|
+
setTextEdit(false)
|
|
3392
|
+
setPanel(false)
|
|
3393
|
+
setStyles(false)
|
|
3394
|
+
setHist(false)
|
|
3395
|
+
setPhone(false)
|
|
3396
|
+
closeCheck()
|
|
3397
|
+
setStyleEdit(true)
|
|
3398
|
+
}
|
|
3399
|
+
useEffect(() => { if (!styleEdit) setSty(null) }, [styleEdit])
|
|
3400
|
+
const onStylePick = (sc, p) => {
|
|
3401
|
+
if (canvas) setSel(sc.key)
|
|
3402
|
+
const pg = pageOf(sc, p.href)
|
|
3403
|
+
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: {} })
|
|
3404
|
+
}
|
|
3405
|
+
const setStyleProp = (patch) => setSty((c) => (c ? { ...c, live: { ...c.live, ...patch } } : c))
|
|
3406
|
+
// 交给模型:先问服务端那几条规则在源码第几行(本地文件才有;问不到就只写文件名)
|
|
3407
|
+
const askStyle = async () => {
|
|
3408
|
+
const s0 = sty
|
|
3409
|
+
if (!s0) return
|
|
3410
|
+
let located = null
|
|
3411
|
+
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()]
|
|
3412
|
+
if (fileTarget && items.length) {
|
|
3413
|
+
try {
|
|
3414
|
+
const r = await post('locate', { session: sid, items: items.map((x) => ({ href: x.href, sel: x.sel, n: x.n })) })
|
|
3415
|
+
located = new Map(items.map((x, i) => [`${x.href}|${x.sel}|${x.n}`, r && Array.isArray(r.items) ? r.items[i] : null]))
|
|
3416
|
+
} catch { /* 找不到行号就只写文件名 */ }
|
|
3417
|
+
}
|
|
3418
|
+
insert(formatStyleAsk(t, s0, stRef.current, located), 'insertedSend')
|
|
3419
|
+
}
|
|
3420
|
+
const saveStyle = () => {
|
|
3421
|
+
const s0 = sty
|
|
3422
|
+
if (!s0 || !Object.keys(s0.live).length) return
|
|
3423
|
+
if (!fileTarget) return askStyle()
|
|
3424
|
+
const saved = s0.live
|
|
3425
|
+
post('style', { session: sid, key: s0.key, loc: s0.loc, css: saved, name: s0.name }).then((r) => {
|
|
3426
|
+
if (r && r.ok) {
|
|
3427
|
+
say(t('styleSaved', { file: r.file }))
|
|
3428
|
+
// 写进去的算「原样」了;写的时候又拖过的留着
|
|
3429
|
+
setSty((c) => (c && c.key === s0.key && c.selector === s0.selector
|
|
3430
|
+
? { ...c, base: { ...c.base, ...saved }, live: Object.fromEntries(Object.entries(c.live).filter(([k, v]) => saved[k] !== v)) } : c))
|
|
3431
|
+
} else if (r && r.reason === 'busy') say(t('styleBusyTip'))
|
|
3432
|
+
else { say(t('styleMiss')); askStyle() }
|
|
3433
|
+
}, () => askStyle())
|
|
3434
|
+
}
|
|
3435
|
+
useEffect(() => {
|
|
3436
|
+
if (!styleEdit) return undefined
|
|
3437
|
+
const onKey = (e) => { if (e.key === 'Escape') setStyleEdit(false) }
|
|
3438
|
+
window.addEventListener('keydown', onKey)
|
|
3439
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
3440
|
+
}, [styleEdit])
|
|
3441
|
+
|
|
2284
3442
|
useEffect(() => {
|
|
2285
3443
|
if (!dropping) return undefined
|
|
2286
3444
|
const off = () => setDropping(false)
|
|
@@ -2367,13 +3525,19 @@ window.__ModuleLoader__.load({
|
|
|
2367
3525
|
}
|
|
2368
3526
|
|
|
2369
3527
|
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc) },
|
|
2370
|
-
h(Screen, { t,
|
|
3528
|
+
h(Screen, { t, remote,
|
|
2371
3529
|
src: sc.url,
|
|
2372
3530
|
label: labelOf(sc),
|
|
2373
3531
|
busy: busy && !sc.v,
|
|
2374
3532
|
born: !sc.v && isBorn(sc.key),
|
|
2375
3533
|
picking: picking && !sc.v,
|
|
2376
3534
|
textEdit: textEdit && !sc.v,
|
|
3535
|
+
styleEdit: styleEdit && !sc.v,
|
|
3536
|
+
styleSel: sty && sty.key === sc.key ? sty.selector : '',
|
|
3537
|
+
styleCss: sty && sty.key === sc.key ? sty.live : null,
|
|
3538
|
+
tweak: sc.v ? null : tweak,
|
|
3539
|
+
onStylePick: (p) => onStylePick(sc, p),
|
|
3540
|
+
onStyleCancel: () => setStyleEdit(false),
|
|
2377
3541
|
dark,
|
|
2378
3542
|
apis: apis.current,
|
|
2379
3543
|
apiKey: ik(sc),
|
|
@@ -2391,16 +3555,23 @@ window.__ModuleLoader__.load({
|
|
|
2391
3555
|
}))
|
|
2392
3556
|
|
|
2393
3557
|
// ── 导出图片 ──
|
|
2394
|
-
|
|
3558
|
+
// 截一屏:图片和它的 CSS 尺寸(PDF / PPT 按这个定页面大小)
|
|
3559
|
+
// 那一屏的接口(截图、体检);刚载入的等它就绪
|
|
3560
|
+
const apiOf = async (sc) => {
|
|
2395
3561
|
let api = apis.current.get(ik(sc))
|
|
2396
3562
|
for (let i = 0; i < 40 && !(api && api.ready()); i++) {
|
|
2397
3563
|
await new Promise((r) => setTimeout(r, 150))
|
|
2398
3564
|
api = apis.current.get(ik(sc))
|
|
2399
3565
|
}
|
|
2400
3566
|
if (!api) throw new Error('not loaded')
|
|
3567
|
+
return api
|
|
3568
|
+
}
|
|
3569
|
+
const captureOf = async (sc, full) => {
|
|
3570
|
+
const api = await apiOf(sc)
|
|
2401
3571
|
const r = await api.capture({ full: !!full, scale: 2 })
|
|
2402
|
-
return loadImg(r.png)
|
|
3572
|
+
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [] }
|
|
2403
3573
|
}
|
|
3574
|
+
const shotOf = async (sc, full) => (await captureOf(sc, full)).img
|
|
2404
3575
|
|
|
2405
3576
|
const saveImage = async (name, blob) => {
|
|
2406
3577
|
const png = await blobB64(blob)
|
|
@@ -2416,6 +3587,31 @@ window.__ModuleLoader__.load({
|
|
|
2416
3587
|
try {
|
|
2417
3588
|
const prefix = ver ? `v${ver.n}-` : ''
|
|
2418
3589
|
const suffix = dark ? '-dark' : ''
|
|
3590
|
+
if (mode === 'pdf' || mode === 'pptx') {
|
|
3591
|
+
// 每屏一页。PDF 里网页、邮件放整页长图(页面多高就多高);PPT 一张幻灯片一个画面
|
|
3592
|
+
const long = mode === 'pdf' && (kind === 'web' || kind === 'email')
|
|
3593
|
+
const pages = []
|
|
3594
|
+
const keys = []
|
|
3595
|
+
for (const sc of shown) {
|
|
3596
|
+
try {
|
|
3597
|
+
const r = await captureOf(sc, long)
|
|
3598
|
+
pages.push({ jpg: await jpegB64(r.img), w: r.w, h: r.h, title: labelOf(sc), links: r.links })
|
|
3599
|
+
keys.push(sc.key)
|
|
3600
|
+
} catch (e) { console.warn('[dsh-design-studio] 截图失败', sc.key, e) }
|
|
3601
|
+
}
|
|
3602
|
+
if (!pages.length) throw new Error('no screens')
|
|
3603
|
+
// 原型里的跳转:路径换成第几页,PDF / PPT 里点了能跳过去
|
|
3604
|
+
for (const p of pages) {
|
|
3605
|
+
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)
|
|
3606
|
+
}
|
|
3607
|
+
const sp = splitTitles(pages.map((x) => x.title))
|
|
3608
|
+
const folder = st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : ''
|
|
3609
|
+
const title = sp.brand || folder || st.label || ''
|
|
3610
|
+
const r = await post('doc', { session: sid, format: mode, name: prefix + (title || 'design') + suffix, title, bg: hexOf(pickColors(tokens).bg), pages })
|
|
3611
|
+
const file = String(r.path || '').split(/[\\/]/).pop()
|
|
3612
|
+
say(t('exportedDoc', { name: file, n: pages.length }), { label: t('reveal'), run: () => post('reveal', { path: r.path }).catch(() => {}) })
|
|
3613
|
+
return
|
|
3614
|
+
}
|
|
2419
3615
|
if (mode === 'show') {
|
|
2420
3616
|
const shots = []
|
|
2421
3617
|
for (const sc of shown) {
|
|
@@ -2471,7 +3667,17 @@ window.__ModuleLoader__.load({
|
|
|
2471
3667
|
insert(t('variantText', { name: labelOf(sc), b, where: whereOf(sc.url, stRef.current) }), 'insertedSend')
|
|
2472
3668
|
}
|
|
2473
3669
|
|
|
3670
|
+
// 同一屏的几个版本(开局的三个方向 home-a / b / c,或「再出两版」的 login / login-b / login-c):挑一个
|
|
3671
|
+
const pickDirection = (sc) => {
|
|
3672
|
+
const g = takesOf(shown.map((x) => x.key), sc.key)
|
|
3673
|
+
if (!g) return
|
|
3674
|
+
const others = g.others.join('、')
|
|
3675
|
+
const where = whereOf(sc.url, stRef.current)
|
|
3676
|
+
insert(g.letter ? t('pickText', { name: labelOf(sc), where, others, base: g.stem }) : t('pickKeep', { name: labelOf(sc), where, others }), 'insertedSend')
|
|
3677
|
+
}
|
|
3678
|
+
|
|
2474
3679
|
const actions = (sc) => [
|
|
3680
|
+
!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
3681
|
h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download)),
|
|
2476
3682
|
sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle)),
|
|
2477
3683
|
h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single)),
|
|
@@ -2507,9 +3713,128 @@ window.__ModuleLoader__.load({
|
|
|
2507
3713
|
const hintTop = (picking || textEdit) && !canvas && !inMulti && !ver && !demo && kind !== 'web'
|
|
2508
3714
|
const flows = canvas && !inMulti && !picking && !textEdit ? flowsOf(shown, (sc) => (infoOf(ik(sc)) || {}).links, fileTarget) : []
|
|
2509
3715
|
|
|
3716
|
+
// ── 设计体检:画布上量所有屏,单页量这一屏;问题标在页面上,列在面板里 ──
|
|
3717
|
+
const closeCheck = () => {
|
|
3718
|
+
if (ck) for (const sc of ck.list || []) { const a = apis.current.get(ik(sc)); if (a) a.checkClear() }
|
|
3719
|
+
setCk(null)
|
|
3720
|
+
}
|
|
3721
|
+
const checkList = () => (canvas ? shown : [shown.find((x) => x.key === focus) || { key: focus, url, label: st.label }])
|
|
3722
|
+
// 几屏一起量(各量各的 iframe),一屏卡住不拖累别的
|
|
3723
|
+
const measure = (list, quiet) => Promise.all(list.map(async (sc) => {
|
|
3724
|
+
const row = { key: sc.key, v: sc.v, label: labelOf(sc), url: sc.url, issues: [] }
|
|
3725
|
+
try {
|
|
3726
|
+
const r = await (await apiOf(sc)).check(kind, quiet)
|
|
3727
|
+
row.issues = Array.isArray(r.issues) ? r.issues : []
|
|
3728
|
+
if (r.metrics && typeof r.metrics === 'object') row.metrics = r.metrics
|
|
3729
|
+
} catch (e) { row.error = e.message }
|
|
3730
|
+
return row
|
|
3731
|
+
}))
|
|
3732
|
+
const total = (list) => list.reduce((m, x) => m + x.issues.length, 0)
|
|
3733
|
+
const runCheck = async () => {
|
|
3734
|
+
setPanel(false); setHist(false); setPhone(false)
|
|
3735
|
+
setCk({ running: true, list: [] })
|
|
3736
|
+
const out = await measure(checkList(), false)
|
|
3737
|
+
setCk({ running: false, list: out })
|
|
3738
|
+
setCkCount(total(out))
|
|
3739
|
+
// 有问题的第一屏被面板挡着、在画布外面:挪过来
|
|
3740
|
+
const first = out.find((x) => x.issues.length)
|
|
3741
|
+
if (canvas && first) setReveal((r) => ({ key: first.key, n: ((r && r.n) || 0) + 1 }))
|
|
3742
|
+
return out
|
|
3743
|
+
}
|
|
3744
|
+
const focusIssue = (key, i) => {
|
|
3745
|
+
const sc = (ck && ck.list.find((x) => x.key === key)) || null
|
|
3746
|
+
if (!sc) return
|
|
3747
|
+
if (canvas) { setSel(key); setReveal((r) => ({ key, n: ((r && r.n) || 0) + 1 })) }
|
|
3748
|
+
const a = apis.current.get(ik(sc))
|
|
3749
|
+
if (a) a.checkFocus(i)
|
|
3750
|
+
}
|
|
3751
|
+
// 打开一套设计、模型又干完一轮(turns 变了):面板开着就再查一次(改没改好一眼看到);关着就悄悄量一下,问题数标在按钮上。
|
|
3752
|
+
// 模型干着活时不量;等一会儿再量:刚写完的屏还在刷新、播生成动画
|
|
3753
|
+
const autoCk = useRef(null)
|
|
3754
|
+
const ckKey = url && !busy ? `${site}|${(st && st.turns) || 0}` : ''
|
|
3755
|
+
const seenCk = useRef('')
|
|
3756
|
+
useEffect(() => {
|
|
3757
|
+
if (!ckKey || ckKey === seenCk.current) return undefined
|
|
3758
|
+
seenCk.current = ckKey
|
|
3759
|
+
const timer = setTimeout(async () => {
|
|
3760
|
+
const a = autoCk.current
|
|
3761
|
+
if (a.off) return
|
|
3762
|
+
if (a.ck && !a.ck.running) return a.run()
|
|
3763
|
+
if (a.ck) return
|
|
3764
|
+
const out = await a.measure(a.list(), true)
|
|
3765
|
+
if (!autoCk.current.ck) setCkCount(total(out))
|
|
3766
|
+
}, 2500)
|
|
3767
|
+
return () => clearTimeout(timer)
|
|
3768
|
+
}, [ckKey])
|
|
3769
|
+
useEffect(() => { setCkCount(0) }, [site])
|
|
3770
|
+
// 模型调了 check_design(轮询里出现新的 checkReq):量完把清单交回去;面板开着就顺手刷新面板,关着就只更新按钮上的数。
|
|
3771
|
+
// 稍等一下再量:模型刚写完的屏还在刷新
|
|
3772
|
+
const ckReq = (st && st.checkReq) || ''
|
|
3773
|
+
const doneReq = useRef('')
|
|
3774
|
+
useEffect(() => {
|
|
3775
|
+
if (!ckReq || ckReq === doneReq.current) return undefined
|
|
3776
|
+
doneReq.current = ckReq
|
|
3777
|
+
const timer = setTimeout(async () => {
|
|
3778
|
+
const a = autoCk.current
|
|
3779
|
+
let body
|
|
3780
|
+
if (a.off) body = { error: t('ckToolOff') }
|
|
3781
|
+
else {
|
|
3782
|
+
const out = a.ck && !a.ck.running ? await a.run() : await a.measure(a.list(), true)
|
|
3783
|
+
if (!autoCk.current.ck) setCkCount(total(out))
|
|
3784
|
+
// measured:真量到的屏数。一屏都没量到(这台设备上的预览不回话)时服务端先不收,等别的设备上的右侧栏
|
|
3785
|
+
body = { text: formatCheckTool(t, out, stRef.current), measured: out.filter((x) => !x.error).length }
|
|
3786
|
+
}
|
|
3787
|
+
post('check', { session: sid, id: ckReq, ...body }).catch(() => {})
|
|
3788
|
+
}, 1500)
|
|
3789
|
+
return () => clearTimeout(timer)
|
|
3790
|
+
}, [ckReq])
|
|
3791
|
+
const fixIssues = () => {
|
|
3792
|
+
if (!ck) return
|
|
3793
|
+
insert(formatCheck(t, ck.list, stRef.current), 'insertedSend')
|
|
3794
|
+
closeCheck()
|
|
3795
|
+
}
|
|
3796
|
+
const checkOff = !url || !!ver || !!demo || inMulti
|
|
3797
|
+
const styleOff = !url || !!ver || !!demo || inMulti || busy
|
|
3798
|
+
useEffect(() => { if (styleOff) setStyleEdit(false) }, [styleOff])
|
|
3799
|
+
autoCk.current = { ck, off: checkOff, run: runCheck, list: checkList, measure }
|
|
3800
|
+
useEffect(() => { if (checkOff && ck) closeCheck() }, [checkOff])
|
|
3801
|
+
|
|
2510
3802
|
const toggleDemo = () => (demo ? setDemo(null) : startDemo())
|
|
2511
|
-
const toggleHist = () => { setHist(!hist); setPanel(false); setPhone(false) }
|
|
2512
|
-
|
|
3803
|
+
const toggleHist = () => { setHist(!hist); setPanel(false); setStyles(false); setPhone(false); closeCheck(); setStyleEdit(false) }
|
|
3804
|
+
// 右边同一时间只开一个面板:别的开了,这几个关掉;开这几个时别的关掉
|
|
3805
|
+
const oldOpen = !!(panel || styles || hist || phone || ck || styleEdit)
|
|
3806
|
+
useEffect(() => { if (oldOpen) setPane('') }, [oldOpen])
|
|
3807
|
+
const openPane = (p) => {
|
|
3808
|
+
const next = pane === p ? '' : p
|
|
3809
|
+
if (next) { setPanel(false); setStyles(false); setHist(false); setPhone(false); closeCheck(); setStyleEdit(false) }
|
|
3810
|
+
setPane(next)
|
|
3811
|
+
}
|
|
3812
|
+
const setTweak = (k, v) => setTw((c) => {
|
|
3813
|
+
if (String(k.value) === String(v)) { const out = { ...c }; delete out[k.id]; return out }
|
|
3814
|
+
return { ...c, [k.id]: v }
|
|
3815
|
+
})
|
|
3816
|
+
const saveTweaks = async () => {
|
|
3817
|
+
try {
|
|
3818
|
+
// 带上拖的是哪套设计:请求路上会话换了设计,服务端就不写
|
|
3819
|
+
const r = await post('tweak', { session: sid, root: st && st.target ? st.target.root : '', values: tw })
|
|
3820
|
+
if (!r.ok) return say(r.reason === 'busy' ? t('styleBusyTip') : r.reason === 'moved' ? t('twMoved') : t('twNoWrite'))
|
|
3821
|
+
if (r.missed && r.missed.length) say(t('twMissed', { names: r.missed.map((x) => x.label).join(t('sep')) }))
|
|
3822
|
+
else showFlash(t('twSaved', { n: r.changed.length }))
|
|
3823
|
+
} catch (e) {
|
|
3824
|
+
say(t('failed', { msg: e.message }))
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
const doHandoff = ({ fw, label, dest, only }) => {
|
|
3828
|
+
insert(t('hoText', {
|
|
3829
|
+
fw: label,
|
|
3830
|
+
key: fw,
|
|
3831
|
+
dest,
|
|
3832
|
+
scope: only ? t('hoScopeOne', { name: only }) : t('hoAll', { n: screens.length || 1 }),
|
|
3833
|
+
only: only ? `,screens: ["${only}"]` : '',
|
|
3834
|
+
}), 'insertedSend')
|
|
3835
|
+
setPane('')
|
|
3836
|
+
}
|
|
3837
|
+
const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
|
|
2513
3838
|
const reload = () => setReloadSeq((x) => x + 1)
|
|
2514
3839
|
|
|
2515
3840
|
const bar = h('div', { className: 'ddp-bar' },
|
|
@@ -2521,6 +3846,7 @@ window.__ModuleLoader__.load({
|
|
|
2521
3846
|
h('button', { className: 'ddp-btn pick' + (picking ? ' on' : ''), title: t('pickTip'), disabled: !url || !!ver || !!demo, onClick: togglePick },
|
|
2522
3847
|
h(I.pick), h('span', null, picking ? t('picking') : t('pick'))),
|
|
2523
3848
|
h('button', { className: 'ddp-btn' + (textEdit ? ' on' : ''), title: t('textEdit'), disabled: !url || !!ver || !!demo || busy, onClick: toggleText }, h(I.text)),
|
|
3849
|
+
h('button', { className: 'ddp-btn' + (styleEdit ? ' on' : ''), title: t('styleEdit'), disabled: styleOff, onClick: toggleStyle }, h(I.sliders)),
|
|
2524
3850
|
h('span', { className: 'ddp-sep' }),
|
|
2525
3851
|
h('div', { className: 'ddp-seg' },
|
|
2526
3852
|
h('button', { className: 'ddp-btn' + (canvas && !inMulti ? ' sel' : ''), title: t('canvas'), onClick: () => { setMulti(false); change({ view: 'canvas' }) } }, h(I.canvas)),
|
|
@@ -2528,24 +3854,35 @@ window.__ModuleLoader__.load({
|
|
|
2528
3854
|
h('button', { className: 'ddp-btn' + (inMulti ? ' sel' : ''), title: sizeSet ? t('sizes') : t('sizesNo'), disabled: !sizeSet || !!ver, onClick: () => setMulti(!inMulti) }, h(I.sizes))),
|
|
2529
3855
|
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver, onClick: toggleDemo }, h(I.play)),
|
|
2530
3856
|
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)),
|
|
3857
|
+
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)),
|
|
3858
|
+
h('button', { className: 'ddp-btn o2' + (pane === 'tweaks' ? ' sel' : '') + (tlist.length && pane !== 'tweaks' ? ' has' : ''), title: t('tweaks'), disabled: !fileTarget || !!ver, onClick: () => openPane('tweaks') }, h(I.dial)),
|
|
3859
|
+
h('button', { className: 'ddp-btn o2' + (ck ? ' sel' : ''), title: ckCount && !ck ? t('checkCount', { n: ckCount }) : t('checkTip'), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
|
|
3860
|
+
h(I.stetho), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
|
|
2532
3861
|
h('button', { className: 'ddp-btn o2' + (hist || ver ? ' sel' : ''), title: t('history'), disabled: !fileTarget, onClick: toggleHist }, h(I.history)),
|
|
3862
|
+
h('button', { className: 'ddp-btn o2' + (pane === 'handoff' ? ' sel' : ''), title: t('handoff'), disabled: !url || !!ver, onClick: () => openPane('handoff') }, h(I.code)),
|
|
2533
3863
|
h(ExportMenu, { t, kind, count: shown.length, open: exMenu, setOpen: setExMenu, onPick: (m) => doExport(m), disabled: !url }),
|
|
2534
3864
|
anyDark || dark ? h('button', { className: 'ddp-btn o1' + (dark ? ' sel' : ''), title: t('dark'), onClick: () => setDark(!dark) }, h(I.moon)) : null,
|
|
2535
3865
|
h('button', { className: 'ddp-btn o1' + (phone ? ' sel' : '') + (phoneOn && !phone ? ' has' : ''), title: t('phone'), disabled: !url, onClick: togglePhone }, h(I.scan)),
|
|
2536
3866
|
h('button', { className: 'ddp-btn o1', title: t('refresh'), onClick: reload }, h(I.refresh)),
|
|
2537
3867
|
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: [
|
|
3868
|
+
h(MoreMenu, { t, open: more, setOpen: setMore, has: (phoneOn && !phone) || (!!ckCount && !ck && !checkOff), items: [
|
|
2539
3869
|
['o2', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo],
|
|
3870
|
+
['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
|
|
2540
3871
|
['o2', I.history, t('history'), hist || !!ver, !fileTarget, toggleHist],
|
|
3872
|
+
['o2', I.dial, t('tweaks'), pane === 'tweaks', !fileTarget || !!ver, () => openPane('tweaks')],
|
|
3873
|
+
['o2', I.code, t('handoff'), pane === 'handoff', !url || !!ver, () => openPane('handoff')],
|
|
3874
|
+
['', I.library, t('systems'), pane === 'systems', false, () => openPane('systems')],
|
|
2541
3875
|
anyDark || dark ? ['o1', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
|
|
2542
3876
|
['o1', I.scan, t('phone'), phone, !url, togglePhone],
|
|
2543
3877
|
['o1', I.refresh, t('refresh'), false, false, reload],
|
|
2544
3878
|
['o1', I.external, t('external'), false, !url, url],
|
|
2545
3879
|
] }))
|
|
2546
3880
|
|
|
2547
|
-
|
|
2548
|
-
|
|
3881
|
+
// 模型用提问卡问你、在等回答:不转圈,换成一个橙点
|
|
3882
|
+
const busyChip = (cls) => st && st.asking
|
|
3883
|
+
? h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'q' }), h('span', null, t('asking')))
|
|
3884
|
+
: h('div', { className: 'ddp-chip' + cls, key: 'busy' }, h('i', { className: 'sp' }),
|
|
3885
|
+
h('span', { className: 'tx' }, st && st.writing ? t('writing', { file: st.writing }) : t('working')))
|
|
2549
3886
|
|
|
2550
3887
|
// ── 参考图:拖进来 / 粘贴 / 从设计规范面板选;存进设计目录的 refs/,尺寸和主要颜色放进输入框 ──
|
|
2551
3888
|
const addRef = async (file) => {
|
|
@@ -2594,6 +3931,39 @@ window.__ModuleLoader__.load({
|
|
|
2594
3931
|
})
|
|
2595
3932
|
const root = (...kids) => h('div', rootProps, ...kids, dropLayer, fileInput)
|
|
2596
3933
|
|
|
3934
|
+
// 调节滑块 / 品牌设计系统 / 转成代码(右边的面板)
|
|
3935
|
+
const sidePane = () => {
|
|
3936
|
+
if (pane === 'tweaks') {
|
|
3937
|
+
return h(TweaksPanel, {
|
|
3938
|
+
t,
|
|
3939
|
+
tw: tweaks,
|
|
3940
|
+
live: tw,
|
|
3941
|
+
write: busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('twSaveTip') },
|
|
3942
|
+
onSet: setTweak,
|
|
3943
|
+
onReset: () => setTw({}),
|
|
3944
|
+
onAsk: () => insert(formatTweaks(t, tlist, tw), 'insertedSend'),
|
|
3945
|
+
onSave: saveTweaks,
|
|
3946
|
+
onMake: (errs) => insert(errs && errs.length ? t('twFixText', { errs: errs.join(';') }) + '\n' : t('twMakeText') + '\n', 'insertedSend'),
|
|
3947
|
+
onClose: () => setPane(''),
|
|
3948
|
+
})
|
|
3949
|
+
}
|
|
3950
|
+
if (pane === 'systems') {
|
|
3951
|
+
return h(SystemsPanel, {
|
|
3952
|
+
t,
|
|
3953
|
+
rev: (st && st.turns) || 0,
|
|
3954
|
+
hasDesign: !!url,
|
|
3955
|
+
onExtract: (src) => insert(t('sysExtractText', { src }) + '\n', 'insertedSend'),
|
|
3956
|
+
onApply: (x) => insert(t('sysApplyText', { name: x.name, dir: x.dir }) + '\n', 'insertedSend'),
|
|
3957
|
+
onSay: say,
|
|
3958
|
+
onClose: () => setPane(''),
|
|
3959
|
+
})
|
|
3960
|
+
}
|
|
3961
|
+
if (pane === 'handoff') {
|
|
3962
|
+
return h(HandoffPanel, { t, sid, root: fileTarget ? st.target.root : '', fileTarget, count: screens.length, cur: (curScreen && curScreen.key) || focus, onGo: doHandoff, onClose: () => setPane('') })
|
|
3963
|
+
}
|
|
3964
|
+
return null
|
|
3965
|
+
}
|
|
3966
|
+
|
|
2597
3967
|
if (!url) {
|
|
2598
3968
|
if (st ? st.loading : !waited) return root(h('div', { className: 'ddp-stage', ref: stageRef }))
|
|
2599
3969
|
if (busy) {
|
|
@@ -2614,9 +3984,12 @@ window.__ModuleLoader__.load({
|
|
|
2614
3984
|
onClick: () => post('open', { session: sid, target: st.hint }).then((s) => { if (poll) poll.set(s) }, (e) => say(t('openFailed', { msg: e.message }))),
|
|
2615
3985
|
}, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
|
|
2616
3986
|
h('ol', { className: 'ddp-steps' }, h('li', null, t('step1')), h('li', null, t('step2')), h('li', null, t('step3'))),
|
|
3987
|
+
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
3988
|
h('p', null, t('emptyHint')),
|
|
2618
3989
|
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
2619
|
-
h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose')))
|
|
3990
|
+
h('p', { className: 'ddp-refline' }, t('refEmpty'), ' · ', h('button', { className: 'lk', onClick: pickRef }, t('refChoose'))),
|
|
3991
|
+
h('p', { className: 'ddp-refline' }, t('sysEmptyLine'), ' · ', h('button', { className: 'lk', onClick: () => openPane('systems') }, t('sysOpen')))),
|
|
3992
|
+
pane === 'systems' ? sidePane() : null,
|
|
2620
3993
|
toast ? h('div', { className: 'ddp-toast' }, h('span', null, toast.text)) : null)
|
|
2621
3994
|
}
|
|
2622
3995
|
|
|
@@ -2634,7 +4007,7 @@ window.__ModuleLoader__.load({
|
|
|
2634
4007
|
h('div', { className: 'ddp-single' },
|
|
2635
4008
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2636
4009
|
h(Device, { kind, w: size[0], h: size[1], fill, label },
|
|
2637
|
-
h(Screen, { t,
|
|
4010
|
+
h(Screen, { t, remote,
|
|
2638
4011
|
src: urlOfKey(l.key),
|
|
2639
4012
|
label,
|
|
2640
4013
|
busy: false,
|
|
@@ -2642,6 +4015,7 @@ window.__ModuleLoader__.load({
|
|
|
2642
4015
|
picking: false,
|
|
2643
4016
|
demo: isTop,
|
|
2644
4017
|
dark,
|
|
4018
|
+
tweak,
|
|
2645
4019
|
zoom: z,
|
|
2646
4020
|
reloadSeq,
|
|
2647
4021
|
onNav: isTop ? demoGo : undefined,
|
|
@@ -2668,7 +4042,7 @@ window.__ModuleLoader__.load({
|
|
|
2668
4042
|
sel: '',
|
|
2669
4043
|
labelOf: (sc) => sc.label,
|
|
2670
4044
|
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,
|
|
4045
|
+
h(Screen, { t, remote,
|
|
2672
4046
|
src: base.url,
|
|
2673
4047
|
label: labelOf(base),
|
|
2674
4048
|
busy,
|
|
@@ -2676,6 +4050,7 @@ window.__ModuleLoader__.load({
|
|
|
2676
4050
|
picking,
|
|
2677
4051
|
textEdit,
|
|
2678
4052
|
dark,
|
|
4053
|
+
tweak,
|
|
2679
4054
|
quietPick: true,
|
|
2680
4055
|
zoom: z,
|
|
2681
4056
|
reloadSeq,
|
|
@@ -2697,7 +4072,7 @@ window.__ModuleLoader__.load({
|
|
|
2697
4072
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
2698
4073
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
2699
4074
|
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,
|
|
4075
|
+
const screenOf = (sc) => h(Screen, { t, remote, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
2701
4076
|
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk) })
|
|
2702
4077
|
body = h('div', { className: 'ddp-single' },
|
|
2703
4078
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
@@ -2719,9 +4094,11 @@ window.__ModuleLoader__.load({
|
|
|
2719
4094
|
kind,
|
|
2720
4095
|
size,
|
|
2721
4096
|
ghost,
|
|
2722
|
-
picking: picking || textEdit,
|
|
4097
|
+
picking: picking || textEdit || styleEdit,
|
|
2723
4098
|
sel: sel || focus,
|
|
2724
4099
|
labelOf,
|
|
4100
|
+
reveal,
|
|
4101
|
+
inset: ck || styleEdit || panel || styles || pane ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
2725
4102
|
render: (sc, z) => renderScreen(sc, z, false),
|
|
2726
4103
|
flows,
|
|
2727
4104
|
onSelect: setSel,
|
|
@@ -2730,16 +4107,18 @@ window.__ModuleLoader__.load({
|
|
|
2730
4107
|
} else {
|
|
2731
4108
|
const fill = kind === 'web'
|
|
2732
4109
|
const [dw, dh] = deviceBox(kind, size)
|
|
2733
|
-
|
|
4110
|
+
// 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
|
|
4111
|
+
const aside = !fill && (ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? 296 : 0
|
|
4112
|
+
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
4113
|
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' },
|
|
4114
|
+
body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
|
|
2736
4115
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
2737
4116
|
renderScreen(one, z, fill)))
|
|
2738
4117
|
}
|
|
2739
4118
|
|
|
2740
4119
|
let chip = null
|
|
2741
4120
|
const side = canvas || inMulti ? ' left' : ''
|
|
2742
|
-
const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : ''
|
|
4121
|
+
const hintText = picking ? t(canvas || inMulti ? 'pickAny' : 'pickOne') : textEdit ? t(canvas || inMulti ? 'textAny' : 'textOne') : styleEdit && !sty ? t(canvas ? 'styleAny' : 'styleOne') : ''
|
|
2743
4122
|
if (flash) chip = h('div', { className: 'ddp-chip ok' + side, key: 'ok' }, h(I.check), h('span', null, flash))
|
|
2744
4123
|
else if (busy) chip = busyChip(side)
|
|
2745
4124
|
|
|
@@ -2749,8 +4128,24 @@ window.__ModuleLoader__.load({
|
|
|
2749
4128
|
h('div', { className: 'ddp-body' + (ver || demo || hintTop ? ' vb' : '') }, body),
|
|
2750
4129
|
busy ? h('div', { className: 'ddp-glow' }) : null,
|
|
2751
4130
|
busy ? h('div', { className: 'ddp-ring' }) : null,
|
|
2752
|
-
|
|
2753
|
-
|
|
4131
|
+
// 右边开着面板时提示条居中在面板左边那块,别被面板盖住一半
|
|
4132
|
+
hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
|
|
4133
|
+
ck ? h(CheckPanel, { t, ck, onFocus: focusIssue, onFix: fixIssues, onAgain: runCheck, onClose: closeCheck }) : null,
|
|
4134
|
+
styleEdit ? h(StylePanel, {
|
|
4135
|
+
t,
|
|
4136
|
+
sty,
|
|
4137
|
+
tokens: (sty && (infoOf(sty.key) || {}).tokens) || tokens,
|
|
4138
|
+
write: !fileTarget ? { ok: false, tip: t('styleNoWrite') } : busy ? { ok: false, tip: t('styleBusyTip') } : { ok: true, tip: t('styleSaveTip') },
|
|
4139
|
+
onSet: setStyleProp,
|
|
4140
|
+
onReset: () => setSty((c) => (c ? { ...c, live: {} } : c)),
|
|
4141
|
+
onAsk: askStyle,
|
|
4142
|
+
onSave: saveStyle,
|
|
4143
|
+
onClose: () => setStyleEdit(false),
|
|
4144
|
+
}) : null,
|
|
4145
|
+
panel ? h(TokensPanel, { t, tokens, onPick: (tok) => insert(formatToken(tok, stRef.current)), onAsk: () => insert(t('tokensAskText') + '\n', 'insertedSend'), onRef: pickRef,
|
|
4146
|
+
onStyles: () => { setPanel(false); setStyles(true) }, onSystems: () => openPane('systems'), onClose: () => setPanel(false) }) : null,
|
|
4147
|
+
pane ? sidePane() : null,
|
|
4148
|
+
styles ? h(StylesPanel, { t, onPick: (x) => { setStyles(false); insert(formatStyle(t, x, true), 'insertedSend') }, onClose: () => setStyles(false) }) : null,
|
|
2754
4149
|
phone ? h(PhonePanel, {
|
|
2755
4150
|
t,
|
|
2756
4151
|
sid,
|
|
@@ -2849,7 +4244,7 @@ window.__ModuleLoader__.load({
|
|
|
2849
4244
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
2850
4245
|
exports.inject = INJECT
|
|
2851
4246
|
// 测试用
|
|
2852
|
-
exports._test = { poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
4247
|
+
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
4248
|
return module.exports
|
|
2854
4249
|
},
|
|
2855
4250
|
})
|