dsh-design-studio 0.7.1 → 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 +8 -6
- package/lib/browser.js +34 -0
- package/lib/client.js +479 -59
- package/lib/core.js +6 -4
- package/lib/handoff.js +14 -0
- package/lib/index.js +5 -5
- package/lib/look.js +39 -13
- package/lib/picker.js +951 -35
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/skills/ui-design/SKILL.md +5 -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 +63 -0
package/lib/client.js
CHANGED
|
@@ -83,7 +83,9 @@ window.__ModuleLoader__.load({
|
|
|
83
83
|
kind: '类型',
|
|
84
84
|
kindWeb: '网页',
|
|
85
85
|
kindApp: 'App',
|
|
86
|
+
kindMini: '小程序',
|
|
86
87
|
kindTablet: '平板',
|
|
88
|
+
kindDesktop: '桌面软件',
|
|
87
89
|
kindPoster: '海报',
|
|
88
90
|
kindSlides: '幻灯片',
|
|
89
91
|
kindEmail: '邮件',
|
|
@@ -139,6 +141,12 @@ window.__ModuleLoader__.load({
|
|
|
139
141
|
ck_tight: '离固定栏太近',
|
|
140
142
|
ck_jserr: '页面脚本报错',
|
|
141
143
|
ck_missing: '文件没载出来',
|
|
144
|
+
ck_capsule: '被小程序胶囊挡住',
|
|
145
|
+
ck_island: '被灵动岛挡住',
|
|
146
|
+
ck_homebar: '伸进底部横条区',
|
|
147
|
+
ck_tabbar: '标签栏项太多',
|
|
148
|
+
ck_navbar: '没有导航栏',
|
|
149
|
+
ck_winctl: '被窗口按钮挡住',
|
|
142
150
|
cd_hscroll: '伸出屏幕 {px}px',
|
|
143
151
|
cd_clipped: '被切掉 {px}px',
|
|
144
152
|
cd_overflow: '伸出框 {px}px',
|
|
@@ -155,6 +163,16 @@ window.__ModuleLoader__.load({
|
|
|
155
163
|
cd_tight: '离{edgeName} {bar} 只有 {px}px(留 16px 以上)',
|
|
156
164
|
cd_jserr: '{msg}{at}',
|
|
157
165
|
cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
|
|
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',
|
|
158
176
|
ckEdge_top: '顶栏',
|
|
159
177
|
ckEdge_bottom: '底栏',
|
|
160
178
|
ckWhen_top: '没滚时',
|
|
@@ -249,12 +267,31 @@ window.__ModuleLoader__.load({
|
|
|
249
267
|
styleJoin: ';',
|
|
250
268
|
lang: 'zh',
|
|
251
269
|
stylesHead: '先挑一个风格开工',
|
|
252
|
-
stylesTip: '
|
|
270
|
+
stylesTip: '点一个看样板,满意了点「用这个风格」,整套配色、字体、圆角放进输入框;再写要做什么',
|
|
253
271
|
stylesOpen: '换个风格…',
|
|
254
272
|
stylesTitle: '换个风格',
|
|
255
|
-
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%',
|
|
256
290
|
styleText: '〔风格〕{name}——{desc}\n配色:{colors}\n字体:{fonts}\n形状:{shape}\n要点:{notes}\n{ask}',
|
|
257
|
-
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}
|
|
291
|
+
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}({accUse}),链接和小字强调 {link}{bgImage}',
|
|
292
|
+
accBig: '图形、大字',
|
|
293
|
+
accBlock: '只做图形和色块,别拿来写字;色块上的字用 {text}',
|
|
294
|
+
accShape: '只做图形,别拿来写字',
|
|
258
295
|
styleBgImage: ';底色上叠 {v}',
|
|
259
296
|
styleFonts: '标题 {head};正文 {body}',
|
|
260
297
|
styleShape: '圆角 {r}px;阴影 {s}',
|
|
@@ -396,7 +433,9 @@ window.__ModuleLoader__.load({
|
|
|
396
433
|
kind: 'Type',
|
|
397
434
|
kindWeb: 'Web',
|
|
398
435
|
kindApp: 'App',
|
|
436
|
+
kindMini: 'Mini program',
|
|
399
437
|
kindTablet: 'Tablet',
|
|
438
|
+
kindDesktop: 'Desktop app',
|
|
400
439
|
kindPoster: 'Poster',
|
|
401
440
|
kindSlides: 'Slides',
|
|
402
441
|
kindEmail: 'Email',
|
|
@@ -452,6 +491,12 @@ window.__ModuleLoader__.load({
|
|
|
452
491
|
ck_tight: 'Too close to a fixed bar',
|
|
453
492
|
ck_jserr: 'Script error',
|
|
454
493
|
ck_missing: 'File failed to load',
|
|
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',
|
|
455
500
|
cd_hscroll: 'sticks out {px}px',
|
|
456
501
|
cd_clipped: '{px}px cut off',
|
|
457
502
|
cd_overflow: 'sticks out {px}px',
|
|
@@ -468,6 +513,16 @@ window.__ModuleLoader__.load({
|
|
|
468
513
|
cd_tight: 'only {px}px from the {edgeName} {bar} (leave 16px or more)',
|
|
469
514
|
cd_jserr: '{msg}{at}',
|
|
470
515
|
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
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',
|
|
471
526
|
ckEdge_top: 'top bar',
|
|
472
527
|
ckEdge_bottom: 'bottom bar',
|
|
473
528
|
ckWhen_top: 'before scrolling, ',
|
|
@@ -562,12 +617,31 @@ window.__ModuleLoader__.load({
|
|
|
562
617
|
styleJoin: '; ',
|
|
563
618
|
lang: 'en',
|
|
564
619
|
stylesHead: 'Start from a style',
|
|
565
|
-
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',
|
|
566
621
|
stylesOpen: 'Try another style…',
|
|
567
622
|
stylesTitle: 'Change the style',
|
|
568
|
-
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',
|
|
569
640
|
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
570
|
-
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} (
|
|
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}',
|
|
642
|
+
accBig: 'graphics and large text only',
|
|
643
|
+
accBlock: 'graphics and color blocks only, not for text; text on those blocks uses {text}',
|
|
644
|
+
accShape: 'graphics only, not for text',
|
|
571
645
|
styleBgImage: '; on the background: {v}',
|
|
572
646
|
styleFonts: 'headings {head}; body {body}',
|
|
573
647
|
styleShape: 'radius {r}px; shadow {s}',
|
|
@@ -727,8 +801,8 @@ window.__ModuleLoader__.load({
|
|
|
727
801
|
.ddp-frame.cur{opacity:1;z-index:2;pointer-events:auto;transition:opacity .32s ease}
|
|
728
802
|
.ddp-dev{position:relative;background:#fff;box-shadow:0 0 0 .5px rgba(0,0,0,.14),0 18px 48px rgba(0,0,0,.2)}
|
|
729
803
|
.ddp-dev.fill{position:absolute;inset:0;box-shadow:none;border-radius:0}
|
|
730
|
-
.ddp-dev.k-app,.ddp-dev.k-tablet{background:#0e0e12;box-shadow:inset 0 0 0 1.5px #2b2b33,0 0 0 1px rgba(0,0,0,.45),0 24px 60px rgba(0,0,0,.3)}
|
|
731
|
-
.ddp-dev.fill.k-app,.ddp-dev.fill.k-tablet{background:#fff;box-shadow:none}
|
|
804
|
+
.ddp-dev.k-app,.ddp-dev.k-miniprogram,.ddp-dev.k-tablet{background:#0e0e12;box-shadow:inset 0 0 0 1.5px #2b2b33,0 0 0 1px rgba(0,0,0,.45),0 24px 60px rgba(0,0,0,.3)}
|
|
805
|
+
.ddp-dev.fill.k-app,.ddp-dev.fill.k-miniprogram,.ddp-dev.fill.k-tablet{background:#fff;box-shadow:none}
|
|
732
806
|
.ddp-vp{position:absolute;overflow:hidden;background:#fff;isolation:isolate}
|
|
733
807
|
.ddp-vp:has(.ddp-scr.dk),.ddp-scr.dk .ddp-frame{background:#111}
|
|
734
808
|
.ddp-chrome{position:absolute;left:0;right:0;top:0;align-items:center;gap:7px;padding:0 13px;background:#ebebef;border-bottom:1px solid rgba(0,0,0,.07);border-radius:inherit;border-bottom-left-radius:0;border-bottom-right-radius:0;overflow:hidden}
|
|
@@ -737,6 +811,19 @@ window.__ModuleLoader__.load({
|
|
|
737
811
|
.ddp-chrome i:nth-child(3){background:#28c840}
|
|
738
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}
|
|
739
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}
|
|
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}
|
|
820
|
+
.ddp-cap .pl{fill:rgba(255,255,255,.6);stroke:rgba(0,0,0,.08);stroke-width:.5}
|
|
821
|
+
.ddp-cap .sp{stroke:rgba(0,0,0,.15);stroke-width:.5}
|
|
822
|
+
.ddp-cap .rg{fill:none;stroke:#000;stroke-width:2}
|
|
823
|
+
.ddp-cap.w{fill:#fff}
|
|
824
|
+
.ddp-cap.w .pl{fill:rgba(0,0,0,.15);stroke:rgba(255,255,255,.25)}
|
|
825
|
+
.ddp-cap.w .sp{stroke:rgba(255,255,255,.25)}
|
|
826
|
+
.ddp-cap.w .rg{stroke:#fff}
|
|
740
827
|
.ddp-body{position:absolute;inset:0}
|
|
741
828
|
.ddp-body.vb{top:46px}
|
|
742
829
|
.ddp-single{position:absolute;inset:0;overflow:hidden}
|
|
@@ -900,14 +987,47 @@ window.__ModuleLoader__.load({
|
|
|
900
987
|
.ddp-style .cnt{padding:0 14px 6px;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
901
988
|
.ddp-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:10px 8px}
|
|
902
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}
|
|
903
|
-
.ddp-sc .pv{position:relative;display:block;height:
|
|
904
|
-
.ddp-sc:hover .pv{transform:translateY(-1px);box-shadow:0 0 0 1.5px ${ACCENT},0 6px 16px rgba(0,0,0,.12)}
|
|
905
|
-
.ddp-sc:
|
|
906
|
-
.ddp-sc
|
|
907
|
-
.ddp-sc .pv .
|
|
908
|
-
.ddp-
|
|
909
|
-
.ddp-
|
|
910
|
-
.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}
|
|
911
1031
|
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
912
1032
|
.ddp-tries{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:440px}
|
|
913
1033
|
.ddp-tries>span{flex-basis:100%;font-size:11px;line-height:16px}
|
|
@@ -1050,7 +1170,9 @@ window.__ModuleLoader__.load({
|
|
|
1050
1170
|
single: () => svg([h('rect', { key: 1, x: 5, y: 3.5, width: 14, height: 17, rx: 2.2 })]),
|
|
1051
1171
|
web: () => svg([h('rect', { key: 1, x: 3, y: 4.5, width: 18, height: 15, rx: 2.2 }), h('path', { key: 2, d: 'M3 9h18' }), h('path', { key: 3, d: 'M6 6.8h.01M8.5 6.8h.01' })]),
|
|
1052
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' })]),
|
|
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 })]),
|
|
1053
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' })]),
|
|
1054
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 })]),
|
|
1055
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' })]),
|
|
1056
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' })]),
|
|
@@ -1312,6 +1434,8 @@ window.__ModuleLoader__.load({
|
|
|
1312
1434
|
const SANS = '"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1313
1435
|
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1314
1436
|
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1437
|
+
const MONO = '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace'
|
|
1438
|
+
const GROTESK = '"Helvetica Neue", Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif'
|
|
1315
1439
|
const STYLES = [
|
|
1316
1440
|
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1317
1441
|
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
@@ -1359,8 +1483,117 @@ window.__ModuleLoader__.load({
|
|
|
1359
1483
|
head: ROUND, w: 700, radius: 18, shadow: '6px 6px 14px #C8CED6, -6px -6px 14px #FFFFFF',
|
|
1360
1484
|
nzh: '卡片和底同色,靠一亮一暗两道阴影凸起,按下用内阴影;容易看不清,所以主按钮一定用主色实底、文字够深;别整屏都是凸起',
|
|
1361
1485
|
nen: 'Cards share the background color and rise with a light and a dark shadow; pressed states use inset shadows; it gets low-contrast fast, so the main button is solid primary with strong text; don\'t make everything raised' },
|
|
1486
|
+
{ id: 'guochao', zh: '国潮', en: 'Guochao', dzh: '中国红配金、粗宋体、传统纹样', den: 'Chinese red and gold, bold Song type, traditional patterns',
|
|
1487
|
+
c: { bg: '#FBF3E4', surface: '#FFFAF0', text: '#2A1410', muted: '#6F4A3A', line: '#E8D3B5', primary: '#B8202A', accent: '#C8952E', on: '#FFF7E6', link: '#A31D26' },
|
|
1488
|
+
head: SERIF, w: 800, radius: 6, shadow: '0 6px 18px rgba(140,30,20,.12)',
|
|
1489
|
+
nzh: '中国红的大色块配金色细线;标题用粗宋体,可以竖排;祥云、回纹、印章这类纹样用 SVG 画,只做点缀别铺满;底色是米黄纸色,不用纯白',
|
|
1490
|
+
nen: 'Big Chinese-red blocks with fine gold lines; bold Song headings, vertical text allowed; clouds, fret borders and seal stamps drawn in SVG as accents only; rice-paper background, not pure white' },
|
|
1491
|
+
{ id: 'ink', zh: '中式雅致', en: 'Ink & paper', dzh: '宣纸底、墨色字、朱砂点缀', den: 'Paper white, ink-black type, a touch of cinnabar',
|
|
1492
|
+
c: { bg: '#F5F2EC', surface: '#FBF9F5', text: '#1E1E1C', muted: '#5F5B54', line: '#DAD4C8', primary: '#2F3B37', accent: '#8C2F2B', on: '#F5F2EC', link: '#8C2F2B' },
|
|
1493
|
+
head: SERIF, w: 500, radius: 4, shadow: 'none',
|
|
1494
|
+
nzh: '大量留白、细线分隔;标题细宋体、字距放开;朱砂红只像印章一样点一两处;插画用浅灰的水墨晕染(SVG 模糊色块),不用照片堆满',
|
|
1495
|
+
nen: 'Lots of space and hairline dividers; light Song headings with open tracking; cinnabar red only in one or two seal-like spots; illustrations as pale ink washes (blurred SVG shapes), not photo walls' },
|
|
1496
|
+
{ id: 'cyber', zh: '赛博朋克', en: 'Cyberpunk', dzh: '近黑底、品红青色霓虹、等宽字', den: 'Near-black, magenta and cyan neon, monospace',
|
|
1497
|
+
c: { bg: '#0B0B16', surface: '#151528', text: '#E9E6FF', muted: '#A09CC8', line: '#2E2B52', primary: '#FF2E88', accent: '#22E1FF', on: '#0B0B16', link: '#22E1FF' },
|
|
1498
|
+
head: MONO, w: 700, radius: 2, shadow: '0 0 0 1px #FF2E88, 0 0 18px rgba(255,46,136,.45)',
|
|
1499
|
+
nzh: '标题用等宽字、全大写,可以加一点故障错位(两层彩色 text-shadow);边框是 1px 霓虹线加外发光,发光只给主按钮和关键数字;背景可以叠细网格线;正文照样要清楚',
|
|
1500
|
+
nen: 'Monospace, all-caps headings with a slight glitch (two colored text-shadows); 1px neon borders with glow, glow only on the main button and key numbers; a fine grid on the background is fine; body text stays readable' },
|
|
1501
|
+
{ id: 'cute', zh: '可爱卡通', en: 'Cute', dzh: '粉黄糖果色、大圆角、圆体字', den: 'Candy pink and yellow, big radii, rounded type',
|
|
1502
|
+
c: { bg: '#FFF4F6', surface: '#FFFFFF', text: '#3A2540', muted: '#7A5E80', line: '#F3D3DD', primary: '#FF7AA8', accent: '#FFC93C', on: '#3A2540', link: '#C2366F' },
|
|
1503
|
+
head: ROUND, w: 800, radius: 24, shadow: '0 6px 0 #F3C4D3',
|
|
1504
|
+
nzh: '圆角很大、字胖胖的;按钮像糖果,底下一道同色系的厚阴影;配小动物或角色插画(SVG 简单几何画);粉色和黄色两种就够,主色按钮上用深色字',
|
|
1505
|
+
nen: 'Very round corners and chubby type; candy-like buttons with a thick same-hue shadow underneath; small animal or mascot illustrations (simple SVG shapes); pink and yellow are enough; dark text on the primary button' },
|
|
1506
|
+
{ id: 'retro', zh: '复古', en: 'Retro', dzh: '焦橙芥末黄墨绿、粗衬线、颗粒感', den: 'Burnt orange, mustard and teal, fat serif, grain',
|
|
1507
|
+
c: { bg: '#F3E6CF', surface: '#FBF2E2', text: '#2E1F14', muted: '#6A5240', line: '#DDC7A4', primary: '#B5471D', accent: '#E0A526', on: '#FFF6E8', link: '#2F5D50' },
|
|
1508
|
+
head: SERIF, w: 800, radius: 14, shadow: '0 4px 0 #2E1F14',
|
|
1509
|
+
nzh: '焦橙、芥末黄、墨绿三色;粗衬线大标题;彩虹条纹、圆形徽章做装饰;底色叠一层很淡的颗粒噪点(SVG feTurbulence);图片调成褪色暖调',
|
|
1510
|
+
nen: 'Burnt orange, mustard and teal; fat serif headlines; rainbow stripes and round badges as decoration; a very light grain over the background (SVG feTurbulence); faded warm photos' },
|
|
1511
|
+
{ id: 'sale', zh: '电商促销', en: 'Sale', dzh: '红橙主色、大价格、优惠券', den: 'Red-orange, big prices, coupons',
|
|
1512
|
+
c: { bg: '#FFF5F0', surface: '#FFFFFF', text: '#1F1A17', muted: '#6B5E57', line: '#F5DDD2', primary: '#CF2E0E', accent: '#FFB800', on: '#FFFFFF', link: '#C2290B' },
|
|
1513
|
+
head: SANS, w: 800, radius: 12, shadow: '0 4px 12px rgba(207,46,14,.12)',
|
|
1514
|
+
nzh: '价格用大号粗体数字(¥ 小一号),原价灰色划线;「限时」「包邮」这类小标签用色块;优惠券用金黄色、带锯齿边;倒计时醒目;信息多但分区清楚,主按钮通栏',
|
|
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' },
|
|
1362
1541
|
]
|
|
1363
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
|
+
|
|
1585
|
+
/** 两个 #RRGGBB 的对比度(WCAG) */
|
|
1586
|
+
function hexRatio(a, b) {
|
|
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] }
|
|
1588
|
+
const [x, y] = [l(a), l(b)].sort((p, q) => q - p)
|
|
1589
|
+
return (x + 0.05) / (y + 0.05)
|
|
1590
|
+
}
|
|
1591
|
+
/** 点缀色能怎么用:和底色、卡片都 ≥ 3:1 能写大字;不然只做色块(上面的字用正文色,要 ≥ 4.5:1)或只做图形 */
|
|
1592
|
+
function accentUse(c) {
|
|
1593
|
+
if (Math.min(hexRatio(c.accent, c.bg), hexRatio(c.accent, c.surface)) >= 3) return 'accBig'
|
|
1594
|
+
return hexRatio(c.text, c.accent) >= 4.5 ? 'accBlock' : 'accShape'
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1364
1597
|
/** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
|
|
1365
1598
|
function formatStyle(t, pre, restyle) {
|
|
1366
1599
|
const en = t('lang') === 'en'
|
|
@@ -1369,7 +1602,7 @@ window.__ModuleLoader__.load({
|
|
|
1369
1602
|
return t('styleText', {
|
|
1370
1603
|
name: en ? pre.en : pre.zh,
|
|
1371
1604
|
desc: en ? pre.den : pre.dzh,
|
|
1372
|
-
colors: t('styleColors', { ...c, bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1605
|
+
colors: t('styleColors', { ...c, accUse: t(accentUse(c), c), bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1373
1606
|
fonts,
|
|
1374
1607
|
shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
|
|
1375
1608
|
notes: en ? pre.nen : pre.nzh,
|
|
@@ -1474,6 +1707,8 @@ window.__ModuleLoader__.load({
|
|
|
1474
1707
|
otherText: String(it.otherText || '').slice(0, 20),
|
|
1475
1708
|
edgeName: edge ? t('ckEdge_' + edge) : '',
|
|
1476
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')) : '',
|
|
1477
1712
|
what: it.type === 'missing' ? t('ckWhat_' + (['css', 'script', 'font'].includes(it.what) ? it.what : 'other')) : '',
|
|
1478
1713
|
at: it.type === 'jserr' && it.src ? ` (${it.src}${it.line ? ':' + it.line : ''})` : '',
|
|
1479
1714
|
})
|
|
@@ -1533,9 +1768,9 @@ window.__ModuleLoader__.load({
|
|
|
1533
1768
|
|
|
1534
1769
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
1535
1770
|
|
|
1536
|
-
const SHOW_COLS = { app: 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 }
|
|
1537
1772
|
const CANVAS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1538
|
-
const FRAMED = { app: 1, tablet: 1, web: 1 }
|
|
1773
|
+
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, desktop: 1, web: 1 }
|
|
1539
1774
|
|
|
1540
1775
|
function loadImg(blob) {
|
|
1541
1776
|
return new Promise((resolve, reject) => {
|
|
@@ -1560,13 +1795,13 @@ window.__ModuleLoader__.load({
|
|
|
1560
1795
|
}
|
|
1561
1796
|
|
|
1562
1797
|
/** 在 (x, y) 画一个带外框的屏:w × ht 是屏幕本身;img 按宽铺进去,高出来的裁掉 */
|
|
1563
|
-
function drawDevice(ctx, kind, img, x, y, w, ht, label) {
|
|
1798
|
+
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite, os) {
|
|
1564
1799
|
const f = frameOf(kind, w, ht)
|
|
1565
1800
|
const pad = f.pad || 0
|
|
1566
1801
|
const top = f.top || 0
|
|
1567
1802
|
const r = f.r || 0
|
|
1568
1803
|
const [W, H] = frameSize(kind, w, ht)
|
|
1569
|
-
const dark = kind === 'app' || kind === 'tablet'
|
|
1804
|
+
const dark = kind === 'app' || kind === 'miniprogram' || kind === 'tablet'
|
|
1570
1805
|
ctx.save()
|
|
1571
1806
|
ctx.shadowColor = 'rgba(15, 18, 30, .22)'
|
|
1572
1807
|
ctx.shadowBlur = 48
|
|
@@ -1618,6 +1853,88 @@ window.__ModuleLoader__.load({
|
|
|
1618
1853
|
ctx.fillStyle = '#000'
|
|
1619
1854
|
ctx.fill()
|
|
1620
1855
|
}
|
|
1856
|
+
// 胶囊颜色用页面截图那一刻自己量的(和画布上一致);老页面没报就看截图
|
|
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()
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** 截图里胶囊那块是不是深色(导出时没有页面可问,看图) */
|
|
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) {
|
|
1903
|
+
try {
|
|
1904
|
+
const k = img.width / w
|
|
1905
|
+
const c = document.createElement('canvas')
|
|
1906
|
+
c.width = Math.max(1, Math.round(sw * k))
|
|
1907
|
+
c.height = Math.max(1, Math.round(sh * k))
|
|
1908
|
+
const g = c.getContext('2d')
|
|
1909
|
+
g.drawImage(img, sx * k, sy * k, sw * k, sh * k, 0, 0, c.width, c.height)
|
|
1910
|
+
const d = g.getImageData(0, 0, c.width, c.height).data
|
|
1911
|
+
let sum = 0
|
|
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]
|
|
1913
|
+
return sum / (d.length / 4) < 140
|
|
1914
|
+
} catch { return false }
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/** 小程序右上角的胶囊(「···」和关闭圈),和画布上 .ddp-cap 一样 */
|
|
1918
|
+
function drawCapsule(ctx, x, y, white) {
|
|
1919
|
+
const fg = white ? '#FFFFFF' : '#000000'
|
|
1920
|
+
ctx.save()
|
|
1921
|
+
rrect(ctx, x, y, CAP.w, CAP.h, CAP.h / 2)
|
|
1922
|
+
ctx.fillStyle = white ? 'rgba(0,0,0,.15)' : 'rgba(255,255,255,.6)'
|
|
1923
|
+
ctx.fill()
|
|
1924
|
+
rrect(ctx, x + 0.25, y + 0.25, CAP.w - 0.5, CAP.h - 0.5, CAP.h / 2)
|
|
1925
|
+
ctx.strokeStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.08)'
|
|
1926
|
+
ctx.lineWidth = 0.5
|
|
1927
|
+
ctx.stroke()
|
|
1928
|
+
ctx.fillStyle = fg
|
|
1929
|
+
;[[20.5, 2.2], [28, 3.1], [35.5, 2.2]].forEach(([cx, r]) => { ctx.beginPath(); ctx.arc(x + cx, y + 16, r, 0, Math.PI * 2); ctx.fill() })
|
|
1930
|
+
ctx.fillStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.15)'
|
|
1931
|
+
ctx.fillRect(x + 43.25, y + 9, 0.5, 14)
|
|
1932
|
+
ctx.strokeStyle = fg
|
|
1933
|
+
ctx.lineWidth = 2
|
|
1934
|
+
ctx.beginPath(); ctx.arc(x + 65, y + 16, 7.5, 0, Math.PI * 2); ctx.stroke()
|
|
1935
|
+
ctx.fillStyle = fg
|
|
1936
|
+
ctx.beginPath(); ctx.arc(x + 65, y + 16, 2.6, 0, Math.PI * 2); ctx.fill()
|
|
1937
|
+
ctx.restore()
|
|
1621
1938
|
}
|
|
1622
1939
|
|
|
1623
1940
|
function canvasBlob(canvas) {
|
|
@@ -1625,7 +1942,7 @@ window.__ModuleLoader__.load({
|
|
|
1625
1942
|
}
|
|
1626
1943
|
|
|
1627
1944
|
/** 一屏带外框(四周留出阴影的地方,背景透明) */
|
|
1628
|
-
async function composeOne(kind, size, img, label) {
|
|
1945
|
+
async function composeOne(kind, size, img, label, top, os) {
|
|
1629
1946
|
const [w, ht] = size
|
|
1630
1947
|
const [W, H] = frameSize(kind, w, ht)
|
|
1631
1948
|
const m = 56
|
|
@@ -1635,7 +1952,7 @@ window.__ModuleLoader__.load({
|
|
|
1635
1952
|
c.height = Math.round((H + m * 2) * S)
|
|
1636
1953
|
const ctx = c.getContext('2d')
|
|
1637
1954
|
ctx.scale(S, S)
|
|
1638
|
-
drawDevice(ctx, kind, img, m, m, w, ht, label)
|
|
1955
|
+
drawDevice(ctx, kind, img, m, m, w, ht, label, top, os)
|
|
1639
1956
|
return canvasBlob(c)
|
|
1640
1957
|
}
|
|
1641
1958
|
|
|
@@ -1789,7 +2106,7 @@ window.__ModuleLoader__.load({
|
|
|
1789
2106
|
* 整套展示图:几屏带外框排成网格,上面是品牌名和一行说明,铺一层底色 + 点缀色的光晕。
|
|
1790
2107
|
* shots: [{ img, label }]
|
|
1791
2108
|
*/
|
|
1792
|
-
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark }) {
|
|
2109
|
+
async function composeShowcase({ kind, size, shots, title, sub, bg, accent, mark, os }) {
|
|
1793
2110
|
const [w, ht] = size
|
|
1794
2111
|
const [DW, DH] = frameSize(kind, w, ht)
|
|
1795
2112
|
const u = Math.max(1, Math.min(2, DW / 430))
|
|
@@ -1850,7 +2167,7 @@ window.__ModuleLoader__.load({
|
|
|
1850
2167
|
const off = ((cols - inRow) * (DW + gap)) / 2 // 最后一行不满就居中
|
|
1851
2168
|
const x = pad + off + col * (DW + gap)
|
|
1852
2169
|
const yy = y + row * (DH + labH + gap * 0.6)
|
|
1853
|
-
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label)
|
|
2170
|
+
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top, os)
|
|
1854
2171
|
ctx.fillStyle = muted
|
|
1855
2172
|
ctx.font = `500 ${16 * u}px ${CANVAS_FONT}`
|
|
1856
2173
|
ctx.textAlign = 'center'
|
|
@@ -1927,16 +2244,18 @@ window.__ModuleLoader__.load({
|
|
|
1927
2244
|
|
|
1928
2245
|
// ── 设计类型:画幅和外框 ─────────────────────────────────────────────────
|
|
1929
2246
|
|
|
1930
|
-
const KIND_LIST = ['web', 'app', 'tablet', 'poster', 'slides', 'email']
|
|
1931
|
-
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', 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' }
|
|
1932
2249
|
const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
|
|
1933
2250
|
const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
|
|
1934
|
-
const KIND_SIZE = { web: [1280, 800], app: [390, 844], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
1935
|
-
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island
|
|
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 桌面软件的窗口按钮
|
|
1936
2253
|
const FRAME = {
|
|
1937
2254
|
web: { pad: 0, top: 34, r: 10, ir: 0 },
|
|
1938
2255
|
app: { pad: 11, top: 0, r: 55, ir: 44, island: 11 },
|
|
2256
|
+
miniprogram: { pad: 11, top: 0, r: 55, ir: 44, island: 11, capsule: 1 },
|
|
1939
2257
|
tablet: { pad: 14, top: 0, r: 34, ir: 21 },
|
|
2258
|
+
desktop: { pad: 0, top: 0, r: 12, ir: 12, win: 1 },
|
|
1940
2259
|
poster: { pad: 0, top: 0, r: 6, ir: 6 },
|
|
1941
2260
|
slides: { pad: 0, top: 0, r: 8, ir: 8 },
|
|
1942
2261
|
email: { pad: 0, top: 0, r: 10, ir: 10 },
|
|
@@ -1947,15 +2266,21 @@ window.__ModuleLoader__.load({
|
|
|
1947
2266
|
const f = FRAME[kind] || FRAME.web
|
|
1948
2267
|
return f.island && w && ht / w < 1.9 ? { ...f, island: 0 } : f
|
|
1949
2268
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2269
|
+
// 小程序胶囊:离屏幕顶 48(状态栏 44 + 4)、离右边 7、87×32(和页面体检 picker.js 的 CAP 一样)
|
|
2270
|
+
const CAP = { top: 48, right: 7, w: 87, h: 32 }
|
|
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' }
|
|
2275
|
+
|
|
2276
|
+
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" / "desktop windows" → { kind, size, os }(os:win,不写是 Mac) */
|
|
1953
2277
|
function parseMeta(content) {
|
|
1954
|
-
const out = { kind: '', size: '' }
|
|
2278
|
+
const out = { kind: '', size: '', os: '' }
|
|
1955
2279
|
for (const w of String(content || '').toLowerCase().split(/[\s,;]+/)) {
|
|
1956
2280
|
if (!out.kind && (KIND_LIST.includes(w) || META_ALIAS[w])) out.kind = KIND_LIST.includes(w) ? w : META_ALIAS[w]
|
|
1957
2281
|
const m = /^(\d{2,5})[x×*](\d{2,5})$/.exec(w)
|
|
1958
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'
|
|
1959
2284
|
}
|
|
1960
2285
|
return out
|
|
1961
2286
|
}
|
|
@@ -2023,7 +2348,9 @@ window.__ModuleLoader__.load({
|
|
|
2023
2348
|
const SIZE_SETS = {
|
|
2024
2349
|
web: [['szPhone', 'app', [390, 844]], ['szTablet', 'tablet', [820, 1180]], ['szDesktop', 'web', [1440, 900]]],
|
|
2025
2350
|
app: [['szSmall', 'app', [375, 667]], ['szStd', 'app', [393, 852]], ['szLarge', 'app', [430, 932]]],
|
|
2351
|
+
miniprogram: [['szSmall', 'miniprogram', [375, 667]], ['szStd', 'miniprogram', [375, 812]], ['szLarge', 'miniprogram', [414, 896]]],
|
|
2026
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]]],
|
|
2027
2354
|
}
|
|
2028
2355
|
|
|
2029
2356
|
/** 大小不一的几块排成一行(顶端对齐),算出整块大小和放得下的缩放 */
|
|
@@ -2414,10 +2741,10 @@ window.__ModuleLoader__.load({
|
|
|
2414
2741
|
}
|
|
2415
2742
|
|
|
2416
2743
|
/**
|
|
2417
|
-
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App
|
|
2418
|
-
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。
|
|
2744
|
+
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
|
|
2745
|
+
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
|
|
2419
2746
|
*/
|
|
2420
|
-
function Device({ kind, w, h: ht, fill, label, children }) {
|
|
2747
|
+
function Device({ kind, w, h: ht, fill, label, top, os, children }) {
|
|
2421
2748
|
const f = fill ? { pad: 0, top: 0, r: 0, ir: 0 } : frameOf(kind, w, ht)
|
|
2422
2749
|
return h('div', {
|
|
2423
2750
|
className: `ddp-dev k-${kind}` + (fill ? ' fill' : ''),
|
|
@@ -2425,7 +2752,31 @@ window.__ModuleLoader__.load({
|
|
|
2425
2752
|
},
|
|
2426
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 || '')),
|
|
2427
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),
|
|
2428
|
-
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } })
|
|
2755
|
+
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } }),
|
|
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 })))
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function Capsule({ white, style }) {
|
|
2775
|
+
return h('svg', { className: 'ddp-cap' + (white ? ' w' : ''), style, viewBox: `0 0 ${CAP.w} ${CAP.h}`, 'aria-hidden': true },
|
|
2776
|
+
h('rect', { className: 'pl', x: 0.25, y: 0.25, width: CAP.w - 0.5, height: CAP.h - 0.5, rx: CAP.h / 2 - 0.25 }),
|
|
2777
|
+
h('circle', { cx: 20.5, cy: 16, r: 2.2 }), h('circle', { cx: 28, cy: 16, r: 3.1 }), h('circle', { cx: 35.5, cy: 16, r: 2.2 }),
|
|
2778
|
+
h('path', { className: 'sp', d: 'M43.5 9v14' }),
|
|
2779
|
+
h('circle', { className: 'rg', cx: 65, cy: 16, r: 7.5 }), h('circle', { cx: 65, cy: 16, r: 2.6 }))
|
|
2429
2780
|
}
|
|
2430
2781
|
|
|
2431
2782
|
/**
|
|
@@ -2462,7 +2813,7 @@ window.__ModuleLoader__.load({
|
|
|
2462
2813
|
const rem = remOf(props)
|
|
2463
2814
|
const wins = useRef(new Map())
|
|
2464
2815
|
const life = useRef(null)
|
|
2465
|
-
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), snapTimer: null, gen: 0, at: '', deps: null }
|
|
2816
|
+
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), tops: new Map(), snapTimer: null, gen: 0, at: '', deps: null }
|
|
2466
2817
|
const curRef = useRef(0)
|
|
2467
2818
|
const P = useRef(props)
|
|
2468
2819
|
P.current = props
|
|
@@ -2571,10 +2922,12 @@ window.__ModuleLoader__.load({
|
|
|
2571
2922
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
2572
2923
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2573
2924
|
if (p.styleEdit) styleTo(id, p)
|
|
2925
|
+
if (L.tops.has(id) && p.onTop) p.onTop(L.tops.get(id))
|
|
2574
2926
|
if (first && p.onShown) p.onShown()
|
|
2575
2927
|
if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
|
|
2576
2928
|
L.file = ''
|
|
2577
2929
|
setTimeout(() => setFrames((fs) => fs.filter((f) => f.id >= curRef.current || f.id > id)), 450)
|
|
2930
|
+
for (const k of [...L.tops.keys()]) if (k < id) L.tops.delete(k)
|
|
2578
2931
|
if (L.again) {
|
|
2579
2932
|
L.again = false
|
|
2580
2933
|
postTo(id, { type: 'snapshot' })
|
|
@@ -2588,6 +2941,7 @@ window.__ModuleLoader__.load({
|
|
|
2588
2941
|
L.again = false
|
|
2589
2942
|
L.seeds.clear()
|
|
2590
2943
|
L.seedOf.clear()
|
|
2944
|
+
L.tops.clear()
|
|
2591
2945
|
L.gen++
|
|
2592
2946
|
L.at = ''
|
|
2593
2947
|
L.deps = null
|
|
@@ -2639,6 +2993,11 @@ window.__ModuleLoader__.load({
|
|
|
2639
2993
|
case 'links':
|
|
2640
2994
|
if (p.onLinks && Array.isArray(d.links)) p.onLinks(d.links)
|
|
2641
2995
|
break
|
|
2996
|
+
case 'top':
|
|
2997
|
+
// 每帧记着自己的;只有正显示的那帧算数(预载的换上来时再用,旧帧晚到的不管)
|
|
2998
|
+
L.tops.set(id, !!d.dark)
|
|
2999
|
+
if (isCur && p.onTop) p.onTop(!!d.dark)
|
|
3000
|
+
break
|
|
2642
3001
|
case 'changed':
|
|
2643
3002
|
if (isCur) { L.file = d.file || L.file; load(d.seed) }
|
|
2644
3003
|
break
|
|
@@ -2933,16 +3292,71 @@ window.__ModuleLoader__.load({
|
|
|
2933
3292
|
h('button', { className: 'go', onClick: onSave, disabled: !n || !write.ok, title: write.tip }, h('span', null, t('styleSave')))))
|
|
2934
3293
|
}
|
|
2935
3294
|
|
|
2936
|
-
/**
|
|
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 */
|
|
2937
3323
|
function StyleCards({ t, onPick }) {
|
|
2938
3324
|
const en = t('lang') === 'en'
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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 })),
|
|
2946
3360
|
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
2947
3361
|
}
|
|
2948
3362
|
|
|
@@ -3427,6 +3841,8 @@ window.__ModuleLoader__.load({
|
|
|
3427
3841
|
const [flash, setFlash] = useState('')
|
|
3428
3842
|
const [reloadSeq, setReloadSeq] = useState(0)
|
|
3429
3843
|
const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
|
|
3844
|
+
const [tops, setTops] = useState({}) // 屏 → 顶栏右边是不是深色(小程序胶囊画白的)
|
|
3845
|
+
const setTop = (k, dk) => setTops((cur) => (cur[k] === dk ? cur : { ...cur, [k]: dk }))
|
|
3430
3846
|
const [sel, setSel] = useState('')
|
|
3431
3847
|
const [panel, setPanel] = useState(false)
|
|
3432
3848
|
const [styles, setStyles] = useState(false) // 换风格面板
|
|
@@ -3829,7 +4245,7 @@ window.__ModuleLoader__.load({
|
|
|
3829
4245
|
else change({ view: 'single' })
|
|
3830
4246
|
}
|
|
3831
4247
|
|
|
3832
|
-
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(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 },
|
|
3833
4249
|
h(Screen, { t, remote,
|
|
3834
4250
|
src: sc.url,
|
|
3835
4251
|
label: labelOf(sc),
|
|
@@ -3856,6 +4272,7 @@ window.__ModuleLoader__.load({
|
|
|
3856
4272
|
onNote: (n) => note(n, sc),
|
|
3857
4273
|
onMeta: (m, title, toks, dk, links) => remember(ik(sc), m, title, toks, dk, links),
|
|
3858
4274
|
onLinks: (links) => rememberLinks(ik(sc), links),
|
|
4275
|
+
onTop: (dk) => setTop(ik(sc), dk),
|
|
3859
4276
|
onBuilt: (title) => onBuilt(sc, title),
|
|
3860
4277
|
}))
|
|
3861
4278
|
|
|
@@ -3874,9 +4291,8 @@ window.__ModuleLoader__.load({
|
|
|
3874
4291
|
const captureOf = async (sc, full) => {
|
|
3875
4292
|
const api = await apiOf(sc)
|
|
3876
4293
|
const r = await api.capture({ full: !!full, scale: 2 })
|
|
3877
|
-
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [] }
|
|
4294
|
+
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [], top: typeof r.top === 'boolean' ? r.top : null }
|
|
3878
4295
|
}
|
|
3879
|
-
const shotOf = async (sc, full) => (await captureOf(sc, full)).img
|
|
3880
4296
|
|
|
3881
4297
|
const saveImage = async (name, blob) => {
|
|
3882
4298
|
const png = await blobB64(blob)
|
|
@@ -3920,7 +4336,7 @@ window.__ModuleLoader__.load({
|
|
|
3920
4336
|
if (mode === 'show') {
|
|
3921
4337
|
const shots = []
|
|
3922
4338
|
for (const sc of shown) {
|
|
3923
|
-
try { shots.push({ img:
|
|
4339
|
+
try { const c = await captureOf(sc, false); shots.push({ img: c.img, top: c.top, title: labelOf(sc) }) } catch (e) { console.warn('[dsh-design-studio] 截图失败', sc.key, e) }
|
|
3924
4340
|
}
|
|
3925
4341
|
if (!shots.length) throw new Error('no screens')
|
|
3926
4342
|
const sp = splitTitles(shots.map((x) => x.title))
|
|
@@ -3928,7 +4344,8 @@ window.__ModuleLoader__.load({
|
|
|
3928
4344
|
const blob = await composeShowcase({
|
|
3929
4345
|
kind,
|
|
3930
4346
|
size,
|
|
3931
|
-
shots: shots.map((x, i) => ({ img: x.img, label: sp.labels[i] || x.title })),
|
|
4347
|
+
shots: shots.map((x, i) => ({ img: x.img, top: x.top, label: sp.labels[i] || x.title })),
|
|
4348
|
+
os: meta.os,
|
|
3932
4349
|
title: sp.brand || (st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : st.label) || '',
|
|
3933
4350
|
sub: t('showSub', { n: shots.length, kind: t(KIND_KEY[kind]), date: ymd(new Date()) }),
|
|
3934
4351
|
bg: colors.bg,
|
|
@@ -3942,8 +4359,8 @@ window.__ModuleLoader__.load({
|
|
|
3942
4359
|
const sc = shown.find((x) => x.key === k) || shown[0]
|
|
3943
4360
|
if (!sc) throw new Error('no screen')
|
|
3944
4361
|
const base = prefix + sc.key.replace(/\.html?$/i, '') + suffix
|
|
3945
|
-
const img = await
|
|
3946
|
-
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc)))
|
|
4362
|
+
const { img, top } = await captureOf(sc, mode === 'long')
|
|
4363
|
+
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top, meta.os))
|
|
3947
4364
|
else {
|
|
3948
4365
|
const c = document.createElement('canvas')
|
|
3949
4366
|
c.width = img.width
|
|
@@ -4317,7 +4734,7 @@ window.__ModuleLoader__.load({
|
|
|
4317
4734
|
return h('div', { key: l.id, className: cls },
|
|
4318
4735
|
h('div', { className: 'ddp-single' },
|
|
4319
4736
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4320
|
-
h(Device, { kind, w: size[0], h: size[1], fill, label },
|
|
4737
|
+
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key], os: meta.os },
|
|
4321
4738
|
h(Screen, { t, remote,
|
|
4322
4739
|
src: urlOfKey(l.key),
|
|
4323
4740
|
label,
|
|
@@ -4334,6 +4751,7 @@ window.__ModuleLoader__.load({
|
|
|
4334
4751
|
onShown: () => demoShown(l.id),
|
|
4335
4752
|
onMeta: (m, title, toks, dk, links) => remember(l.key, m, title, toks, dk, links),
|
|
4336
4753
|
onLinks: (links) => rememberLinks(l.key, links),
|
|
4754
|
+
onTop: (dk) => setTop(l.key, dk),
|
|
4337
4755
|
})))))
|
|
4338
4756
|
}))
|
|
4339
4757
|
} else if (inMulti) {
|
|
@@ -4352,7 +4770,7 @@ window.__ModuleLoader__.load({
|
|
|
4352
4770
|
picking: picking || textEdit,
|
|
4353
4771
|
sel: '',
|
|
4354
4772
|
labelOf: (sc) => sc.label,
|
|
4355
|
-
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base) },
|
|
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 },
|
|
4356
4774
|
h(Screen, { t, remote,
|
|
4357
4775
|
src: base.url,
|
|
4358
4776
|
label: labelOf(base),
|
|
@@ -4370,6 +4788,7 @@ window.__ModuleLoader__.load({
|
|
|
4370
4788
|
onEdited: (d) => onEdited(base, d),
|
|
4371
4789
|
onEditDone: () => setTextEdit(false),
|
|
4372
4790
|
onNote: (n) => note(n, base),
|
|
4791
|
+
onTop: (dk) => setTop(ik(base) + '|' + i, dk),
|
|
4373
4792
|
})),
|
|
4374
4793
|
})
|
|
4375
4794
|
} else if (ver && cmp) {
|
|
@@ -4382,12 +4801,13 @@ window.__ModuleLoader__.load({
|
|
|
4382
4801
|
const [dw, dh] = deviceBox(kind, size)
|
|
4383
4802
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
4384
4803
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
4385
|
-
const tagAt = (side) => ({ top: kind === 'app' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
4804
|
+
const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
4386
4805
|
const screenOf = (sc) => h(Screen, { t, remote, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
4387
|
-
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk) })
|
|
4806
|
+
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk), onTop: (dk) => setTop(ik(sc), dk) })
|
|
4388
4807
|
body = h('div', { className: 'ddp-single' },
|
|
4389
4808
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4390
|
-
|
|
4809
|
+
// 胶囊那块露的是哪一边(把手拖过胶囊中间就是左边那一版),胶囊就跟哪一边的顶栏
|
|
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 },
|
|
4391
4811
|
h('div', { className: 'ddp-cmp' + (cmpDrag ? ' drag' : ''), ref: cmpRef },
|
|
4392
4812
|
h('div', { className: 'ly' }, has ? screenOf(old) : h('div', { className: 'miss' }, t('notInVer', { n: ver.n }))),
|
|
4393
4813
|
// 右边一层只露出把手右边那块:外层裁宽度,里面整屏往左挪(iframe 不变大小、不重排;clip-path 盖 iframe 有的内核画不出来)
|
|
@@ -4556,7 +4976,7 @@ window.__ModuleLoader__.load({
|
|
|
4556
4976
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
4557
4977
|
exports.inject = INJECT
|
|
4558
4978
|
// 测试用
|
|
4559
|
-
exports._test = { STYLES, formatStyle, 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 }
|
|
4560
4980
|
return module.exports
|
|
4561
4981
|
},
|
|
4562
4982
|
})
|