dsh-design-studio 0.5.0 → 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 +8 -2
- package/README.md +15 -2
- package/cordis.patch.yml +1 -1
- package/lib/browser.js +27 -5
- package/lib/client.js +380 -6
- package/lib/core.js +35 -9
- package/lib/handoff.js +555 -0
- package/lib/history.js +1 -1
- package/lib/index.js +258 -6
- package/lib/look.js +1 -1
- package/lib/picker.js +35 -0
- package/lib/serve.js +2 -1
- package/lib/system.js +1360 -0
- package/lib/tool.js +6 -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 +27 -1
package/lib/client.js
CHANGED
|
@@ -303,6 +303,49 @@ window.__ModuleLoader__.load({
|
|
|
303
303
|
failed: '没放进去:{msg}',
|
|
304
304
|
openFailed: '打不开:{msg}',
|
|
305
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% 以下再做下一屏。设计目录里的文件别改。',
|
|
306
349
|
}
|
|
307
350
|
const EN = {
|
|
308
351
|
title: 'Design Studio',
|
|
@@ -568,6 +611,49 @@ window.__ModuleLoader__.load({
|
|
|
568
611
|
failed: 'Could not add it: {msg}',
|
|
569
612
|
openFailed: 'Could not open: {msg}',
|
|
570
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.',
|
|
571
657
|
}
|
|
572
658
|
|
|
573
659
|
function makeT(ctx) {
|
|
@@ -883,6 +969,39 @@ window.__ModuleLoader__.load({
|
|
|
883
969
|
.ddp-act .ddp-btn{height:22px;min-width:22px;padding:0 4px;color:${ACCENT}}
|
|
884
970
|
.ddp-act .ddp-btn svg{width:13px;height:13px}
|
|
885
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}
|
|
886
1005
|
.ddp-more{display:none}
|
|
887
1006
|
.ddp-menu .o1,.ddp-menu .o2{display:none}
|
|
888
1007
|
@container (max-width:700px){.ddp-bar>.o1{display:none}.ddp-bar>.ddp-more{display:block}.ddp-menu .o1{display:flex}}
|
|
@@ -940,6 +1059,9 @@ window.__ModuleLoader__.load({
|
|
|
940
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 })]),
|
|
941
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' })]),
|
|
942
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' })]),
|
|
943
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),
|
|
944
1066
|
}
|
|
945
1067
|
// 引导页上的大图标
|
|
@@ -2219,6 +2341,7 @@ window.__ModuleLoader__.load({
|
|
|
2219
2341
|
L.readied.add(id)
|
|
2220
2342
|
L.title = d.title || ''
|
|
2221
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 })
|
|
2222
2345
|
if (p.onMeta) p.onMeta(d.meta || '', d.title || '', Array.isArray(d.tokens) ? d.tokens : [], !!d.dark, Array.isArray(d.links) ? d.links : [])
|
|
2223
2346
|
if (p.onHref && d.href) p.onHref(d.href)
|
|
2224
2347
|
if (isCur) {
|
|
@@ -2296,6 +2419,9 @@ window.__ModuleLoader__.load({
|
|
|
2296
2419
|
const cssSig = JSON.stringify(props.styleCss || {})
|
|
2297
2420
|
useEffect(() => { if (cur && props.styleEdit) postTo(cur, { type: 'style', css: props.styleCss || {} }) }, [cssSig])
|
|
2298
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])
|
|
2299
2425
|
useEffect(() => {
|
|
2300
2426
|
// 刚退出改字(点按钮、Esc、模型开始干活):页面会把正在改的那一处交上来,这一小会儿还收
|
|
2301
2427
|
if (editWas.current && !props.textEdit) editOffAt.current = Date.now()
|
|
@@ -2396,7 +2522,7 @@ window.__ModuleLoader__.load({
|
|
|
2396
2522
|
}
|
|
2397
2523
|
|
|
2398
2524
|
/** 设计规范面板:按组列出变量,点一个放进输入框 */
|
|
2399
|
-
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onClose }) {
|
|
2525
|
+
function TokensPanel({ t, tokens, onPick, onAsk, onRef, onStyles, onSystems, onClose }) {
|
|
2400
2526
|
const groups = {}
|
|
2401
2527
|
for (const tok of tokens) (groups[tokenKind(tok)] = groups[tokenKind(tok)] || []).push(tok)
|
|
2402
2528
|
const px = (v) => Math.max(0, parseFloat(v) * (/rem|em$/.test(v) ? 16 : 1)) || 0
|
|
@@ -2424,6 +2550,7 @@ window.__ModuleLoader__.load({
|
|
|
2424
2550
|
: h('div', { className: 'none' }, t('tokensEmpty'), h('div', null, h('button', { className: 'ddp-go', onClick: onAsk }, t('tokensAsk'))))),
|
|
2425
2551
|
onRef || onStyles ? h('footer', null,
|
|
2426
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,
|
|
2427
2554
|
onRef ? h('button', { title: t('refPickTip'), onClick: onRef }, h(I.image), h('span', null, t('refPick'))) : null) : null)
|
|
2428
2555
|
}
|
|
2429
2556
|
|
|
@@ -2530,6 +2657,160 @@ window.__ModuleLoader__.load({
|
|
|
2530
2657
|
h('div', { className: 'bd' }, h(StyleCards, { t, onPick })))
|
|
2531
2658
|
}
|
|
2532
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')))))
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2533
2814
|
/** 菜单开着时:点外面、按 Esc 关掉 */
|
|
2534
2815
|
function useDismiss(open, setOpen, ref) {
|
|
2535
2816
|
useEffect(() => {
|
|
@@ -2873,6 +3154,8 @@ window.__ModuleLoader__.load({
|
|
|
2873
3154
|
const [ckCount, setCkCount] = useState(0) // 最近一次体检(模型干完活会悄悄查一次)有几个问题:面板关着时标在按钮上
|
|
2874
3155
|
const [reveal, setReveal] = useState(null) // 画布上要露出来的那一屏 { key, n }(体检里点了一条)
|
|
2875
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 → 值(还没写进文件)
|
|
2876
3159
|
const apis = useRef(new Map()) // 屏 → 截图用的接口
|
|
2877
3160
|
const cmpRef = useRef(null)
|
|
2878
3161
|
const [box, setBox] = useState({ w: 0, h: 0 })
|
|
@@ -2935,6 +3218,20 @@ window.__ModuleLoader__.load({
|
|
|
2935
3218
|
// 远程看(手机走外网等):本地文件的预览由服务端打包传过来(见 Screen),目录里改了哪些文件也一起带过去
|
|
2936
3219
|
const remote = fileTarget && sid ? { sid, at: st.changedAt || 0, files: st.changedFiles || [], changes: st.changes || [] } : null
|
|
2937
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
|
|
3234
|
+
|
|
2938
3235
|
// 看旧版本:画布上换成那一版的几屏(按现在的顺序,那一版独有的接在后面)
|
|
2939
3236
|
const vbase = ver ? `${origin}/__dsh_design__/v/${ver.n}/` : ''
|
|
2940
3237
|
const shown = ver
|
|
@@ -3238,6 +3535,7 @@ window.__ModuleLoader__.load({
|
|
|
3238
3535
|
styleEdit: styleEdit && !sc.v,
|
|
3239
3536
|
styleSel: sty && sty.key === sc.key ? sty.selector : '',
|
|
3240
3537
|
styleCss: sty && sty.key === sc.key ? sty.live : null,
|
|
3538
|
+
tweak: sc.v ? null : tweak,
|
|
3241
3539
|
onStylePick: (p) => onStylePick(sc, p),
|
|
3242
3540
|
onStyleCancel: () => setStyleEdit(false),
|
|
3243
3541
|
dark,
|
|
@@ -3503,6 +3801,39 @@ window.__ModuleLoader__.load({
|
|
|
3503
3801
|
|
|
3504
3802
|
const toggleDemo = () => (demo ? setDemo(null) : startDemo())
|
|
3505
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
|
+
}
|
|
3506
3837
|
const togglePhone = () => { setPhone(!phone); setPanel(false); setStyles(false); setHist(false); closeCheck(); setStyleEdit(false) }
|
|
3507
3838
|
const reload = () => setReloadSeq((x) => x + 1)
|
|
3508
3839
|
|
|
@@ -3524,9 +3855,11 @@ window.__ModuleLoader__.load({
|
|
|
3524
3855
|
h('button', { className: 'ddp-btn o2' + (demo ? ' sel' : ''), title: t('demo'), disabled: !url || !!ver, onClick: toggleDemo }, h(I.play)),
|
|
3525
3856
|
h(KindMenu, { t, kind, open: menu, setOpen: setMenu, onPick: (k) => { setMenu(false); change({ kind: k }) } }),
|
|
3526
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)),
|
|
3527
3859
|
h('button', { className: 'ddp-btn o2' + (ck ? ' sel' : ''), title: ckCount && !ck ? t('checkCount', { n: ckCount }) : t('checkTip'), disabled: checkOff, onClick: () => (ck ? closeCheck() : runCheck()) },
|
|
3528
3860
|
h(I.stetho), ckCount && !ck && !checkOff ? h('span', { className: 'bdg' }, ckCount > 99 ? '99+' : String(ckCount)) : null),
|
|
3529
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)),
|
|
3530
3863
|
h(ExportMenu, { t, kind, count: shown.length, open: exMenu, setOpen: setExMenu, onPick: (m) => doExport(m), disabled: !url }),
|
|
3531
3864
|
anyDark || dark ? h('button', { className: 'ddp-btn o1' + (dark ? ' sel' : ''), title: t('dark'), onClick: () => setDark(!dark) }, h(I.moon)) : null,
|
|
3532
3865
|
h('button', { className: 'ddp-btn o1' + (phone ? ' sel' : '') + (phoneOn && !phone ? ' has' : ''), title: t('phone'), disabled: !url, onClick: togglePhone }, h(I.scan)),
|
|
@@ -3536,6 +3869,9 @@ window.__ModuleLoader__.load({
|
|
|
3536
3869
|
['o2', I.play, t('demoShort'), !!demo, !url || !!ver, toggleDemo],
|
|
3537
3870
|
['o2', I.stetho, t('check'), !!ck, checkOff, () => (ck ? closeCheck() : runCheck())],
|
|
3538
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')],
|
|
3539
3875
|
anyDark || dark ? ['o1', I.moon, t('dark'), dark, false, () => setDark(!dark)] : null,
|
|
3540
3876
|
['o1', I.scan, t('phone'), phone, !url, togglePhone],
|
|
3541
3877
|
['o1', I.refresh, t('refresh'), false, false, reload],
|
|
@@ -3595,6 +3931,39 @@ window.__ModuleLoader__.load({
|
|
|
3595
3931
|
})
|
|
3596
3932
|
const root = (...kids) => h('div', rootProps, ...kids, dropLayer, fileInput)
|
|
3597
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
|
+
|
|
3598
3967
|
if (!url) {
|
|
3599
3968
|
if (st ? st.loading : !waited) return root(h('div', { className: 'ddp-stage', ref: stageRef }))
|
|
3600
3969
|
if (busy) {
|
|
@@ -3618,7 +3987,9 @@ window.__ModuleLoader__.load({
|
|
|
3618
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)) })),
|
|
3619
3988
|
h('p', null, t('emptyHint')),
|
|
3620
3989
|
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
3621
|
-
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,
|
|
3622
3993
|
toast ? h('div', { className: 'ddp-toast' }, h('span', null, toast.text)) : null)
|
|
3623
3994
|
}
|
|
3624
3995
|
|
|
@@ -3644,6 +4015,7 @@ window.__ModuleLoader__.load({
|
|
|
3644
4015
|
picking: false,
|
|
3645
4016
|
demo: isTop,
|
|
3646
4017
|
dark,
|
|
4018
|
+
tweak,
|
|
3647
4019
|
zoom: z,
|
|
3648
4020
|
reloadSeq,
|
|
3649
4021
|
onNav: isTop ? demoGo : undefined,
|
|
@@ -3678,6 +4050,7 @@ window.__ModuleLoader__.load({
|
|
|
3678
4050
|
picking,
|
|
3679
4051
|
textEdit,
|
|
3680
4052
|
dark,
|
|
4053
|
+
tweak,
|
|
3681
4054
|
quietPick: true,
|
|
3682
4055
|
zoom: z,
|
|
3683
4056
|
reloadSeq,
|
|
@@ -3725,7 +4098,7 @@ window.__ModuleLoader__.load({
|
|
|
3725
4098
|
sel: sel || focus,
|
|
3726
4099
|
labelOf,
|
|
3727
4100
|
reveal,
|
|
3728
|
-
inset: ck || styleEdit || panel || styles ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
4101
|
+
inset: ck || styleEdit || panel || styles || pane ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
3729
4102
|
render: (sc, z) => renderScreen(sc, z, false),
|
|
3730
4103
|
flows,
|
|
3731
4104
|
onSelect: setSel,
|
|
@@ -3735,7 +4108,7 @@ window.__ModuleLoader__.load({
|
|
|
3735
4108
|
const fill = kind === 'web'
|
|
3736
4109
|
const [dw, dh] = deviceBox(kind, size)
|
|
3737
4110
|
// 右边开着面板(体检、调样式、设计规范、换风格):手机这类有外框的让到面板左边(侧栏够宽时),别被挡住
|
|
3738
|
-
const aside = !fill && (ck || styleEdit || panel || styles) && box.w - 296 >= 240 ? 296 : 0
|
|
4111
|
+
const aside = !fill && (ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? 296 : 0
|
|
3739
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))
|
|
3740
4113
|
const one = ver ? shown.find((x) => x.key === focus) || shown[0] || { key: focus, url, label: st.label } : { key: focus, url, label: st.label }
|
|
3741
4114
|
body = h('div', { className: 'ddp-single', style: aside ? { right: aside } : undefined },
|
|
@@ -3756,7 +4129,7 @@ window.__ModuleLoader__.load({
|
|
|
3756
4129
|
busy ? h('div', { className: 'ddp-glow' }) : null,
|
|
3757
4130
|
busy ? h('div', { className: 'ddp-ring' }) : null,
|
|
3758
4131
|
// 右边开着面板时提示条居中在面板左边那块,别被面板盖住一半
|
|
3759
|
-
hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
|
|
4132
|
+
hintText ? h('div', { className: 'ddp-hint' + ((ck || styleEdit || panel || styles || pane) && box.w - 296 >= 240 ? ' side' : '') }, hintText) : null,
|
|
3760
4133
|
ck ? h(CheckPanel, { t, ck, onFocus: focusIssue, onFix: fixIssues, onAgain: runCheck, onClose: closeCheck }) : null,
|
|
3761
4134
|
styleEdit ? h(StylePanel, {
|
|
3762
4135
|
t,
|
|
@@ -3770,7 +4143,8 @@ window.__ModuleLoader__.load({
|
|
|
3770
4143
|
onClose: () => setStyleEdit(false),
|
|
3771
4144
|
}) : null,
|
|
3772
4145
|
panel ? h(TokensPanel, { t, tokens, onPick: (tok) => insert(formatToken(tok, stRef.current)), onAsk: () => insert(t('tokensAskText') + '\n', 'insertedSend'), onRef: pickRef,
|
|
3773
|
-
onStyles: () => { setPanel(false); setStyles(true) }, onClose: () => setPanel(false) }) : null,
|
|
4146
|
+
onStyles: () => { setPanel(false); setStyles(true) }, onSystems: () => openPane('systems'), onClose: () => setPanel(false) }) : null,
|
|
4147
|
+
pane ? sidePane() : null,
|
|
3774
4148
|
styles ? h(StylesPanel, { t, onPick: (x) => { setStyles(false); insert(formatStyle(t, x, true), 'insertedSend') }, onClose: () => setStyles(false) }) : null,
|
|
3775
4149
|
phone ? h(PhonePanel, {
|
|
3776
4150
|
t,
|
package/lib/core.js
CHANGED
|
@@ -519,7 +519,7 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
519
519
|
|
|
520
520
|
function persist(id, st) {
|
|
521
521
|
if (!store) return
|
|
522
|
-
if (st.target) saved[id] = { target: st.target, source: st.source, design: st.design, kind: st.kind, size: st.size, view: st.view, routes: st.routes, order: st.target.kind === 'file' ? st.screens : undefined, at: now() }
|
|
522
|
+
if (st.target) saved[id] = { target: st.target, source: st.source, design: st.design, kind: st.kind, size: st.size, view: st.view, routes: st.routes, order: st.target.kind === 'file' ? st.screens : undefined, cwd: cwds.get(id) || (saved[id] && saved[id].cwd) || undefined, at: now() }
|
|
523
523
|
else delete saved[id]
|
|
524
524
|
// 只留最近 200 个会话
|
|
525
525
|
const ids = Object.keys(saved)
|
|
@@ -573,7 +573,8 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
function cwdOf(id) {
|
|
576
|
-
|
|
576
|
+
// DSH 重启后还没跑过工具时内存里没有,用缓存里记的(右侧栏「转成代码」要靠它找项目)
|
|
577
|
+
return cwds.get(id) || (saved[id] && saved[id].cwd) || os.homedir()
|
|
577
578
|
}
|
|
578
579
|
|
|
579
580
|
/**
|
|
@@ -759,7 +760,10 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
759
760
|
break
|
|
760
761
|
}
|
|
761
762
|
case 'tool/result': {
|
|
762
|
-
if (d.meta && typeof d.meta.cwd === 'string' && d.meta.cwd)
|
|
763
|
+
if (d.meta && typeof d.meta.cwd === 'string' && d.meta.cwd) {
|
|
764
|
+
cwds.set(sessionId, d.meta.cwd)
|
|
765
|
+
if (st.target && saved[sessionId] && saved[sessionId].cwd !== d.meta.cwd) persist(sessionId, st)
|
|
766
|
+
}
|
|
763
767
|
st.writing = ''
|
|
764
768
|
if (!live) break
|
|
765
769
|
const pending = st.pendingHtml
|
|
@@ -838,10 +842,7 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
838
842
|
const st = states.get(id)
|
|
839
843
|
const t = st && st.target
|
|
840
844
|
if (!t || t.kind !== 'file') return { ok: false, reason: 'not-file' }
|
|
841
|
-
|
|
842
|
-
const realOf = (p) => { try { return fs.realpathSync(p) } catch { return p } }
|
|
843
|
-
const mine = realOf(t.root)
|
|
844
|
-
for (const o of states.values()) if (o.turn && o.target && o.target.kind === 'file' && (o.target.root === t.root || realOf(o.target.root) === mine)) return { ok: false, reason: 'busy' }
|
|
845
|
+
if (busyRoot(t.root)) return { ok: false, reason: 'busy' }
|
|
845
846
|
const name = String(key || path.basename(t.file))
|
|
846
847
|
const file = path.join(t.root, name)
|
|
847
848
|
if (path.dirname(file) !== t.root || !HTML_EXT.test(name) || !fs.existsSync(file)) return { ok: false, reason: 'no-screen' }
|
|
@@ -857,6 +858,31 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
857
858
|
return { ok: true, file: name }
|
|
858
859
|
}
|
|
859
860
|
|
|
861
|
+
/** 这个目录有没有会话的模型正在干活(它可能正整份重写文件,这时改了会被盖掉);经软链接打开的同一个目录也算 */
|
|
862
|
+
function busyRoot(root) {
|
|
863
|
+
const realOf = (p) => { try { return fs.realpathSync(p) } catch { return p } }
|
|
864
|
+
const mine = realOf(root)
|
|
865
|
+
for (const o of states.values()) if (o.turn && o.target && o.target.kind === 'file' && (o.target.root === root || realOf(o.target.root) === mine)) return true
|
|
866
|
+
return false
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* 改设计目录里的几个文件(调节滑块写回:tokens.css、每屏 <html> 上的属性、tweaks.json):fn(目录) → { ok, files, … };
|
|
871
|
+
* 改前改后各存一版。开发服务器的页面、模型正在干活时不改;给了 expectRoot 而现在看的不是它也不改
|
|
872
|
+
*/
|
|
873
|
+
function editDir(id, fn, label, expectRoot) {
|
|
874
|
+
const st = states.get(id)
|
|
875
|
+
const t = st && st.target
|
|
876
|
+
if (!t || t.kind !== 'file') return { ok: false, reason: 'not-file' }
|
|
877
|
+
// 右侧栏发请求时看的是哪套设计:这期间换了设计就别写,免得写进另一套
|
|
878
|
+
if (expectRoot !== undefined && t.root !== expectRoot) return { ok: false, reason: 'moved' }
|
|
879
|
+
if (busyRoot(t.root)) return { ok: false, reason: 'busy' }
|
|
880
|
+
keepVersion(st, '')
|
|
881
|
+
const r = fn(t.root)
|
|
882
|
+
if (r && r.ok && r.files && r.files.length) keepVersion(st, typeof label === 'function' ? label(r) : label)
|
|
883
|
+
return r
|
|
884
|
+
}
|
|
885
|
+
|
|
860
886
|
/** 版本列表(新的在前);开发服务器没有版本(项目自己有 git) */
|
|
861
887
|
function versions(id) {
|
|
862
888
|
const st = states.get(id)
|
|
@@ -881,7 +907,7 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
881
907
|
const { target, url, label, source, rev, openRev, turn, writing, changedAt, kind, size, view, origin } = st
|
|
882
908
|
const file = target.kind === 'file'
|
|
883
909
|
return {
|
|
884
|
-
target: file ? { kind: 'file', file: target.file } : { kind: 'proxy', port: target.port, path: target.path },
|
|
910
|
+
target: file ? { kind: 'file', file: target.file, root: target.root } : { kind: 'proxy', port: target.port, path: target.path },
|
|
885
911
|
url,
|
|
886
912
|
label,
|
|
887
913
|
source,
|
|
@@ -908,5 +934,5 @@ export function createTracker({ open, skipPorts = () => new Set(), now = Date.no
|
|
|
908
934
|
}
|
|
909
935
|
}
|
|
910
936
|
|
|
911
|
-
return { observe, setTarget, setView, clear, changed, snapshot, cwdOf, stateOf, versions, restoreVersion, editText, editStyle, keepVersion: (id, label) => keepVersion(stateOf(id), label) }
|
|
937
|
+
return { observe, setTarget, setView, clear, changed, snapshot, cwdOf, stateOf, versions, restoreVersion, editText, editStyle, editDir, keepVersion: (id, label) => keepVersion(stateOf(id), label) }
|
|
912
938
|
}
|