dsh-design-studio 0.7.0 → 0.8.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,6 +83,7 @@ window.__ModuleLoader__.load({
83
83
  kind: '类型',
84
84
  kindWeb: '网页',
85
85
  kindApp: 'App',
86
+ kindMini: '小程序',
86
87
  kindTablet: '平板',
87
88
  kindPoster: '海报',
88
89
  kindSlides: '幻灯片',
@@ -139,6 +140,7 @@ window.__ModuleLoader__.load({
139
140
  ck_tight: '离固定栏太近',
140
141
  ck_jserr: '页面脚本报错',
141
142
  ck_missing: '文件没载出来',
143
+ ck_capsule: '被小程序胶囊挡住',
142
144
  cd_hscroll: '伸出屏幕 {px}px',
143
145
  cd_clipped: '被切掉 {px}px',
144
146
  cd_overflow: '伸出框 {px}px',
@@ -155,6 +157,7 @@ window.__ModuleLoader__.load({
155
157
  cd_tight: '离{edgeName} {bar} 只有 {px}px(留 16px 以上)',
156
158
  cd_jserr: '{msg}{at}',
157
159
  cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
160
+ cd_capsule: '伸进右上角胶囊 {px}px(胶囊是微信画的、盖不住:顶栏右边从 x={x} 起空着)',
158
161
  ckEdge_top: '顶栏',
159
162
  ckEdge_bottom: '底栏',
160
163
  ckWhen_top: '没滚时',
@@ -254,7 +257,10 @@ window.__ModuleLoader__.load({
254
257
  stylesTitle: '换个风格',
255
258
  stylesSwap: '点一个,让模型把现在的设计换成这个风格(先改 tokens.css,布局和文案不动)',
256
259
  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}',
260
+ styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}({accUse}),链接和小字强调 {link}{bgImage}',
261
+ accBig: '图形、大字',
262
+ accBlock: '只做图形和色块,别拿来写字;色块上的字用 {text}',
263
+ accShape: '只做图形,别拿来写字',
258
264
  styleBgImage: ';底色上叠 {v}',
259
265
  styleFonts: '标题 {head};正文 {body}',
260
266
  styleShape: '圆角 {r}px;阴影 {s}',
@@ -396,6 +402,7 @@ window.__ModuleLoader__.load({
396
402
  kind: 'Type',
397
403
  kindWeb: 'Web',
398
404
  kindApp: 'App',
405
+ kindMini: 'Mini program',
399
406
  kindTablet: 'Tablet',
400
407
  kindPoster: 'Poster',
401
408
  kindSlides: 'Slides',
@@ -452,6 +459,7 @@ window.__ModuleLoader__.load({
452
459
  ck_tight: 'Too close to a fixed bar',
453
460
  ck_jserr: 'Script error',
454
461
  ck_missing: 'File failed to load',
462
+ ck_capsule: 'Under the mini program capsule',
455
463
  cd_hscroll: 'sticks out {px}px',
456
464
  cd_clipped: '{px}px cut off',
457
465
  cd_overflow: 'sticks out {px}px',
@@ -468,6 +476,7 @@ window.__ModuleLoader__.load({
468
476
  cd_tight: 'only {px}px from the {edgeName} {bar} (leave 16px or more)',
469
477
  cd_jserr: '{msg}{at}',
470
478
  cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
479
+ cd_capsule: '{px}px under the top-right capsule (WeChat draws it on top; keep the top bar clear from x={x})',
471
480
  ckEdge_top: 'top bar',
472
481
  ckEdge_bottom: 'bottom bar',
473
482
  ckWhen_top: 'before scrolling, ',
@@ -567,7 +576,10 @@ window.__ModuleLoader__.load({
567
576
  stylesTitle: 'Change the style',
568
577
  stylesSwap: 'Pick one and the model restyles the current design (tokens.css first; layout and copy stay)',
569
578
  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}',
579
+ 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
+ accBig: 'graphics and large text only',
581
+ accBlock: 'graphics and color blocks only, not for text; text on those blocks uses {text}',
582
+ accShape: 'graphics only, not for text',
571
583
  styleBgImage: '; on the background: {v}',
572
584
  styleFonts: 'headings {head}; body {body}',
573
585
  styleShape: 'radius {r}px; shadow {s}',
@@ -727,8 +739,8 @@ window.__ModuleLoader__.load({
727
739
  .ddp-frame.cur{opacity:1;z-index:2;pointer-events:auto;transition:opacity .32s ease}
728
740
  .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
741
  .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}
742
+ .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)}
743
+ .ddp-dev.fill.k-app,.ddp-dev.fill.k-miniprogram,.ddp-dev.fill.k-tablet{background:#fff;box-shadow:none}
732
744
  .ddp-vp{position:absolute;overflow:hidden;background:#fff;isolation:isolate}
733
745
  .ddp-vp:has(.ddp-scr.dk),.ddp-scr.dk .ddp-frame{background:#111}
734
746
  .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 +749,14 @@ window.__ModuleLoader__.load({
737
749
  .ddp-chrome i:nth-child(3){background:#28c840}
738
750
  .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
751
  .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
+ .ddp-cap{position:absolute;width:87px;height:32px;z-index:4;pointer-events:none;overflow:visible;fill:#000}
753
+ .ddp-cap .pl{fill:rgba(255,255,255,.6);stroke:rgba(0,0,0,.08);stroke-width:.5}
754
+ .ddp-cap .sp{stroke:rgba(0,0,0,.15);stroke-width:.5}
755
+ .ddp-cap .rg{fill:none;stroke:#000;stroke-width:2}
756
+ .ddp-cap.w{fill:#fff}
757
+ .ddp-cap.w .pl{fill:rgba(0,0,0,.15);stroke:rgba(255,255,255,.25)}
758
+ .ddp-cap.w .sp{stroke:rgba(255,255,255,.25)}
759
+ .ddp-cap.w .rg{stroke:#fff}
740
760
  .ddp-body{position:absolute;inset:0}
741
761
  .ddp-body.vb{top:46px}
742
762
  .ddp-single{position:absolute;inset:0;overflow:hidden}
@@ -1050,6 +1070,7 @@ window.__ModuleLoader__.load({
1050
1070
  single: () => svg([h('rect', { key: 1, x: 5, y: 3.5, width: 14, height: 17, rx: 2.2 })]),
1051
1071
  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
1072
  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
+ 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
1074
  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' })]),
1054
1075
  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
1076
  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' })]),
@@ -1312,6 +1333,7 @@ window.__ModuleLoader__.load({
1312
1333
  const SANS = '"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", system-ui, sans-serif'
1313
1334
  const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
1314
1335
  const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
1336
+ const MONO = '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace'
1315
1337
  const STYLES = [
1316
1338
  { id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
1317
1339
  c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
@@ -1359,8 +1381,50 @@ window.__ModuleLoader__.load({
1359
1381
  head: ROUND, w: 700, radius: 18, shadow: '6px 6px 14px #C8CED6, -6px -6px 14px #FFFFFF',
1360
1382
  nzh: '卡片和底同色,靠一亮一暗两道阴影凸起,按下用内阴影;容易看不清,所以主按钮一定用主色实底、文字够深;别整屏都是凸起',
1361
1383
  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' },
1384
+ { id: 'guochao', zh: '国潮', en: 'Guochao', dzh: '中国红配金、粗宋体、传统纹样', den: 'Chinese red and gold, bold Song type, traditional patterns',
1385
+ c: { bg: '#FBF3E4', surface: '#FFFAF0', text: '#2A1410', muted: '#6F4A3A', line: '#E8D3B5', primary: '#B8202A', accent: '#C8952E', on: '#FFF7E6', link: '#A31D26' },
1386
+ head: SERIF, w: 800, radius: 6, shadow: '0 6px 18px rgba(140,30,20,.12)',
1387
+ nzh: '中国红的大色块配金色细线;标题用粗宋体,可以竖排;祥云、回纹、印章这类纹样用 SVG 画,只做点缀别铺满;底色是米黄纸色,不用纯白',
1388
+ 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' },
1389
+ { id: 'ink', zh: '中式雅致', en: 'Ink & paper', dzh: '宣纸底、墨色字、朱砂点缀', den: 'Paper white, ink-black type, a touch of cinnabar',
1390
+ c: { bg: '#F5F2EC', surface: '#FBF9F5', text: '#1E1E1C', muted: '#5F5B54', line: '#DAD4C8', primary: '#2F3B37', accent: '#8C2F2B', on: '#F5F2EC', link: '#8C2F2B' },
1391
+ head: SERIF, w: 500, radius: 4, shadow: 'none',
1392
+ nzh: '大量留白、细线分隔;标题细宋体、字距放开;朱砂红只像印章一样点一两处;插画用浅灰的水墨晕染(SVG 模糊色块),不用照片堆满',
1393
+ 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' },
1394
+ { id: 'cyber', zh: '赛博朋克', en: 'Cyberpunk', dzh: '近黑底、品红青色霓虹、等宽字', den: 'Near-black, magenta and cyan neon, monospace',
1395
+ c: { bg: '#0B0B16', surface: '#151528', text: '#E9E6FF', muted: '#A09CC8', line: '#2E2B52', primary: '#FF2E88', accent: '#22E1FF', on: '#0B0B16', link: '#22E1FF' },
1396
+ head: MONO, w: 700, radius: 2, shadow: '0 0 0 1px #FF2E88, 0 0 18px rgba(255,46,136,.45)',
1397
+ nzh: '标题用等宽字、全大写,可以加一点故障错位(两层彩色 text-shadow);边框是 1px 霓虹线加外发光,发光只给主按钮和关键数字;背景可以叠细网格线;正文照样要清楚',
1398
+ 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' },
1399
+ { id: 'cute', zh: '可爱卡通', en: 'Cute', dzh: '粉黄糖果色、大圆角、圆体字', den: 'Candy pink and yellow, big radii, rounded type',
1400
+ c: { bg: '#FFF4F6', surface: '#FFFFFF', text: '#3A2540', muted: '#7A5E80', line: '#F3D3DD', primary: '#FF7AA8', accent: '#FFC93C', on: '#3A2540', link: '#C2366F' },
1401
+ head: ROUND, w: 800, radius: 24, shadow: '0 6px 0 #F3C4D3',
1402
+ nzh: '圆角很大、字胖胖的;按钮像糖果,底下一道同色系的厚阴影;配小动物或角色插画(SVG 简单几何画);粉色和黄色两种就够,主色按钮上用深色字',
1403
+ 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' },
1404
+ { id: 'retro', zh: '复古', en: 'Retro', dzh: '焦橙芥末黄墨绿、粗衬线、颗粒感', den: 'Burnt orange, mustard and teal, fat serif, grain',
1405
+ c: { bg: '#F3E6CF', surface: '#FBF2E2', text: '#2E1F14', muted: '#6A5240', line: '#DDC7A4', primary: '#B5471D', accent: '#E0A526', on: '#FFF6E8', link: '#2F5D50' },
1406
+ head: SERIF, w: 800, radius: 14, shadow: '0 4px 0 #2E1F14',
1407
+ nzh: '焦橙、芥末黄、墨绿三色;粗衬线大标题;彩虹条纹、圆形徽章做装饰;底色叠一层很淡的颗粒噪点(SVG feTurbulence);图片调成褪色暖调',
1408
+ 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' },
1409
+ { id: 'sale', zh: '电商促销', en: 'Sale', dzh: '红橙主色、大价格、优惠券', den: 'Red-orange, big prices, coupons',
1410
+ c: { bg: '#FFF5F0', surface: '#FFFFFF', text: '#1F1A17', muted: '#6B5E57', line: '#F5DDD2', primary: '#CF2E0E', accent: '#FFB800', on: '#FFFFFF', link: '#C2290B' },
1411
+ head: SANS, w: 800, radius: 12, shadow: '0 4px 12px rgba(207,46,14,.12)',
1412
+ nzh: '价格用大号粗体数字(¥ 小一号),原价灰色划线;「限时」「包邮」这类小标签用色块;优惠券用金黄色、带锯齿边;倒计时醒目;信息多但分区清楚,主按钮通栏',
1413
+ 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' },
1362
1414
  ]
1363
1415
 
1416
+ /** 两个 #RRGGBB 的对比度(WCAG) */
1417
+ function hexRatio(a, b) {
1418
+ 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] }
1419
+ const [x, y] = [l(a), l(b)].sort((p, q) => q - p)
1420
+ return (x + 0.05) / (y + 0.05)
1421
+ }
1422
+ /** 点缀色能怎么用:和底色、卡片都 ≥ 3:1 能写大字;不然只做色块(上面的字用正文色,要 ≥ 4.5:1)或只做图形 */
1423
+ function accentUse(c) {
1424
+ if (Math.min(hexRatio(c.accent, c.bg), hexRatio(c.accent, c.surface)) >= 3) return 'accBig'
1425
+ return hexRatio(c.text, c.accent) >= 4.5 ? 'accBlock' : 'accShape'
1426
+ }
1427
+
1364
1428
  /** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
1365
1429
  function formatStyle(t, pre, restyle) {
1366
1430
  const en = t('lang') === 'en'
@@ -1369,7 +1433,7 @@ window.__ModuleLoader__.load({
1369
1433
  return t('styleText', {
1370
1434
  name: en ? pre.en : pre.zh,
1371
1435
  desc: en ? pre.den : pre.dzh,
1372
- colors: t('styleColors', { ...c, bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
1436
+ colors: t('styleColors', { ...c, accUse: t(accentUse(c), c), bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
1373
1437
  fonts,
1374
1438
  shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
1375
1439
  notes: en ? pre.nen : pre.nzh,
@@ -1533,9 +1597,9 @@ window.__ModuleLoader__.load({
1533
1597
 
1534
1598
  // ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
1535
1599
 
1536
- const SHOW_COLS = { app: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
1600
+ const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
1537
1601
  const CANVAS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif'
1538
- const FRAMED = { app: 1, tablet: 1, web: 1 }
1602
+ const FRAMED = { app: 1, miniprogram: 1, tablet: 1, web: 1 }
1539
1603
 
1540
1604
  function loadImg(blob) {
1541
1605
  return new Promise((resolve, reject) => {
@@ -1560,13 +1624,13 @@ window.__ModuleLoader__.load({
1560
1624
  }
1561
1625
 
1562
1626
  /** 在 (x, y) 画一个带外框的屏:w × ht 是屏幕本身;img 按宽铺进去,高出来的裁掉 */
1563
- function drawDevice(ctx, kind, img, x, y, w, ht, label) {
1627
+ function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite) {
1564
1628
  const f = frameOf(kind, w, ht)
1565
1629
  const pad = f.pad || 0
1566
1630
  const top = f.top || 0
1567
1631
  const r = f.r || 0
1568
1632
  const [W, H] = frameSize(kind, w, ht)
1569
- const dark = kind === 'app' || kind === 'tablet'
1633
+ const dark = kind === 'app' || kind === 'miniprogram' || kind === 'tablet'
1570
1634
  ctx.save()
1571
1635
  ctx.shadowColor = 'rgba(15, 18, 30, .22)'
1572
1636
  ctx.shadowBlur = 48
@@ -1618,6 +1682,47 @@ window.__ModuleLoader__.load({
1618
1682
  ctx.fillStyle = '#000'
1619
1683
  ctx.fill()
1620
1684
  }
1685
+ // 胶囊颜色用页面截图那一刻自己量的(和画布上一致);老页面没报就看截图
1686
+ if (f.capsule) drawCapsule(ctx, x + pad + w - CAP.right - CAP.w, y + pad + CAP.top, typeof capWhite === 'boolean' ? capWhite : !!img && capDarkOf(img, w))
1687
+ }
1688
+
1689
+ /** 截图里胶囊那块是不是深色(导出时没有页面可问,看图) */
1690
+ function capDarkOf(img, w) {
1691
+ try {
1692
+ const k = img.width / w
1693
+ 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))
1696
+ 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)
1698
+ const d = g.getImageData(0, 0, c.width, c.height).data
1699
+ let sum = 0
1700
+ for (let i = 0; i < d.length; i += 4) sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]
1701
+ return sum / (d.length / 4) < 140
1702
+ } catch { return false }
1703
+ }
1704
+
1705
+ /** 小程序右上角的胶囊(「···」和关闭圈),和画布上 .ddp-cap 一样 */
1706
+ function drawCapsule(ctx, x, y, white) {
1707
+ const fg = white ? '#FFFFFF' : '#000000'
1708
+ ctx.save()
1709
+ rrect(ctx, x, y, CAP.w, CAP.h, CAP.h / 2)
1710
+ ctx.fillStyle = white ? 'rgba(0,0,0,.15)' : 'rgba(255,255,255,.6)'
1711
+ ctx.fill()
1712
+ rrect(ctx, x + 0.25, y + 0.25, CAP.w - 0.5, CAP.h - 0.5, CAP.h / 2)
1713
+ ctx.strokeStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.08)'
1714
+ ctx.lineWidth = 0.5
1715
+ ctx.stroke()
1716
+ ctx.fillStyle = fg
1717
+ ;[[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() })
1718
+ ctx.fillStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.15)'
1719
+ ctx.fillRect(x + 43.25, y + 9, 0.5, 14)
1720
+ ctx.strokeStyle = fg
1721
+ ctx.lineWidth = 2
1722
+ ctx.beginPath(); ctx.arc(x + 65, y + 16, 7.5, 0, Math.PI * 2); ctx.stroke()
1723
+ ctx.fillStyle = fg
1724
+ ctx.beginPath(); ctx.arc(x + 65, y + 16, 2.6, 0, Math.PI * 2); ctx.fill()
1725
+ ctx.restore()
1621
1726
  }
1622
1727
 
1623
1728
  function canvasBlob(canvas) {
@@ -1625,7 +1730,7 @@ window.__ModuleLoader__.load({
1625
1730
  }
1626
1731
 
1627
1732
  /** 一屏带外框(四周留出阴影的地方,背景透明) */
1628
- async function composeOne(kind, size, img, label) {
1733
+ async function composeOne(kind, size, img, label, top) {
1629
1734
  const [w, ht] = size
1630
1735
  const [W, H] = frameSize(kind, w, ht)
1631
1736
  const m = 56
@@ -1635,7 +1740,7 @@ window.__ModuleLoader__.load({
1635
1740
  c.height = Math.round((H + m * 2) * S)
1636
1741
  const ctx = c.getContext('2d')
1637
1742
  ctx.scale(S, S)
1638
- drawDevice(ctx, kind, img, m, m, w, ht, label)
1743
+ drawDevice(ctx, kind, img, m, m, w, ht, label, top)
1639
1744
  return canvasBlob(c)
1640
1745
  }
1641
1746
 
@@ -1850,7 +1955,7 @@ window.__ModuleLoader__.load({
1850
1955
  const off = ((cols - inRow) * (DW + gap)) / 2 // 最后一行不满就居中
1851
1956
  const x = pad + off + col * (DW + gap)
1852
1957
  const yy = y + row * (DH + labH + gap * 0.6)
1853
- drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label)
1958
+ drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top)
1854
1959
  ctx.fillStyle = muted
1855
1960
  ctx.font = `500 ${16 * u}px ${CANVAS_FONT}`
1856
1961
  ctx.textAlign = 'center'
@@ -1927,15 +2032,16 @@ window.__ModuleLoader__.load({
1927
2032
 
1928
2033
  // ── 设计类型:画幅和外框 ─────────────────────────────────────────────────
1929
2034
 
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' }
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' }
1932
2037
  const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
1933
2038
  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 灵动岛离顶的距离
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 小程序右上角的胶囊
1936
2041
  const FRAME = {
1937
2042
  web: { pad: 0, top: 34, r: 10, ir: 0 },
1938
2043
  app: { pad: 11, top: 0, r: 55, ir: 44, island: 11 },
2044
+ miniprogram: { pad: 11, top: 0, r: 55, ir: 44, island: 11, capsule: 1 },
1939
2045
  tablet: { pad: 14, top: 0, r: 34, ir: 21 },
1940
2046
  poster: { pad: 0, top: 0, r: 6, ir: 6 },
1941
2047
  slides: { pad: 0, top: 0, r: 8, ir: 8 },
@@ -1947,7 +2053,9 @@ window.__ModuleLoader__.load({
1947
2053
  const f = FRAME[kind] || FRAME.web
1948
2054
  return f.island && w && ht / w < 1.9 ? { ...f, island: 0 } : f
1949
2055
  }
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' }
2056
+ // 小程序胶囊:离屏幕顶 48(状态栏 44 + 4)、离右边 7、87×32(和页面体检 picker.js 的 CAP 一样)
2057
+ 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' }
1951
2059
 
1952
2060
  /** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" → { kind, size } */
1953
2061
  function parseMeta(content) {
@@ -2023,6 +2131,7 @@ window.__ModuleLoader__.load({
2023
2131
  const SIZE_SETS = {
2024
2132
  web: [['szPhone', 'app', [390, 844]], ['szTablet', 'tablet', [820, 1180]], ['szDesktop', 'web', [1440, 900]]],
2025
2133
  app: [['szSmall', 'app', [375, 667]], ['szStd', 'app', [393, 852]], ['szLarge', 'app', [430, 932]]],
2134
+ miniprogram: [['szSmall', 'miniprogram', [375, 667]], ['szStd', 'miniprogram', [375, 812]], ['szLarge', 'miniprogram', [414, 896]]],
2026
2135
  tablet: [['szSmall', 'tablet', [744, 1133]], ['szStd', 'tablet', [820, 1180]], ['szLarge', 'tablet', [1032, 1376]]],
2027
2136
  }
2028
2137
 
@@ -2414,10 +2523,10 @@ window.__ModuleLoader__.load({
2414
2523
  }
2415
2524
 
2416
2525
  /**
2417
- * 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛),海报 / 幻灯片 / 邮件是一张卡片。
2418
- * fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。
2526
+ * 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
2527
+ * fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
2419
2528
  */
2420
- function Device({ kind, w, h: ht, fill, label, children }) {
2529
+ function Device({ kind, w, h: ht, fill, label, top, children }) {
2421
2530
  const f = fill ? { pad: 0, top: 0, r: 0, ir: 0 } : frameOf(kind, w, ht)
2422
2531
  return h('div', {
2423
2532
  className: `ddp-dev k-${kind}` + (fill ? ' fill' : ''),
@@ -2425,7 +2534,16 @@ window.__ModuleLoader__.load({
2425
2534
  },
2426
2535
  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
2536
  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) } }))
2537
+ 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)
2539
+ }
2540
+
2541
+ function Capsule({ white, style }) {
2542
+ return h('svg', { className: 'ddp-cap' + (white ? ' w' : ''), style, viewBox: `0 0 ${CAP.w} ${CAP.h}`, 'aria-hidden': true },
2543
+ 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 }),
2544
+ 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 }),
2545
+ h('path', { className: 'sp', d: 'M43.5 9v14' }),
2546
+ h('circle', { className: 'rg', cx: 65, cy: 16, r: 7.5 }), h('circle', { cx: 65, cy: 16, r: 2.6 }))
2429
2547
  }
2430
2548
 
2431
2549
  /**
@@ -2462,7 +2580,7 @@ window.__ModuleLoader__.load({
2462
2580
  const rem = remOf(props)
2463
2581
  const wins = useRef(new Map())
2464
2582
  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 }
2583
+ 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
2584
  const curRef = useRef(0)
2467
2585
  const P = useRef(props)
2468
2586
  P.current = props
@@ -2571,10 +2689,12 @@ window.__ModuleLoader__.load({
2571
2689
  if (p.demo) postTo(id, { type: 'demo', on: true })
2572
2690
  if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
2573
2691
  if (p.styleEdit) styleTo(id, p)
2692
+ if (L.tops.has(id) && p.onTop) p.onTop(L.tops.get(id))
2574
2693
  if (first && p.onShown) p.onShown()
2575
2694
  if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
2576
2695
  L.file = ''
2577
2696
  setTimeout(() => setFrames((fs) => fs.filter((f) => f.id >= curRef.current || f.id > id)), 450)
2697
+ for (const k of [...L.tops.keys()]) if (k < id) L.tops.delete(k)
2578
2698
  if (L.again) {
2579
2699
  L.again = false
2580
2700
  postTo(id, { type: 'snapshot' })
@@ -2588,6 +2708,7 @@ window.__ModuleLoader__.load({
2588
2708
  L.again = false
2589
2709
  L.seeds.clear()
2590
2710
  L.seedOf.clear()
2711
+ L.tops.clear()
2591
2712
  L.gen++
2592
2713
  L.at = ''
2593
2714
  L.deps = null
@@ -2639,6 +2760,11 @@ window.__ModuleLoader__.load({
2639
2760
  case 'links':
2640
2761
  if (p.onLinks && Array.isArray(d.links)) p.onLinks(d.links)
2641
2762
  break
2763
+ case 'top':
2764
+ // 每帧记着自己的;只有正显示的那帧算数(预载的换上来时再用,旧帧晚到的不管)
2765
+ L.tops.set(id, !!d.dark)
2766
+ if (isCur && p.onTop) p.onTop(!!d.dark)
2767
+ break
2642
2768
  case 'changed':
2643
2769
  if (isCur) { L.file = d.file || L.file; load(d.seed) }
2644
2770
  break
@@ -2939,9 +3065,10 @@ window.__ModuleLoader__.load({
2939
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) },
2940
3066
  h('span', { className: 'pv', style: { background: x.c.bg, backgroundImage: x.bgImage || 'none', color: x.c.text } },
2941
3067
  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' } }),
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' } }),
2943
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,
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}` } },
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}` } },
2945
3072
  h('i', { className: 'ln', style: { background: x.c.text } }), h('i', { className: 'ln s', style: { background: x.c.muted } }))),
2946
3073
  h('span', { className: 'nm' }, en ? x.en : x.zh))))
2947
3074
  }
@@ -3427,6 +3554,8 @@ window.__ModuleLoader__.load({
3427
3554
  const [flash, setFlash] = useState('')
3428
3555
  const [reloadSeq, setReloadSeq] = useState(0)
3429
3556
  const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
3557
+ const [tops, setTops] = useState({}) // 屏 → 顶栏右边是不是深色(小程序胶囊画白的)
3558
+ const setTop = (k, dk) => setTops((cur) => (cur[k] === dk ? cur : { ...cur, [k]: dk }))
3430
3559
  const [sel, setSel] = useState('')
3431
3560
  const [panel, setPanel] = useState(false)
3432
3561
  const [styles, setStyles] = useState(false) // 换风格面板
@@ -3829,7 +3958,7 @@ window.__ModuleLoader__.load({
3829
3958
  else change({ view: 'single' })
3830
3959
  }
3831
3960
 
3832
- const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc) },
3961
+ const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)] },
3833
3962
  h(Screen, { t, remote,
3834
3963
  src: sc.url,
3835
3964
  label: labelOf(sc),
@@ -3856,6 +3985,7 @@ window.__ModuleLoader__.load({
3856
3985
  onNote: (n) => note(n, sc),
3857
3986
  onMeta: (m, title, toks, dk, links) => remember(ik(sc), m, title, toks, dk, links),
3858
3987
  onLinks: (links) => rememberLinks(ik(sc), links),
3988
+ onTop: (dk) => setTop(ik(sc), dk),
3859
3989
  onBuilt: (title) => onBuilt(sc, title),
3860
3990
  }))
3861
3991
 
@@ -3874,9 +4004,8 @@ window.__ModuleLoader__.load({
3874
4004
  const captureOf = async (sc, full) => {
3875
4005
  const api = await apiOf(sc)
3876
4006
  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 : [] }
4007
+ 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
4008
  }
3879
- const shotOf = async (sc, full) => (await captureOf(sc, full)).img
3880
4009
 
3881
4010
  const saveImage = async (name, blob) => {
3882
4011
  const png = await blobB64(blob)
@@ -3920,7 +4049,7 @@ window.__ModuleLoader__.load({
3920
4049
  if (mode === 'show') {
3921
4050
  const shots = []
3922
4051
  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) }
4052
+ 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
4053
  }
3925
4054
  if (!shots.length) throw new Error('no screens')
3926
4055
  const sp = splitTitles(shots.map((x) => x.title))
@@ -3928,7 +4057,7 @@ window.__ModuleLoader__.load({
3928
4057
  const blob = await composeShowcase({
3929
4058
  kind,
3930
4059
  size,
3931
- shots: shots.map((x, i) => ({ img: x.img, label: sp.labels[i] || x.title })),
4060
+ shots: shots.map((x, i) => ({ img: x.img, top: x.top, label: sp.labels[i] || x.title })),
3932
4061
  title: sp.brand || (st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : st.label) || '',
3933
4062
  sub: t('showSub', { n: shots.length, kind: t(KIND_KEY[kind]), date: ymd(new Date()) }),
3934
4063
  bg: colors.bg,
@@ -3942,8 +4071,8 @@ window.__ModuleLoader__.load({
3942
4071
  const sc = shown.find((x) => x.key === k) || shown[0]
3943
4072
  if (!sc) throw new Error('no screen')
3944
4073
  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)))
4074
+ const { img, top } = await captureOf(sc, mode === 'long')
4075
+ if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top))
3947
4076
  else {
3948
4077
  const c = document.createElement('canvas')
3949
4078
  c.width = img.width
@@ -4317,7 +4446,7 @@ window.__ModuleLoader__.load({
4317
4446
  return h('div', { key: l.id, className: cls },
4318
4447
  h('div', { className: 'ddp-single' },
4319
4448
  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 },
4449
+ h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key] },
4321
4450
  h(Screen, { t, remote,
4322
4451
  src: urlOfKey(l.key),
4323
4452
  label,
@@ -4334,6 +4463,7 @@ window.__ModuleLoader__.load({
4334
4463
  onShown: () => demoShown(l.id),
4335
4464
  onMeta: (m, title, toks, dk, links) => remember(l.key, m, title, toks, dk, links),
4336
4465
  onLinks: (links) => rememberLinks(l.key, links),
4466
+ onTop: (dk) => setTop(l.key, dk),
4337
4467
  })))))
4338
4468
  }))
4339
4469
  } else if (inMulti) {
@@ -4352,7 +4482,7 @@ window.__ModuleLoader__.load({
4352
4482
  picking: picking || textEdit,
4353
4483
  sel: '',
4354
4484
  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) },
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] },
4356
4486
  h(Screen, { t, remote,
4357
4487
  src: base.url,
4358
4488
  label: labelOf(base),
@@ -4370,6 +4500,7 @@ window.__ModuleLoader__.load({
4370
4500
  onEdited: (d) => onEdited(base, d),
4371
4501
  onEditDone: () => setTextEdit(false),
4372
4502
  onNote: (n) => note(n, base),
4503
+ onTop: (dk) => setTop(ik(base) + '|' + i, dk),
4373
4504
  })),
4374
4505
  })
4375
4506
  } else if (ver && cmp) {
@@ -4382,12 +4513,13 @@ window.__ModuleLoader__.load({
4382
4513
  const [dw, dh] = deviceBox(kind, size)
4383
4514
  const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
4384
4515
  // 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
4385
- const tagAt = (side) => ({ top: kind === 'app' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
4516
+ const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
4386
4517
  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) })
4518
+ 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
4519
  body = h('div', { className: 'ddp-single' },
4389
4520
  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) },
4521
+ // 胶囊那块露的是哪一边(把手拖过胶囊中间就是左边那一版),胶囊就跟哪一边的顶栏
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)] },
4391
4523
  h('div', { className: 'ddp-cmp' + (cmpDrag ? ' drag' : ''), ref: cmpRef },
4392
4524
  h('div', { className: 'ly' }, has ? screenOf(old) : h('div', { className: 'miss' }, t('notInVer', { n: ver.n }))),
4393
4525
  // 右边一层只露出把手右边那块:外层裁宽度,里面整屏往左挪(iframe 不变大小、不重排;clip-path 盖 iframe 有的内核画不出来)
@@ -4556,7 +4688,7 @@ window.__ModuleLoader__.load({
4556
4688
  // 注意:这里是客户端**服务名**(短的),不是包名
4557
4689
  exports.inject = INJECT
4558
4690
  // 测试用
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 }
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 }
4560
4692
  return module.exports
4561
4693
  },
4562
4694
  })
package/lib/core.js CHANGED
@@ -31,11 +31,12 @@ const JOB_READ_TOOL = /^(job_output|bash_output|read_job|job_read)$/i
31
31
  const BG_JOB = /\bbackground job\s+`?([\w.:-]{3,})/i
32
32
  const MAX_SCREENS = 24
33
33
 
34
- // 设计类型:决定右侧用什么画幅显示(网页框、手机、平板、海报、幻灯片、邮件)
35
- export const KINDS = ['web', 'app', 'tablet', 'poster', 'slides', 'email']
34
+ // 设计类型:决定右侧用什么画幅显示(网页框、手机、小程序、平板、海报、幻灯片、邮件)
35
+ export const KINDS = ['web', 'app', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
36
36
  const KIND_ALIAS = {
37
37
  website: 'web', site: 'web', page: 'web', landing: 'web', desktop: 'web', dashboard: 'web',
38
- phone: 'app', mobile: 'app', ios: 'app', android: 'app', miniapp: 'app',
38
+ phone: 'app', mobile: 'app', ios: 'app', android: 'app',
39
+ mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram',
39
40
  pad: 'tablet', ipad: 'tablet',
40
41
  social: 'poster', image: 'poster', banner: 'poster', card: 'poster', flyer: 'poster', cover: 'poster',
41
42
  slide: 'slides', deck: 'slides', ppt: 'slides', presentation: 'slides',
package/lib/handoff.js CHANGED
@@ -199,6 +199,13 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
199
199
  lines.push(`- 框架:${fwLabel}`)
200
200
  if (system) lines.push(`- 设计系统:${system}(设计用的是它的 tokens.css / components.css,项目里也照它)`)
201
201
  lines.push(`- 生成时间:${now.toISOString().slice(0, 16).replace('T', ' ')}`, '')
202
+ if (kind === 'miniprogram') {
203
+ lines.push('## 小程序要点', '')
204
+ lines.push(`- 这是微信小程序的设计(${size[0]} 宽)。要做成真小程序:原生写 WXML / WXSS,或用 Taro、uni-app;框架选的「${fwLabel}」是做成网页的写法,用户要的是哪种先问清。`)
205
+ lines.push(`- 尺寸换成 rpx:设计稿 1px = ${Math.round((750 / size[0]) * 100) / 100}rpx(整屏宽 750rpx)。`)
206
+ lines.push('- 设计稿顶上画的状态栏不用做,微信自带;右上角的胶囊也是微信画的。导航栏用 app.json / 页面 json 的默认导航栏,或 `navigationStyle: custom` 自己画——自己画时状态栏高用 `wx.getWindowInfo().statusBarHeight`、导航栏高度和右边让开的宽度按 `wx.getMenuButtonBoundingClientRect()` 算(设计稿的 44 / 88 / 101px 只是预览手机的)。顶栏是深色的(预览里胶囊是白的),页面配置写 `navigationBarTextStyle: white`。')
207
+ lines.push('- 底部标签栏写进 app.json 的 `tabBar`(样式特殊就用自定义 tabBar),底部留出安全区 `env(safe-area-inset-bottom)`。', '')
208
+ }
202
209
 
203
210
  lines.push('## 屏幕和路由', '', '| 屏 | 设计文件 | 建议路由 | 截图(handoff/shots/) | 跳到 |', '| --- | --- | --- | --- | --- |')
204
211
  for (const r of rows) {
package/lib/index.js CHANGED
@@ -117,7 +117,7 @@ export const previewTool = {
117
117
  name: 'preview_page',
118
118
  description:
119
119
  '在右侧栏「设计工作台」里实时打开一个页面,用户能边看边点选要改的地方。' +
120
- '做网页、App 界面、海报、幻灯片、邮件这类界面设计时用:写好第一个 HTML 文件、或启动开发服务器后调一次就行,之后改文件会自动刷新,不用再调。' +
120
+ '做网页、App 界面、小程序、海报、幻灯片、邮件这类界面设计时用:写好第一个 HTML 文件、或启动开发服务器后调一次就行,之后改文件会自动刷新,不用再调。' +
121
121
  '只是把回答做成一页(对比、表格、清单、图表、计算器)给用户看或用时,有 show_page 工具就用它,不用这里。' +
122
122
  '本地 HTML 写文件路径;开发服务器写地址或端口(如 http://localhost:5173)。只能预览本机页面。' +
123
123
  'kind 说明是哪类设计,右侧按它的画幅显示(App 套手机外框、幻灯片 16:9 …)。' +
@@ -130,7 +130,7 @@ export const previewTool = {
130
130
  kind: {
131
131
  type: 'string',
132
132
  enum: KINDS,
133
- description: '设计类型:web 网页(默认)、app 手机 App 界面(390×844)、tablet 平板、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
133
+ description: '设计类型:web 网页(默认)、app 手机 App 界面(390×844)、miniprogram 微信小程序(375×812,外框带右上角胶囊)、tablet 平板、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
134
134
  },
135
135
  size: { type: 'string', description: '画幅尺寸,可选,如 1080x1920;不填按类型的默认尺寸' },
136
136
  screens: {
@@ -244,7 +244,7 @@ export const checkTool = {
244
244
  // 严重问题的局部截图(截不成就不附;附件库用不了的老版本 DSH 不截,清单照样给)
245
245
  let crops = []
246
246
  if (a.images !== false && ctx && typeof ctx.get === 'function' && ctx.get('attachments')) {
247
- try { crops = await cropIssues(b, rows, { screens: pick.list, dark: !!a.dark }) } catch (e) { studio.log(e) }
247
+ try { crops = await cropIssues(b, rows, { screens: pick.list, dark: !!a.dark, kind }) } catch (e) { studio.log(e) }
248
248
  }
249
249
  const kept = crops.length ? await keepImages(ctx, id, crops) : null
250
250
  if (kept && !kept.images) crops = [] // 存进附件库失败:不提图
@@ -368,9 +368,9 @@ export const lookTool = {
368
368
  const at = ['top', 'bottom', 'full'].includes(a.at) ? a.at : 'top'
369
369
  const pick = pickScreens(snap, a.screens, MAX_SHOTS)
370
370
  if (!pick.list.length) return { text: `没找到要截的屏:${pick.missing.join('、')}。现在有:${(snap.screens || []).map((x) => x.key).join('、') || snap.focus}` }
371
- const shots = await shootAll(b, { screens: pick.list, size: want, at, selector: a.selector, dark: !!a.dark, max: MAX_SHOTS })
371
+ const shots = await shootAll(b, { screens: pick.list, size: want, at, selector: a.selector, dark: !!a.dark, kind, max: MAX_SHOTS })
372
372
  const good = shots.filter((s) => s.jpg).slice(0, MAX_SHOTS)
373
- const text = formatShots(shots, { where: whereOf(snap), missing: pick.missing, more: pick.more })
373
+ const text = formatShots(shots, { where: whereOf(snap), missing: pick.missing, more: pick.more, kind })
374
374
  if (!good.length) return { text }
375
375
  const kept = await keepImages(ctx, id, good, true)
376
376
  if (kept.images) return { text, images: kept.images }