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/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: '点一个,让模型把现在的设计换成这个风格(先改 tokens.css,布局和文案不动)',
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}(只做图形、大字),链接和小字强调 {link}{bgImage}',
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 put its colors, fonts and shapes in the chat box, then say what to make',
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 and the model restyles the current design (tokens.css first; layout and copy stay)',
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} (graphics and large text only), links and small emphasis {link}{bgImage}',
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:58px;border-radius:10px;overflow:hidden;box-shadow:0 0 0 .5px rgba(127,127,127,.35);transition:transform .15s ${EASE},box-shadow .15s}
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:hover{color:var(--dsw-alias-label-primary)}
906
- .ddp-sc .pv b{position:absolute;left:8px;top:6px;font-size:17px;line-height:1}
907
- .ddp-sc .pv .bt{position:absolute;right:8px;top:8px;width:20px;height:9px}
908
- .ddp-sc .pv .cd{position:absolute;left:8px;right:8px;bottom:7px;height:20px}
909
- .ddp-sc .pv .ln{position:absolute;left:6px;top:6px;width:46%;height:3px;border-radius:2px;opacity:.85}
910
- .ddp-sc .pv .ln.s{top:12px;width:30%;opacity:.6}
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
- const META_ALIAS = { phone: 'app', mobile: 'app', ios: 'app', android: 'app', pad: 'tablet', ipad: 'tablet', social: 'poster', image: 'poster', slide: 'slides', deck: 'slides', ppt: 'slides', mail: 'email' }
1951
-
1952
- /** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" → { kind, size } */
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
- /** 一排风格卡:每张是个小样(底色、标题字、卡片、主色按钮),点了交给 onPick */
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
- return h('div', { className: 'ddp-styles' }, ...STYLES.map((x) => h('button', { key: x.id, className: 'ddp-sc', title: en ? x.den : x.dzh, onClick: () => onPick(x) },
2940
- h('span', { className: 'pv', style: { background: x.c.bg, backgroundImage: x.bgImage || 'none', color: x.c.text } },
2941
- h('b', { style: { fontFamily: x.head, fontWeight: x.w } }, en ? 'Aa' : '字'),
2942
- h('i', { className: 'bt', style: { background: x.c.primary, borderRadius: Math.min(x.radius, 6), boxShadow: x.id === 'playful' ? `1.5px 1.5px 0 ${x.c.line}` : 'none', outline: x.id === 'playful' ? `1px solid ${x.c.line}` : 'none' } }),
2943
- h('i', { className: 'cd', style: { background: x.id === 'glass' ? 'rgba(255,255,255,.6)' : x.c.surface, borderRadius: Math.min(x.radius, 8) / 2 + 2,
2944
- boxShadow: x.id === 'soft' ? '2px 2px 5px #C8CED6, -2px -2px 5px #FFFFFF' : x.id === 'playful' ? `2px 2px 0 ${x.c.line}` : x.id === 'editorial' ? `inset 0 1px 0 ${x.c.line}` : `0 0 0 .5px ${x.c.line}` } },
2945
- h('i', { className: 'ln', style: { background: x.c.text } }), h('i', { className: 'ln s', style: { background: x.c.muted } }))),
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: await shotOf(sc, false), title: labelOf(sc) }) } catch (e) { console.warn('[dsh-design-studio] 截图失败', sc.key, e) }
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 shotOf(sc, mode === 'long')
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
- now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now) },
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
  })