dsh-design-studio 0.8.0 → 0.9.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 +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +6 -4
- package/lib/browser.js +34 -0
- package/lib/client.js +334 -46
- package/lib/core.js +4 -3
- package/lib/handoff.js +7 -0
- package/lib/index.js +1 -1
- package/lib/look.js +19 -4
- package/lib/picker.js +407 -34
- package/package.json +1 -1
- package/skills/ui-design/SKILL.md +4 -3
- package/skills/ui-design/references/app.md +1 -1
- package/skills/ui-design/references/desktop.md +24 -0
- package/skills/ui-design/references/miniprogram.md +36 -0
package/lib/client.js
CHANGED
|
@@ -85,6 +85,7 @@ window.__ModuleLoader__.load({
|
|
|
85
85
|
kindApp: 'App',
|
|
86
86
|
kindMini: '小程序',
|
|
87
87
|
kindTablet: '平板',
|
|
88
|
+
kindDesktop: '桌面软件',
|
|
88
89
|
kindPoster: '海报',
|
|
89
90
|
kindSlides: '幻灯片',
|
|
90
91
|
kindEmail: '邮件',
|
|
@@ -141,6 +142,11 @@ window.__ModuleLoader__.load({
|
|
|
141
142
|
ck_jserr: '页面脚本报错',
|
|
142
143
|
ck_missing: '文件没载出来',
|
|
143
144
|
ck_capsule: '被小程序胶囊挡住',
|
|
145
|
+
ck_island: '被灵动岛挡住',
|
|
146
|
+
ck_homebar: '伸进底部横条区',
|
|
147
|
+
ck_tabbar: '标签栏项太多',
|
|
148
|
+
ck_navbar: '没有导航栏',
|
|
149
|
+
ck_winctl: '被窗口按钮挡住',
|
|
144
150
|
cd_hscroll: '伸出屏幕 {px}px',
|
|
145
151
|
cd_clipped: '被切掉 {px}px',
|
|
146
152
|
cd_overflow: '伸出框 {px}px',
|
|
@@ -158,6 +164,15 @@ window.__ModuleLoader__.load({
|
|
|
158
164
|
cd_jserr: '{msg}{at}',
|
|
159
165
|
cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
|
|
160
166
|
cd_capsule: '伸进右上角胶囊 {px}px(胶囊是微信画的、盖不住:顶栏右边从 x={x} 起空着)',
|
|
167
|
+
cd_island: '被灵动岛挡住 {px}px(灵动岛在 y 11–45:状态栏中间空着,内容放到状态栏下面)',
|
|
168
|
+
cd_homebar: '{bar} 里的字或图标伸进底部横条区 {px}px(底部留 34px:栏加 padding-bottom: 34px,预览里 env(safe-area-inset-bottom) 是 0)',
|
|
169
|
+
cd_tabbar: '标签栏有 {n} 项(最多 5 项)',
|
|
170
|
+
cd_navbar: '顶上 y {t}–{b}(导航栏)是空的:一级页放标题或品牌,二级页标题居中加返回',
|
|
171
|
+
cd_winctl: '伸进{osName}窗口按钮 {px}px(外框画的、盖在页面上面:{zone}空着)',
|
|
172
|
+
ckOs_mac: ' Mac ',
|
|
173
|
+
ckOs_win: ' Windows ',
|
|
174
|
+
ckZone_mac: '左上角 x 80、y 40 以内',
|
|
175
|
+
ckZone_win: '右上角 138×32',
|
|
161
176
|
ckEdge_top: '顶栏',
|
|
162
177
|
ckEdge_bottom: '底栏',
|
|
163
178
|
ckWhen_top: '没滚时',
|
|
@@ -252,10 +267,26 @@ window.__ModuleLoader__.load({
|
|
|
252
267
|
styleJoin: ';',
|
|
253
268
|
lang: 'zh',
|
|
254
269
|
stylesHead: '先挑一个风格开工',
|
|
255
|
-
stylesTip: '
|
|
270
|
+
stylesTip: '点一个看样板,满意了点「用这个风格」,整套配色、字体、圆角放进输入框;再写要做什么',
|
|
256
271
|
stylesOpen: '换个风格…',
|
|
257
272
|
stylesTitle: '换个风格',
|
|
258
|
-
stylesSwap: '
|
|
273
|
+
stylesSwap: '点一个看样板;「用这个风格」让模型把现在的设计换成它(先改 tokens.css,布局和文案不动)',
|
|
274
|
+
stylesAll: '全部风格',
|
|
275
|
+
stylePrev: '上一个(←)',
|
|
276
|
+
styleNext: '下一个(→)',
|
|
277
|
+
styleUse: '用这个风格',
|
|
278
|
+
samBrand: '小森',
|
|
279
|
+
samNav: '发现 课程 我的',
|
|
280
|
+
samHead: '把日子过得好一点',
|
|
281
|
+
samSub: '一句说明文字,看看正文和次要文字的样子。',
|
|
282
|
+
samGo: '开始使用',
|
|
283
|
+
samMore: '了解更多 →',
|
|
284
|
+
samItem: '周末手作课',
|
|
285
|
+
samMeta: '2 小时 · 6 人小班',
|
|
286
|
+
samPrice: '¥128',
|
|
287
|
+
samTag: '新',
|
|
288
|
+
samStat: '本周完成',
|
|
289
|
+
samStatMeta: '比上周多 12%',
|
|
259
290
|
styleText: '〔风格〕{name}——{desc}\n配色:{colors}\n字体:{fonts}\n形状:{shape}\n要点:{notes}\n{ask}',
|
|
260
291
|
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}({accUse}),链接和小字强调 {link}{bgImage}',
|
|
261
292
|
accBig: '图形、大字',
|
|
@@ -404,6 +435,7 @@ window.__ModuleLoader__.load({
|
|
|
404
435
|
kindApp: 'App',
|
|
405
436
|
kindMini: 'Mini program',
|
|
406
437
|
kindTablet: 'Tablet',
|
|
438
|
+
kindDesktop: 'Desktop app',
|
|
407
439
|
kindPoster: 'Poster',
|
|
408
440
|
kindSlides: 'Slides',
|
|
409
441
|
kindEmail: 'Email',
|
|
@@ -460,6 +492,11 @@ window.__ModuleLoader__.load({
|
|
|
460
492
|
ck_jserr: 'Script error',
|
|
461
493
|
ck_missing: 'File failed to load',
|
|
462
494
|
ck_capsule: 'Under the mini program capsule',
|
|
495
|
+
ck_island: 'Under the Dynamic Island',
|
|
496
|
+
ck_homebar: 'In the home indicator area',
|
|
497
|
+
ck_tabbar: 'Too many tabs',
|
|
498
|
+
ck_navbar: 'No navigation bar',
|
|
499
|
+
ck_winctl: 'Under the window buttons',
|
|
463
500
|
cd_hscroll: 'sticks out {px}px',
|
|
464
501
|
cd_clipped: '{px}px cut off',
|
|
465
502
|
cd_overflow: 'sticks out {px}px',
|
|
@@ -477,6 +514,15 @@ window.__ModuleLoader__.load({
|
|
|
477
514
|
cd_jserr: '{msg}{at}',
|
|
478
515
|
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
479
516
|
cd_capsule: '{px}px under the top-right capsule (WeChat draws it on top; keep the top bar clear from x={x})',
|
|
517
|
+
cd_island: '{px}px under the Dynamic Island (it sits at y 11–45: keep the middle of the status bar empty and start content below the status bar)',
|
|
518
|
+
cd_homebar: 'text or icons in {bar} reach {px}px into the home indicator area (leave 34px: give the bar padding-bottom: 34px; env(safe-area-inset-bottom) is 0 in the preview)',
|
|
519
|
+
cd_tabbar: 'the tab bar has {n} items (5 at most)',
|
|
520
|
+
cd_navbar: 'nothing at y {t}–{b} (the navigation bar): top-level pages show a title or brand, other pages a centered title and a back button',
|
|
521
|
+
cd_winctl: '{px}px under the{osName}window buttons (the frame draws them on top: keep {zone} clear)',
|
|
522
|
+
ckOs_mac: ' Mac ',
|
|
523
|
+
ckOs_win: ' Windows ',
|
|
524
|
+
ckZone_mac: 'the top-left 80×40',
|
|
525
|
+
ckZone_win: 'the top-right 138×32',
|
|
480
526
|
ckEdge_top: 'top bar',
|
|
481
527
|
ckEdge_bottom: 'bottom bar',
|
|
482
528
|
ckWhen_top: 'before scrolling, ',
|
|
@@ -571,10 +617,26 @@ window.__ModuleLoader__.load({
|
|
|
571
617
|
styleJoin: '; ',
|
|
572
618
|
lang: 'en',
|
|
573
619
|
stylesHead: 'Start from a style',
|
|
574
|
-
stylesTip: 'Pick one to
|
|
620
|
+
stylesTip: 'Pick one to see a sample; "Use this style" puts its colors, fonts and shapes in the chat box, then say what to make',
|
|
575
621
|
stylesOpen: 'Try another style…',
|
|
576
622
|
stylesTitle: 'Change the style',
|
|
577
|
-
stylesSwap: 'Pick one
|
|
623
|
+
stylesSwap: 'Pick one to see a sample; "Use this style" has the model restyle the current design (tokens.css first; layout and copy stay)',
|
|
624
|
+
stylesAll: 'All styles',
|
|
625
|
+
stylePrev: 'Previous (←)',
|
|
626
|
+
styleNext: 'Next (→)',
|
|
627
|
+
styleUse: 'Use this style',
|
|
628
|
+
samBrand: 'Bloom',
|
|
629
|
+
samNav: 'Explore Classes Me',
|
|
630
|
+
samHead: 'Make every day a little better',
|
|
631
|
+
samSub: 'One line of supporting text to show body and muted colors.',
|
|
632
|
+
samGo: 'Get started',
|
|
633
|
+
samMore: 'Learn more →',
|
|
634
|
+
samItem: 'Weekend workshop',
|
|
635
|
+
samMeta: '2 hrs · small group of 6',
|
|
636
|
+
samPrice: '$18',
|
|
637
|
+
samTag: 'New',
|
|
638
|
+
samStat: 'Done this week',
|
|
639
|
+
samStatMeta: '12% more than last week',
|
|
578
640
|
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
579
641
|
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} ({accUse}), links and small emphasis {link}{bgImage}',
|
|
580
642
|
accBig: 'graphics and large text only',
|
|
@@ -750,6 +812,11 @@ window.__ModuleLoader__.load({
|
|
|
750
812
|
.ddp-chrome span{flex:1;min-width:0;margin:0 60px 0 18px;height:20px;border-radius:6px;background:rgba(0,0,0,.05);color:#6b6b74;font:12px/20px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
751
813
|
.ddp-island{position:absolute;left:50%;width:118px;height:34px;margin-left:-59px;border-radius:20px;background:#000;z-index:4;pointer-events:none}
|
|
752
814
|
.ddp-cap{position:absolute;width:87px;height:32px;z-index:4;pointer-events:none;overflow:visible;fill:#000}
|
|
815
|
+
.ddp-dev.k-desktop{box-shadow:0 0 0 .5px rgba(0,0,0,.3),0 22px 60px rgba(0,0,0,.28)}
|
|
816
|
+
.ddp-dev.fill.k-desktop{box-shadow:none}
|
|
817
|
+
.ddp-win{position:absolute;z-index:4;pointer-events:none;overflow:visible}
|
|
818
|
+
.ddp-win .x{fill:none;stroke:#1f1f1f;stroke-width:1}
|
|
819
|
+
.ddp-win.w .x{stroke:#fff}
|
|
753
820
|
.ddp-cap .pl{fill:rgba(255,255,255,.6);stroke:rgba(0,0,0,.08);stroke-width:.5}
|
|
754
821
|
.ddp-cap .sp{stroke:rgba(0,0,0,.15);stroke-width:.5}
|
|
755
822
|
.ddp-cap .rg{fill:none;stroke:#000;stroke-width:2}
|
|
@@ -920,14 +987,47 @@ window.__ModuleLoader__.load({
|
|
|
920
987
|
.ddp-style .cnt{padding:0 14px 6px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
921
988
|
.ddp-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px 8px}
|
|
922
989
|
.ddp-sc{display:flex;flex-direction:column;gap:5px;min-width:0;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}
|
|
923
|
-
.ddp-sc .pv{position:relative;display:block;height:
|
|
924
|
-
.ddp-sc:hover .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
925
|
-
.ddp-sc:
|
|
926
|
-
.ddp-sc
|
|
927
|
-
.ddp-sc .pv .
|
|
928
|
-
.ddp-
|
|
929
|
-
.ddp-
|
|
930
|
-
.ddp-
|
|
990
|
+
.ddp-sc .pv{position:relative;display:block;height:74px;border-radius:10px;overflow:hidden;container-type:inline-size;box-shadow:0 0 0 .5px rgba(127,127,127,.35);transition:transform .15s ${EASE},box-shadow .15s}
|
|
991
|
+
.ddp-sc:hover .pv,.ddp-sc:focus-visible .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
992
|
+
.ddp-sc:focus-visible{outline:none}
|
|
993
|
+
.ddp-sc:hover,.ddp-sc:focus-visible{color:var(--dsw-alias-label-primary)}
|
|
994
|
+
.ddp-sc .pv .ddp-sam{width:calc(100cqw / .32);min-height:calc(100% / .32);transform:scale(.32);transform-origin:0 0;pointer-events:none}
|
|
995
|
+
.ddp-sam{display:flex;flex-direction:column;gap:7px;padding:12px 14px 14px;font-size:11px;line-height:1.45;text-align:left;overflow:hidden;box-sizing:border-box}
|
|
996
|
+
.ddp-sam b{font-weight:600;color:inherit} /* 空白页的 .ddp-empty b 会把字染成界面色 */
|
|
997
|
+
.ddp-sam .top{display:flex;align-items:center;gap:6px;margin-bottom:3px}
|
|
998
|
+
.ddp-sam .top b{font-size:12px}
|
|
999
|
+
.ddp-sam .lg{width:14px;height:14px;flex:none}
|
|
1000
|
+
.ddp-sam .nv{margin-left:auto;font-size:10px;white-space:nowrap}
|
|
1001
|
+
.ddp-sam .av{width:18px;height:18px;flex:none;border-radius:50%}
|
|
1002
|
+
.ddp-sam h5{margin:2px 0 0;font-size:21px;line-height:1.2}
|
|
1003
|
+
.ddp-sam p{margin:0}
|
|
1004
|
+
.ddp-sam .acts{display:flex;align-items:center;gap:12px;margin:2px 0 4px;white-space:nowrap}
|
|
1005
|
+
.ddp-sam .bt{display:inline-flex;align-items:center;height:26px;padding:0 13px;font-weight:600}
|
|
1006
|
+
.ddp-sam .cards{display:grid;grid-template-columns:1.25fr 1fr;gap:9px;align-items:stretch}
|
|
1007
|
+
.ddp-sam .cd{display:flex;flex-direction:column;gap:2px;min-width:0;padding:8px;font-size:10px;box-sizing:border-box}
|
|
1008
|
+
.ddp-sam .cd>*{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
1009
|
+
.ddp-sam .cd>b{font-size:11.5px}
|
|
1010
|
+
.ddp-sam .im{display:block;flex:none;height:46px;margin-bottom:4px}
|
|
1011
|
+
.ddp-sam .pr{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:3px}
|
|
1012
|
+
.ddp-sam .pr b{font-size:15px;font-weight:700}
|
|
1013
|
+
.ddp-sam .chip{padding:1px 7px;border-radius:999px;font-size:9.5px;font-weight:600}
|
|
1014
|
+
.ddp-sam .cd>.num{margin:2px 0 6px;font-size:28px;line-height:1.1}
|
|
1015
|
+
.ddp-sam .bar{position:relative;display:block;flex:none;height:5px;margin-bottom:5px;border-radius:3px}
|
|
1016
|
+
.ddp-sam .bar i{position:absolute;left:0;top:0;bottom:0;width:86%;border-radius:inherit}
|
|
1017
|
+
.ddp-samview{display:flex;flex-direction:column;gap:8px;text-align:left}
|
|
1018
|
+
.ddp-samview .all{align-self:flex-start;height:24px;margin-left:-4px;font-size:12px}
|
|
1019
|
+
.ddp-samview .all svg,.ddp-samview .row svg{width:14px;height:14px}
|
|
1020
|
+
.ddp-samview .ddp-sam{border-radius:12px;box-shadow:0 0 0 .5px rgba(127,127,127,.35),0 8px 24px rgba(0,0,0,.10);animation:ddp-in .2s ${EASE}}
|
|
1021
|
+
.ddp-samview .meta{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px}
|
|
1022
|
+
.ddp-samview .meta b{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
1023
|
+
.ddp-samview .meta p{flex-basis:100%;margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
1024
|
+
.ddp-samview .sw{display:flex;gap:3px;margin-left:auto}
|
|
1025
|
+
.ddp-samview .sw i{width:12px;height:12px;border-radius:50%;box-shadow:0 0 0 .5px rgba(127,127,127,.5)}
|
|
1026
|
+
.ddp-samview .row{display:flex;align-items:center;gap:2px}
|
|
1027
|
+
.ddp-samview .row .ddp-btn{height:26px;min-width:26px}
|
|
1028
|
+
.ddp-samview .row .nx svg{transform:scaleX(-1)}
|
|
1029
|
+
.ddp-samview .row em{min-width:40px;font-style:normal;font-size:11px;text-align:center;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
|
|
1030
|
+
.ddp-samview .row .ddp-go{margin-left:auto;height:28px;font-size:12px}
|
|
931
1031
|
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
932
1032
|
.ddp-tries{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:440px}
|
|
933
1033
|
.ddp-tries>span{flex-basis:100%;font-size:11px;line-height:16px}
|
|
@@ -1072,6 +1172,7 @@ window.__ModuleLoader__.load({
|
|
|
1072
1172
|
app: () => svg([h('rect', { key: 1, x: 7, y: 2.5, width: 10, height: 19, rx: 2.4 }), h('path', { key: 2, d: 'M10.5 5h3' })]),
|
|
1073
1173
|
miniprogram: () => svg([h('rect', { key: 1, x: 6, y: 2.5, width: 12, height: 19, rx: 2.4 }), h('rect', { key: 2, x: 11.5, y: 5.2, width: 4.5, height: 2.4, rx: 1.2 })]),
|
|
1074
1174
|
tablet: () => svg([h('rect', { key: 1, x: 4.5, y: 3, width: 15, height: 18, rx: 2.2 }), h('path', { key: 2, d: 'M11 18h2' })]),
|
|
1175
|
+
desktop: () => svg([h('rect', { key: 1, x: 2.5, y: 4.5, width: 19, height: 15, rx: 2.6 }), h('path', { key: 2, d: 'M9 4.5v15' }), h('path', { key: 3, d: 'M4.8 7.2h.01M6.8 7.2h.01' })]),
|
|
1075
1176
|
poster: () => svg([h('rect', { key: 1, x: 5, y: 3, width: 14, height: 18, rx: 1.8 }), h('path', { key: 2, d: 'M5 16l4-4 3 3 2-2 5 5' }), h('circle', { key: 3, cx: 14.5, cy: 8, r: 1.6 })]),
|
|
1076
1177
|
slides: () => svg([h('rect', { key: 1, x: 2.5, y: 4, width: 19, height: 12, rx: 1.8 }), h('path', { key: 2, d: 'M12 16v3M8.5 20.5h7' }), h('path', { key: 3, d: 'M6.5 8h6M6.5 11h4' })]),
|
|
1077
1178
|
email: () => svg([h('rect', { key: 1, x: 3, y: 5, width: 18, height: 14, rx: 2.2 }), h('path', { key: 2, d: 'M3.5 6.5l8.5 6.5 8.5-6.5' })]),
|
|
@@ -1334,6 +1435,7 @@ window.__ModuleLoader__.load({
|
|
|
1334
1435
|
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1335
1436
|
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1336
1437
|
const MONO = '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace'
|
|
1438
|
+
const GROTESK = '"Helvetica Neue", Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif'
|
|
1337
1439
|
const STYLES = [
|
|
1338
1440
|
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1339
1441
|
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
@@ -1411,8 +1513,75 @@ window.__ModuleLoader__.load({
|
|
|
1411
1513
|
head: SANS, w: 800, radius: 12, shadow: '0 4px 12px rgba(207,46,14,.12)',
|
|
1412
1514
|
nzh: '价格用大号粗体数字(¥ 小一号),原价灰色划线;「限时」「包邮」这类小标签用色块;优惠券用金黄色、带锯齿边;倒计时醒目;信息多但分区清楚,主按钮通栏',
|
|
1413
1515
|
nen: 'Prices in big bold numerals (smaller currency sign), old price struck through in gray; small colored tags like "Limited" and "Free shipping"; gold coupons with notched edges; a prominent countdown; dense but clearly sectioned, full-width main button' },
|
|
1516
|
+
{ id: 'japandi', zh: '日系原木', en: 'Japandi', dzh: '米色原木、低饱和、小字大留白', den: 'Oat and wood tones, muted, small type and lots of space',
|
|
1517
|
+
c: { bg: '#F2EEE6', surface: '#FAF8F3', text: '#2F2B26', muted: '#6B645A', line: '#DDD5C8', primary: '#4F4234', accent: '#8C9A6E', on: '#FAF8F3', link: '#6B4F33' },
|
|
1518
|
+
head: SANS, w: 500, radius: 6, shadow: '0 2px 8px rgba(79,66,52,.06)',
|
|
1519
|
+
nzh: '像杂货店和家居品牌:字号偏小、字重不重,留白很多;颜色只有米、原木棕、灰绿,饱和度都低;照片柔光、自然材质(木、麻、陶);细线分隔,几乎不用阴影;主按钮深棕实底',
|
|
1520
|
+
nen: 'Like a lifestyle or homeware brand: smallish, light type and lots of space; only oat, wood brown and sage, all low saturation; soft-light photos of natural materials (wood, linen, ceramic); hairline dividers, almost no shadow; solid dark-brown main button' },
|
|
1521
|
+
{ id: 'swiss', zh: '瑞士网格', en: 'Swiss grid', dzh: '白底黑字红色块、严格网格', den: 'White, black type, red blocks, strict grid',
|
|
1522
|
+
c: { bg: '#FFFFFF', surface: '#F1F1F1', text: '#0A0A0A', muted: '#545454', line: '#0A0A0A', primary: '#D9001B', accent: '#0A0A0A', on: '#FFFFFF', link: '#C2001A' },
|
|
1523
|
+
head: GROTESK, w: 800, radius: 0, shadow: 'none',
|
|
1524
|
+
nzh: '12 栏严格网格、全部左对齐(不居中);标题是超大粗无衬线、字距收紧(-0.03em);红色大色块只用一两处;用数字编号(01 02 03)和粗黑线分区;没有圆角、阴影和渐变',
|
|
1525
|
+
nen: 'A strict 12-column grid, everything left-aligned (nothing centered); huge, bold, tightly tracked sans headings (-0.03em); one or two big red blocks; numbered sections (01 02 03) and heavy black rules; no radius, shadow or gradient' },
|
|
1526
|
+
{ id: 'brutal', zh: '粗野主义', en: 'Brutalist', dzh: '亮黄底、黑粗框、硬阴影、等宽字', den: 'Bright yellow, thick black borders, hard shadows, monospace',
|
|
1527
|
+
c: { bg: '#FFEF5C', surface: '#FFFFFF', text: '#000000', muted: '#3B3B3B', line: '#000000', primary: '#000000', accent: '#FF5C39', on: '#FFEF5C', link: '#1F3FD1' },
|
|
1528
|
+
head: MONO, w: 800, radius: 0, shadow: '5px 5px 0 #000000',
|
|
1529
|
+
nzh: '卡片和按钮 3px 黑粗边框 + 大偏移的硬阴影(不模糊);标题等宽字、很大;亮黄、白、橙红高饱和撞色;元素可以故意错位、叠放;不用渐变、圆角和柔和阴影;链接带下划线',
|
|
1530
|
+
nen: '3px black borders and big unblurred offset shadows on cards and buttons; large monospace headings; loud yellow, white and red-orange blocks; deliberate offsets and overlaps are fine; no gradients, radius or soft shadows; underlined links' },
|
|
1531
|
+
{ id: 'cream', zh: '奶油风', en: 'Cream', dzh: '奶白燕麦焦糖、大圆角、柔光', den: 'Milk, oat and caramel, big radii, soft light',
|
|
1532
|
+
c: { bg: '#FAF5EE', surface: '#FFFDF9', text: '#3A2F28', muted: '#76675C', line: '#EEE3D5', primary: '#8C5E3C', accent: '#E7C9A6', on: '#FFFFFF', link: '#8A5A36' },
|
|
1533
|
+
head: ROUND, w: 700, radius: 22, shadow: '0 10px 30px rgba(140,94,60,.10)',
|
|
1534
|
+
nzh: '奶白、燕麦、焦糖三个色;圆角大,多用圆形和胶囊形;阴影又大又淡、带一点暖色;字是深咖啡色不用纯黑;照片和插画柔光、低对比;整体软、暖、安静',
|
|
1535
|
+
nen: 'Milk, oat and caramel; big radii, lots of circles and pills; large, faint, warm-tinted shadows; dark coffee text, never pure black; soft, low-contrast photos and illustrations; soft, warm and calm overall' },
|
|
1536
|
+
{ id: 'sport', zh: '运动能量', en: 'Sport', dzh: '黑底荧光绿、粗斜体、大数字', den: 'Black with volt green, heavy italics, big numbers',
|
|
1537
|
+
c: { bg: '#0E0F11', surface: '#1A1C20', text: '#F4F5F6', muted: '#A1A5AD', line: '#2B2E35', primary: '#C8FF2E', accent: '#FF5A1F', on: '#0E0F11', link: '#C8FF2E' },
|
|
1538
|
+
head: SANS, w: 900, radius: 6, shadow: 'none',
|
|
1539
|
+
nzh: '标题超粗斜体、全大写、字距收紧;荧光绿只给主按钮、进度和关键数字;配速、心率、卡路里这类数字做得很大(等宽数字);可以用斜切的色块(transform: skewX(-8deg));照片高对比、有动感;橙色只做警示和次要强调',
|
|
1540
|
+
nen: 'Heavy italic, all-caps, tightly tracked headings; volt green only on the main button, progress and key numbers; big tabular numbers for pace, heart rate and calories; slanted blocks (transform: skewX(-8deg)) are fine; high-contrast action photos; orange only for alerts and secondary emphasis' },
|
|
1414
1541
|
]
|
|
1415
1542
|
|
|
1543
|
+
/** 样板上各风格的招牌细节(卡片、按钮、标题、配图块),没写的按 STYLES 里的圆角阴影来 */
|
|
1544
|
+
const LOOK = {
|
|
1545
|
+
minimal: { card: { boxShadow: '0 1px 2px rgba(24,24,27,.06), inset 0 0 0 1px #E4E4E7' }, img: 'linear-gradient(135deg, #E9EEF8, #C7D6F5)' },
|
|
1546
|
+
warm: { img: 'linear-gradient(135deg, #E7B08A, #A9542A)' },
|
|
1547
|
+
editorial: { card: { background: 'transparent', boxShadow: 'inset 0 3px 0 #111111', borderRadius: 0 }, head: { letterSpacing: '-0.02em' }, img: '#C8231B' },
|
|
1548
|
+
dark: { head: { letterSpacing: '.04em' }, img: 'linear-gradient(135deg, #2A2620, #C9A86A)' },
|
|
1549
|
+
playful: { card: { border: '2px solid #1D1B3A' }, btn: { border: '2px solid #1D1B3A', boxShadow: '3px 3px 0 #1D1B3A' }, img: '#1F9E92' },
|
|
1550
|
+
fresh: { card: { boxShadow: '0 6px 18px rgba(47,125,79,.10), inset 0 0 0 1px #DCE6DA' }, img: 'linear-gradient(135deg, #CFE6C9, #7DB08B)' },
|
|
1551
|
+
tech: { card: { boxShadow: '0 1px 3px rgba(15,23,42,.08), inset 0 0 0 1px #E2E8F0' }, img: 'linear-gradient(135deg, #DCE6FF, #2457F5)' },
|
|
1552
|
+
glass: { card: { background: 'rgba(255,255,255,.55)', backdropFilter: 'blur(14px)', boxShadow: '0 10px 30px rgba(16,21,28,.10), inset 0 0 0 1px rgba(255,255,255,.85)' }, img: 'linear-gradient(135deg, #FFD9C7, #C9E2FF)' },
|
|
1553
|
+
soft: { btn: { boxShadow: '4px 4px 10px #C8CED6, -4px -4px 10px #FFFFFF' }, img: 'linear-gradient(135deg, #DDE2EA, #F4F6F9)' },
|
|
1554
|
+
guochao: { card: { boxShadow: 'inset 0 0 0 1px #C8952E, 0 6px 18px rgba(140,30,20,.12)' } },
|
|
1555
|
+
ink: { head: { letterSpacing: '.12em' }, img: 'radial-gradient(ellipse at 35% 65%, rgba(30,30,28,.55), transparent 62%), #ECE7DD' },
|
|
1556
|
+
cyber: { card: { boxShadow: 'inset 0 0 0 1px #2E2B52' }, head: { textTransform: 'uppercase', letterSpacing: '.04em' } },
|
|
1557
|
+
cute: { btn: { boxShadow: '0 4px 0 #E0628E' }, img: 'linear-gradient(135deg, #FFD3E2, #FFC93C)' },
|
|
1558
|
+
retro: { card: { border: '2px solid #2E1F14' }, btn: { boxShadow: '0 3px 0 #2E1F14' }, img: 'linear-gradient(180deg, #B5471D 0 33%, #E0A526 33% 66%, #2F5D50 66%)' },
|
|
1559
|
+
sale: { img: 'linear-gradient(135deg, #FF8A3D, #CF2E0E)' },
|
|
1560
|
+
japandi: { card: { boxShadow: 'inset 0 0 0 1px #DDD5C8' }, img: 'linear-gradient(160deg, #DCD1BF, #B6A58C)' },
|
|
1561
|
+
swiss: { card: { boxShadow: 'none' }, head: { letterSpacing: '-0.03em' }, img: '#D9001B' },
|
|
1562
|
+
brutal: { card: { border: '3px solid #000000' }, btn: { border: '3px solid #000000', boxShadow: '3px 3px 0 #000000' }, img: '#FF5C39' },
|
|
1563
|
+
cream: { img: 'linear-gradient(135deg, #F3E3CF, #E7C9A6)' },
|
|
1564
|
+
sport: { head: { fontStyle: 'italic', textTransform: 'uppercase', letterSpacing: '-0.01em' }, btn: { transform: 'skewX(-8deg)' }, img: 'linear-gradient(115deg, #C8FF2E 0 42%, #2B2E35 42%)' },
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** 一个风格在样板上的样子:卡片、主按钮、标题字、配图块、小标签 */
|
|
1568
|
+
function styleLook(x) {
|
|
1569
|
+
const c = x.c
|
|
1570
|
+
const o = LOOK[x.id] || {}
|
|
1571
|
+
// 小标签是 9.5px 的小字,要 4.5:1:点缀色在卡片和底色上都够就描边写点缀色,色块上正文色够就做色块,都不行用主色
|
|
1572
|
+
const chip = Math.min(hexRatio(c.accent, c.surface), hexRatio(c.accent, c.bg)) >= 4.5 ? { color: c.accent, boxShadow: `inset 0 0 0 1px ${c.accent}` }
|
|
1573
|
+
: hexRatio(c.text, c.accent) >= 4.5 ? { background: c.accent, color: c.text } : { background: c.primary, color: c.on }
|
|
1574
|
+
return {
|
|
1575
|
+
card: { background: c.surface, borderRadius: x.radius, boxShadow: x.shadow === 'none' ? `inset 0 0 0 1px ${c.line}` : x.shadow, ...o.card },
|
|
1576
|
+
btn: { background: c.primary, color: c.on, borderRadius: Math.min(x.radius, 14), ...o.btn },
|
|
1577
|
+
head: { fontFamily: x.head, fontWeight: x.w, ...o.head },
|
|
1578
|
+
img: o.img || `linear-gradient(135deg, ${c.primary}, ${c.accent})`,
|
|
1579
|
+
// 进度条底:分隔线是黑的(杂志、瑞士、粗野)就换成淡一点的正文色,不然和黑色主色分不开
|
|
1580
|
+
track: c.line === c.text || c.line === c.primary ? `color-mix(in srgb, ${c.text} 14%, ${c.surface})` : c.line,
|
|
1581
|
+
chip,
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1416
1585
|
/** 两个 #RRGGBB 的对比度(WCAG) */
|
|
1417
1586
|
function hexRatio(a, b) {
|
|
1418
1587
|
const l = (hex) => { const v = hex.slice(1).match(/../g).map((x) => parseInt(x, 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2] }
|
|
@@ -1538,6 +1707,8 @@ window.__ModuleLoader__.load({
|
|
|
1538
1707
|
otherText: String(it.otherText || '').slice(0, 20),
|
|
1539
1708
|
edgeName: edge ? t('ckEdge_' + edge) : '',
|
|
1540
1709
|
when: edge ? t('ckWhen_' + edge) : '',
|
|
1710
|
+
osName: it.type === 'winctl' ? t('ckOs_' + (it.os === 'win' ? 'win' : 'mac')) : '',
|
|
1711
|
+
zone: it.type === 'winctl' ? t('ckZone_' + (it.os === 'win' ? 'win' : 'mac')) : '',
|
|
1541
1712
|
what: it.type === 'missing' ? t('ckWhat_' + (['css', 'script', 'font'].includes(it.what) ? it.what : 'other')) : '',
|
|
1542
1713
|
at: it.type === 'jserr' && it.src ? ` (${it.src}${it.line ? ':' + it.line : ''})` : '',
|
|
1543
1714
|
})
|
|
@@ -1597,9 +1768,9 @@ window.__ModuleLoader__.load({
|
|
|
1597
1768
|
|
|
1598
1769
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
1599
1770
|
|
|
1600
|
-
const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
1771
|
+
const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, desktop: 2, web: 2, poster: 3, slides: 2, email: 3 }
|
|
1601
1772
|
const CANVAS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1602
|
-
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, web: 1 }
|
|
1773
|
+
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, desktop: 1, web: 1 }
|
|
1603
1774
|
|
|
1604
1775
|
function loadImg(blob) {
|
|
1605
1776
|
return new Promise((resolve, reject) => {
|
|
@@ -1624,7 +1795,7 @@ window.__ModuleLoader__.load({
|
|
|
1624
1795
|
}
|
|
1625
1796
|
|
|
1626
1797
|
/** 在 (x, y) 画一个带外框的屏:w × ht 是屏幕本身;img 按宽铺进去,高出来的裁掉 */
|
|
1627
|
-
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite) {
|
|
1798
|
+
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite, os) {
|
|
1628
1799
|
const f = frameOf(kind, w, ht)
|
|
1629
1800
|
const pad = f.pad || 0
|
|
1630
1801
|
const top = f.top || 0
|
|
@@ -1684,17 +1855,58 @@ window.__ModuleLoader__.load({
|
|
|
1684
1855
|
}
|
|
1685
1856
|
// 胶囊颜色用页面截图那一刻自己量的(和画布上一致);老页面没报就看截图
|
|
1686
1857
|
if (f.capsule) drawCapsule(ctx, x + pad + w - CAP.right - CAP.w, y + pad + CAP.top, typeof capWhite === 'boolean' ? capWhite : !!img && capDarkOf(img, w))
|
|
1858
|
+
if (f.win) {
|
|
1859
|
+
rrect(ctx, x + 0.25, y + 0.25, w - 0.5, ht - 0.5, f.r)
|
|
1860
|
+
ctx.strokeStyle = 'rgba(0,0,0,.28)'
|
|
1861
|
+
ctx.lineWidth = 0.5
|
|
1862
|
+
ctx.stroke()
|
|
1863
|
+
drawWinCtl(ctx, x, y, w, os, typeof capWhite === 'boolean' ? capWhite : os === 'win' && !!img && areaDarkOf(img, w, w - WINBTN.w * 3, 0, WINBTN.w * 3, WINBTN.h))
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/** 窗口按钮画到导出的图上(和画布上 WinCtl 一样) */
|
|
1868
|
+
function drawWinCtl(ctx, x, y, w, os, white) {
|
|
1869
|
+
ctx.save()
|
|
1870
|
+
if (os === 'win') {
|
|
1871
|
+
const W = WINBTN.w, H = WINBTN.h, x0 = x + w - W * 3
|
|
1872
|
+
ctx.strokeStyle = white ? '#fff' : '#1f1f1f'
|
|
1873
|
+
ctx.lineWidth = 1
|
|
1874
|
+
ctx.beginPath()
|
|
1875
|
+
ctx.moveTo(x0 + W / 2 - 5, y + H / 2 + 0.5); ctx.lineTo(x0 + W / 2 + 5, y + H / 2 + 0.5)
|
|
1876
|
+
ctx.moveTo(x0 + W * 2.5 - 5, y + H / 2 - 5); ctx.lineTo(x0 + W * 2.5 + 5, y + H / 2 + 5)
|
|
1877
|
+
ctx.moveTo(x0 + W * 2.5 + 5, y + H / 2 - 5); ctx.lineTo(x0 + W * 2.5 - 5, y + H / 2 + 5)
|
|
1878
|
+
ctx.stroke()
|
|
1879
|
+
rrect(ctx, x0 + W * 1.5 - 4.5, y + H / 2 - 4.5, 9, 9, 1)
|
|
1880
|
+
ctx.stroke()
|
|
1881
|
+
} else {
|
|
1882
|
+
const { x: lx, y: ly, d, gap } = LIGHTS
|
|
1883
|
+
;['#FF5F57', '#FEBC2E', '#28C840'].forEach((c, i) => {
|
|
1884
|
+
ctx.beginPath()
|
|
1885
|
+
ctx.arc(x + lx + d / 2 + i * (d + gap), y + ly + d / 2, d / 2 - 0.25, 0, Math.PI * 2)
|
|
1886
|
+
ctx.fillStyle = c
|
|
1887
|
+
ctx.fill()
|
|
1888
|
+
ctx.strokeStyle = 'rgba(0,0,0,.12)'
|
|
1889
|
+
ctx.lineWidth = 0.5
|
|
1890
|
+
ctx.stroke()
|
|
1891
|
+
})
|
|
1892
|
+
}
|
|
1893
|
+
ctx.restore()
|
|
1687
1894
|
}
|
|
1688
1895
|
|
|
1689
1896
|
/** 截图里胶囊那块是不是深色(导出时没有页面可问,看图) */
|
|
1690
1897
|
function capDarkOf(img, w) {
|
|
1898
|
+
return areaDarkOf(img, w, w - CAP.right - CAP.w, CAP.top, CAP.w, CAP.h)
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/** 截图里 (sx, sy, sw, sh) 那块(屏幕坐标)是不是深色 */
|
|
1902
|
+
function areaDarkOf(img, w, sx, sy, sw, sh) {
|
|
1691
1903
|
try {
|
|
1692
1904
|
const k = img.width / w
|
|
1693
1905
|
const c = document.createElement('canvas')
|
|
1694
|
-
c.width = Math.max(1, Math.round(
|
|
1695
|
-
c.height = Math.max(1, Math.round(
|
|
1906
|
+
c.width = Math.max(1, Math.round(sw * k))
|
|
1907
|
+
c.height = Math.max(1, Math.round(sh * k))
|
|
1696
1908
|
const g = c.getContext('2d')
|
|
1697
|
-
g.drawImage(img,
|
|
1909
|
+
g.drawImage(img, sx * k, sy * k, sw * k, sh * k, 0, 0, c.width, c.height)
|
|
1698
1910
|
const d = g.getImageData(0, 0, c.width, c.height).data
|
|
1699
1911
|
let sum = 0
|
|
1700
1912
|
for (let i = 0; i < d.length; i += 4) sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]
|
|
@@ -1730,7 +1942,7 @@ window.__ModuleLoader__.load({
|
|
|
1730
1942
|
}
|
|
1731
1943
|
|
|
1732
1944
|
/** 一屏带外框(四周留出阴影的地方,背景透明) */
|
|
1733
|
-
async function composeOne(kind, size, img, label, top) {
|
|
1945
|
+
async function composeOne(kind, size, img, label, top, os) {
|
|
1734
1946
|
const [w, ht] = size
|
|
1735
1947
|
const [W, H] = frameSize(kind, w, ht)
|
|
1736
1948
|
const m = 56
|
|
@@ -1740,7 +1952,7 @@ window.__ModuleLoader__.load({
|
|
|
1740
1952
|
c.height = Math.round((H + m * 2) * S)
|
|
1741
1953
|
const ctx = c.getContext('2d')
|
|
1742
1954
|
ctx.scale(S, S)
|
|
1743
|
-
drawDevice(ctx, kind, img, m, m, w, ht, label, top)
|
|
1955
|
+
drawDevice(ctx, kind, img, m, m, w, ht, label, top, os)
|
|
1744
1956
|
return canvasBlob(c)
|
|
1745
1957
|
}
|
|
1746
1958
|
|
|
@@ -1894,7 +2106,7 @@ window.__ModuleLoader__.load({
|
|
|
1894
2106
|
* 整套展示图:几屏带外框排成网格,上面是品牌名和一行说明,铺一层底色 + 点缀色的光晕。
|
|
1895
2107
|
* shots: [{ img, label }]
|
|
1896
2108
|
*/
|
|
1897
|
-
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark }) {
|
|
2109
|
+
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark, os }) {
|
|
1898
2110
|
const [w, ht] = size
|
|
1899
2111
|
const [DW, DH] = frameSize(kind, w, ht)
|
|
1900
2112
|
const u = Math.max(1, Math.min(2, DW / 430))
|
|
@@ -1955,7 +2167,7 @@ window.__ModuleLoader__.load({
|
|
|
1955
2167
|
const off = ((cols - inRow) * (DW + gap)) / 2 // 最后一行不满就居中
|
|
1956
2168
|
const x = pad + off + col * (DW + gap)
|
|
1957
2169
|
const yy = y + row * (DH + labH + gap * 0.6)
|
|
1958
|
-
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top)
|
|
2170
|
+
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top, os)
|
|
1959
2171
|
ctx.fillStyle = muted
|
|
1960
2172
|
ctx.font = `500 ${16 * u}px ${CANVAS_FONT}`
|
|
1961
2173
|
ctx.textAlign = 'center'
|
|
@@ -2032,17 +2244,18 @@ window.__ModuleLoader__.load({
|
|
|
2032
2244
|
|
|
2033
2245
|
// ── 设计类型:画幅和外框 ─────────────────────────────────────────────────
|
|
2034
2246
|
|
|
2035
|
-
const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
|
|
2036
|
-
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
2247
|
+
const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'desktop', 'poster', 'slides', 'email']
|
|
2248
|
+
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', desktop: 'kindDesktop', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
2037
2249
|
const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
|
|
2038
2250
|
const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
|
|
2039
|
-
const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
2040
|
-
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule
|
|
2251
|
+
const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], desktop: [1280, 800], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
2252
|
+
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule 小程序右上角的胶囊、win 桌面软件的窗口按钮
|
|
2041
2253
|
const FRAME = {
|
|
2042
2254
|
web: { pad: 0, top: 34, r: 10, ir: 0 },
|
|
2043
2255
|
app: { pad: 11, top: 0, r: 55, ir: 44, island: 11 },
|
|
2044
2256
|
miniprogram: { pad: 11, top: 0, r: 55, ir: 44, island: 11, capsule: 1 },
|
|
2045
2257
|
tablet: { pad: 14, top: 0, r: 34, ir: 21 },
|
|
2258
|
+
desktop: { pad: 0, top: 0, r: 12, ir: 12, win: 1 },
|
|
2046
2259
|
poster: { pad: 0, top: 0, r: 6, ir: 6 },
|
|
2047
2260
|
slides: { pad: 0, top: 0, r: 8, ir: 8 },
|
|
2048
2261
|
email: { pad: 0, top: 0, r: 10, ir: 10 },
|
|
@@ -2055,15 +2268,19 @@ window.__ModuleLoader__.load({
|
|
|
2055
2268
|
}
|
|
2056
2269
|
// 小程序胶囊:离屏幕顶 48(状态栏 44 + 4)、离右边 7、87×32(和页面体检 picker.js 的 CAP 一样)
|
|
2057
2270
|
const CAP = { top: 48, right: 7, w: 87, h: 32 }
|
|
2058
|
-
|
|
2271
|
+
// 桌面软件的窗口按钮(和 picker.js 的一样):Mac 红绿灯在左上 x 20、y 20 起,三个 12px 圆点隔 8;Windows 最小化 / 最大化 / 关闭在右上角,各 46×32
|
|
2272
|
+
const LIGHTS = { x: 20, y: 20, d: 12, gap: 8 }
|
|
2273
|
+
const WINBTN = { w: 46, h: 32 }
|
|
2274
|
+
const META_ALIAS = { phone: 'app', mobile: 'app', ios: 'app', android: 'app', mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram', pad: 'tablet', ipad: 'tablet', pc: 'desktop', electron: 'desktop', tauri: 'desktop', mac: 'desktop', macos: 'desktop', windows: 'desktop', win: 'desktop', 桌面: 'desktop', 桌面软件: 'desktop', social: 'poster', image: 'poster', slide: 'slides', deck: 'slides', ppt: 'slides', mail: 'email' }
|
|
2059
2275
|
|
|
2060
|
-
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" → { kind, size } */
|
|
2276
|
+
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" / "desktop windows" → { kind, size, os }(os:win,不写是 Mac) */
|
|
2061
2277
|
function parseMeta(content) {
|
|
2062
|
-
const out = { kind: '', size: '' }
|
|
2278
|
+
const out = { kind: '', size: '', os: '' }
|
|
2063
2279
|
for (const w of String(content || '').toLowerCase().split(/[\s,;]+/)) {
|
|
2064
2280
|
if (!out.kind && (KIND_LIST.includes(w) || META_ALIAS[w])) out.kind = KIND_LIST.includes(w) ? w : META_ALIAS[w]
|
|
2065
2281
|
const m = /^(\d{2,5})[x×*](\d{2,5})$/.exec(w)
|
|
2066
2282
|
if (!out.size && m) out.size = `${m[1]}x${m[2]}`
|
|
2283
|
+
if (!out.os && /^(windows|win|win10|win11)$/.test(w)) out.os = 'win'
|
|
2067
2284
|
}
|
|
2068
2285
|
return out
|
|
2069
2286
|
}
|
|
@@ -2133,6 +2350,7 @@ window.__ModuleLoader__.load({
|
|
|
2133
2350
|
app: [['szSmall', 'app', [375, 667]], ['szStd', 'app', [393, 852]], ['szLarge', 'app', [430, 932]]],
|
|
2134
2351
|
miniprogram: [['szSmall', 'miniprogram', [375, 667]], ['szStd', 'miniprogram', [375, 812]], ['szLarge', 'miniprogram', [414, 896]]],
|
|
2135
2352
|
tablet: [['szSmall', 'tablet', [744, 1133]], ['szStd', 'tablet', [820, 1180]], ['szLarge', 'tablet', [1032, 1376]]],
|
|
2353
|
+
desktop: [['szSmall', 'desktop', [1024, 680]], ['szStd', 'desktop', [1280, 800]], ['szLarge', 'desktop', [1440, 900]]],
|
|
2136
2354
|
}
|
|
2137
2355
|
|
|
2138
2356
|
/** 大小不一的几块排成一行(顶端对齐),算出整块大小和放得下的缩放 */
|
|
@@ -2526,7 +2744,7 @@ window.__ModuleLoader__.load({
|
|
|
2526
2744
|
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
|
|
2527
2745
|
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
|
|
2528
2746
|
*/
|
|
2529
|
-
function Device({ kind, w, h: ht, fill, label, top, children }) {
|
|
2747
|
+
function Device({ kind, w, h: ht, fill, label, top, os, children }) {
|
|
2530
2748
|
const f = fill ? { pad: 0, top: 0, r: 0, ir: 0 } : frameOf(kind, w, ht)
|
|
2531
2749
|
return h('div', {
|
|
2532
2750
|
className: `ddp-dev k-${kind}` + (fill ? ' fill' : ''),
|
|
@@ -2535,7 +2753,22 @@ window.__ModuleLoader__.load({
|
|
|
2535
2753
|
h('div', { className: 'ddp-chrome', style: { display: f.top ? 'flex' : 'none', height: f.top } }, h('i'), h('i'), h('i'), h('span', null, label || '')),
|
|
2536
2754
|
h('div', { className: 'ddp-vp', style: fill ? { inset: 0 } : { left: f.pad, top: f.pad + f.top, width: w, height: ht, borderRadius: f.ir } }, children),
|
|
2537
2755
|
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } }),
|
|
2538
|
-
f.capsule ? h(Capsule, { white: !!top, style: { top: (f.pad || 0) + CAP.top, right: (f.pad || 0) + CAP.right } }) : null
|
|
2756
|
+
f.capsule ? h(Capsule, { white: !!top, style: { top: (f.pad || 0) + CAP.top, right: (f.pad || 0) + CAP.right } }) : null,
|
|
2757
|
+
f.win ? h(WinCtl, { os, white: !!top }) : null)
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/** 桌面软件的窗口按钮:Mac 左上角红绿灯,Windows 右上角最小化 / 最大化 / 关闭(顶栏深色时画白的) */
|
|
2761
|
+
function WinCtl({ os, white }) {
|
|
2762
|
+
if (os === 'win') {
|
|
2763
|
+
const W = WINBTN.w, H = WINBTN.h
|
|
2764
|
+
return h('svg', { className: 'ddp-win' + (white ? ' w' : ''), style: { top: 0, right: 0, width: W * 3, height: H }, viewBox: `0 0 ${W * 3} ${H}`, 'aria-hidden': true },
|
|
2765
|
+
h('path', { className: 'x', d: `M${W / 2 - 5} ${H / 2 + 0.5}h10` }),
|
|
2766
|
+
h('rect', { className: 'x', x: W * 1.5 - 4.5, y: H / 2 - 4.5, width: 9, height: 9, rx: 1 }),
|
|
2767
|
+
h('path', { className: 'x', d: `M${W * 2.5 - 5} ${H / 2 - 5}l10 10M${W * 2.5 + 5} ${H / 2 - 5}l-10 10` }))
|
|
2768
|
+
}
|
|
2769
|
+
const { x, y, d, gap } = LIGHTS
|
|
2770
|
+
return h('svg', { className: 'ddp-win', style: { left: x, top: y, width: d * 3 + gap * 2, height: d }, viewBox: `0 0 ${d * 3 + gap * 2} ${d}`, 'aria-hidden': true },
|
|
2771
|
+
['#FF5F57', '#FEBC2E', '#28C840'].map((c, i) => h('circle', { key: i, cx: d / 2 + i * (d + gap), cy: d / 2, r: d / 2 - 0.25, fill: c, stroke: 'rgba(0,0,0,.12)', strokeWidth: 0.5 })))
|
|
2539
2772
|
}
|
|
2540
2773
|
|
|
2541
2774
|
function Capsule({ white, style }) {
|
|
@@ -3059,17 +3292,71 @@ window.__ModuleLoader__.load({
|
|
|
3059
3292
|
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
3060
3293
|
}
|
|
3061
3294
|
|
|
3062
|
-
/**
|
|
3295
|
+
/** 风格样板:用这个风格画的一小屏(顶栏、标题、按钮、商品卡、数据卡);卡片上缩小着放,点开是原大 */
|
|
3296
|
+
function StyleSample({ t, x }) {
|
|
3297
|
+
const c = x.c
|
|
3298
|
+
const L = styleLook(x)
|
|
3299
|
+
const muted = { color: c.muted }
|
|
3300
|
+
return h('div', { className: 'ddp-sam', style: { background: c.bg, backgroundImage: x.bgImage || 'none', color: c.text, fontFamily: SANS } },
|
|
3301
|
+
h('div', { className: 'top' },
|
|
3302
|
+
h('i', { className: 'lg', style: { background: c.primary, borderRadius: Math.min(x.radius, 5) } }),
|
|
3303
|
+
h('b', { style: L.head }, t('samBrand')),
|
|
3304
|
+
h('span', { className: 'nv', style: muted }, t('samNav')),
|
|
3305
|
+
h('i', { className: 'av', style: { background: L.img } })),
|
|
3306
|
+
h('h5', { style: L.head }, t('samHead')),
|
|
3307
|
+
h('p', { style: muted }, t('samSub')),
|
|
3308
|
+
h('div', { className: 'acts' }, h('span', { className: 'bt', style: L.btn }, t('samGo')), h('span', { style: { color: c.link } }, t('samMore'))),
|
|
3309
|
+
h('div', { className: 'cards' },
|
|
3310
|
+
h('div', { className: 'cd', style: L.card },
|
|
3311
|
+
h('i', { className: 'im', style: { background: L.img, borderRadius: Math.max(0, x.radius - 6) } }),
|
|
3312
|
+
h('b', null, t('samItem')),
|
|
3313
|
+
h('span', { style: muted }, t('samMeta')),
|
|
3314
|
+
h('div', { className: 'pr' }, h('b', { style: { fontFamily: x.head } }, t('samPrice')), h('span', { className: 'chip', style: L.chip }, t('samTag')))),
|
|
3315
|
+
h('div', { className: 'cd', style: L.card },
|
|
3316
|
+
h('span', { style: muted }, t('samStat')),
|
|
3317
|
+
h('b', { className: 'num', style: L.head }, '86%'),
|
|
3318
|
+
h('i', { className: 'bar', style: { background: L.track } }, h('i', { style: { background: c.primary } })),
|
|
3319
|
+
h('span', { style: muted }, t('samStatMeta')))))
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
/** 一排风格卡:每张是缩小的样板;点一张看原大样板(左右翻),「用这个风格」才交给 onPick */
|
|
3063
3323
|
function StyleCards({ t, onPick }) {
|
|
3064
3324
|
const en = t('lang') === 'en'
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3325
|
+
const [on, setOn] = useState(-1) // 正在看样板的那个;-1 是看全部
|
|
3326
|
+
const was = useRef(-1)
|
|
3327
|
+
const box = useRef(null)
|
|
3328
|
+
useEffect(() => {
|
|
3329
|
+
// 进样板:焦点给「用这个风格」;回全部:焦点回到刚才那张卡
|
|
3330
|
+
const el = box.current
|
|
3331
|
+
if (!el) return
|
|
3332
|
+
const f = on >= 0 ? el.querySelector('.ddp-go') : was.current >= 0 ? el.querySelectorAll('.ddp-sc')[was.current] : null
|
|
3333
|
+
if (f) f.focus({ preventScroll: true })
|
|
3334
|
+
// 列表和样板高矮不一样,外面滚动的位置会变:把样板 / 刚才那张卡滚进来
|
|
3335
|
+
if (was.current >= 0 || on >= 0) (on >= 0 ? el : f || el).scrollIntoView({ block: 'nearest' })
|
|
3336
|
+
if (on >= 0) was.current = on
|
|
3337
|
+
}, [on])
|
|
3338
|
+
const n = STYLES.length
|
|
3339
|
+
if (on >= 0) {
|
|
3340
|
+
const x = STYLES[on]
|
|
3341
|
+
const go = (d) => setOn((on + d + n) % n)
|
|
3342
|
+
const onKey = (e) => {
|
|
3343
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); go(e.key === 'ArrowLeft' ? -1 : 1) } else if (e.key === 'Escape') { e.stopPropagation(); setOn(-1) }
|
|
3344
|
+
}
|
|
3345
|
+
return h('div', { className: 'ddp-samview', ref: box, onKeyDown: onKey },
|
|
3346
|
+
h('button', { className: 'ddp-btn all', onClick: () => setOn(-1) }, h(I.back), h('span', null, t('stylesAll'))),
|
|
3347
|
+
h(StyleSample, { key: x.id, t, x }),
|
|
3348
|
+
h('div', { className: 'meta' },
|
|
3349
|
+
h('b', null, en ? x.en : x.zh),
|
|
3350
|
+
h('span', { className: 'sw' }, ...['bg', 'surface', 'text', 'primary', 'accent'].map((k) => h('i', { key: k, title: x.c[k], style: { background: x.c[k] } }))),
|
|
3351
|
+
h('p', null, en ? x.den : x.dzh)),
|
|
3352
|
+
h('div', { className: 'row' },
|
|
3353
|
+
h('button', { className: 'ddp-btn', title: t('stylePrev'), onClick: () => go(-1) }, h(I.back)),
|
|
3354
|
+
h('em', null, `${on + 1} / ${n}`),
|
|
3355
|
+
h('button', { className: 'ddp-btn nx', title: t('styleNext'), onClick: () => go(1) }, h(I.back)),
|
|
3356
|
+
h('button', { className: 'ddp-go', onClick: () => onPick(x) }, t('styleUse'))))
|
|
3357
|
+
}
|
|
3358
|
+
return h('div', { className: 'ddp-styles', ref: box }, ...STYLES.map((x, i) => h('button', { key: x.id, className: 'ddp-sc', title: en ? x.den : x.dzh, onClick: () => setOn(i) },
|
|
3359
|
+
h('span', { className: 'pv', 'aria-hidden': 'true' }, h(StyleSample, { t, x })),
|
|
3073
3360
|
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
3074
3361
|
}
|
|
3075
3362
|
|
|
@@ -3958,7 +4245,7 @@ window.__ModuleLoader__.load({
|
|
|
3958
4245
|
else change({ view: 'single' })
|
|
3959
4246
|
}
|
|
3960
4247
|
|
|
3961
|
-
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)] },
|
|
4248
|
+
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)], os: meta.os },
|
|
3962
4249
|
h(Screen, { t, remote,
|
|
3963
4250
|
src: sc.url,
|
|
3964
4251
|
label: labelOf(sc),
|
|
@@ -4058,6 +4345,7 @@ window.__ModuleLoader__.load({
|
|
|
4058
4345
|
kind,
|
|
4059
4346
|
size,
|
|
4060
4347
|
shots: shots.map((x, i) => ({ img: x.img, top: x.top, label: sp.labels[i] || x.title })),
|
|
4348
|
+
os: meta.os,
|
|
4061
4349
|
title: sp.brand || (st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : st.label) || '',
|
|
4062
4350
|
sub: t('showSub', { n: shots.length, kind: t(KIND_KEY[kind]), date: ymd(new Date()) }),
|
|
4063
4351
|
bg: colors.bg,
|
|
@@ -4072,7 +4360,7 @@ window.__ModuleLoader__.load({
|
|
|
4072
4360
|
if (!sc) throw new Error('no screen')
|
|
4073
4361
|
const base = prefix + sc.key.replace(/\.html?$/i, '') + suffix
|
|
4074
4362
|
const { img, top } = await captureOf(sc, mode === 'long')
|
|
4075
|
-
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top))
|
|
4363
|
+
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top, meta.os))
|
|
4076
4364
|
else {
|
|
4077
4365
|
const c = document.createElement('canvas')
|
|
4078
4366
|
c.width = img.width
|
|
@@ -4446,7 +4734,7 @@ window.__ModuleLoader__.load({
|
|
|
4446
4734
|
return h('div', { key: l.id, className: cls },
|
|
4447
4735
|
h('div', { className: 'ddp-single' },
|
|
4448
4736
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4449
|
-
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key] },
|
|
4737
|
+
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key], os: meta.os },
|
|
4450
4738
|
h(Screen, { t, remote,
|
|
4451
4739
|
src: urlOfKey(l.key),
|
|
4452
4740
|
label,
|
|
@@ -4482,7 +4770,7 @@ window.__ModuleLoader__.load({
|
|
|
4482
4770
|
picking: picking || textEdit,
|
|
4483
4771
|
sel: '',
|
|
4484
4772
|
labelOf: (sc) => sc.label,
|
|
4485
|
-
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base), top: tops[ik(base) + '|' + i] },
|
|
4773
|
+
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base), top: tops[ik(base) + '|' + i], os: meta.os },
|
|
4486
4774
|
h(Screen, { t, remote,
|
|
4487
4775
|
src: base.url,
|
|
4488
4776
|
label: labelOf(base),
|
|
@@ -4519,7 +4807,7 @@ window.__ModuleLoader__.load({
|
|
|
4519
4807
|
body = h('div', { className: 'ddp-single' },
|
|
4520
4808
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4521
4809
|
// 胶囊那块露的是哪一边(把手拖过胶囊中间就是左边那一版),胶囊就跟哪一边的顶栏
|
|
4522
|
-
now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now), top: tops[ik(has && (cx / 100) * size[0] > size[0] - CAP.right - CAP.w / 2 ? old : now)] },
|
|
4810
|
+
now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now), top: tops[ik(has && (cx / 100) * size[0] > size[0] - CAP.right - CAP.w / 2 ? old : now)], os: meta.os },
|
|
4523
4811
|
h('div', { className: 'ddp-cmp' + (cmpDrag ? ' drag' : ''), ref: cmpRef },
|
|
4524
4812
|
h('div', { className: 'ly' }, has ? screenOf(old) : h('div', { className: 'miss' }, t('notInVer', { n: ver.n }))),
|
|
4525
4813
|
// 右边一层只露出把手右边那块:外层裁宽度,里面整屏往左挪(iframe 不变大小、不重排;clip-path 盖 iframe 有的内核画不出来)
|
|
@@ -4688,7 +4976,7 @@ window.__ModuleLoader__.load({
|
|
|
4688
4976
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
4689
4977
|
exports.inject = INJECT
|
|
4690
4978
|
// 测试用
|
|
4691
|
-
exports._test = { STYLES, formatStyle, accentUse, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
4979
|
+
exports._test = { STYLES, formatStyle, accentUse, styleLook, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
4692
4980
|
return module.exports
|
|
4693
4981
|
},
|
|
4694
4982
|
})
|