dsh-design-studio 0.8.0 → 0.9.0

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