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/core.js CHANGED
@@ -31,13 +31,14 @@ 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', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
34
+ // 设计类型:决定右侧用什么画幅显示(网页框、手机、小程序、平板、桌面软件窗口、海报、幻灯片、邮件)
35
+ export const KINDS = ['web', 'app', 'miniprogram', 'tablet', 'desktop', 'poster', 'slides', 'email']
36
36
  const KIND_ALIAS = {
37
- website: 'web', site: 'web', page: 'web', landing: 'web', desktop: 'web', dashboard: 'web',
37
+ website: 'web', site: 'web', page: 'web', landing: 'web', dashboard: 'web',
38
38
  phone: 'app', mobile: 'app', ios: 'app', android: 'app',
39
39
  mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram',
40
40
  pad: 'tablet', ipad: 'tablet',
41
+ pc: 'desktop', electron: 'desktop', tauri: 'desktop', mac: 'desktop', macos: 'desktop', windows: 'desktop', win: 'desktop', 桌面: 'desktop', 桌面软件: 'desktop',
41
42
  social: 'poster', image: 'poster', banner: 'poster', card: 'poster', flyer: 'poster', cover: 'poster',
42
43
  slide: 'slides', deck: 'slides', ppt: 'slides', presentation: 'slides',
43
44
  mail: 'email', newsletter: 'email', edm: 'email',
package/lib/handoff.js CHANGED
@@ -207,6 +207,13 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
207
207
  lines.push('- 底部标签栏写进 app.json 的 `tabBar`(样式特殊就用自定义 tabBar),底部留出安全区 `env(safe-area-inset-bottom)`。', '')
208
208
  }
209
209
 
210
+ if (kind === 'desktop') {
211
+ lines.push('## 桌面软件要点', '')
212
+ lines.push(`- 这是桌面软件的设计(窗口内容 ${size[0]}×${size[1]})。要做成真软件:Electron 或 Tauri 套网页,框架选的「${fwLabel}」写界面;用户要原生(SwiftUI / WinUI)就先问清。`)
213
+ lines.push('- 窗口按钮是系统画的:Mac 用无边框标题栏(Electron `titleBarStyle: \'hiddenInset\'`,Tauri `titleBarStyle: \'Overlay\'`),红绿灯盖在左上角,设计稿左上空出的 80×52 照留;顶栏可拖动的地方加 `-webkit-app-region: drag`,按钮、输入框加 `no-drag`。Windows 用自定义标题栏时右上角 138×32 留给系统按钮(Electron `titleBarOverlay`)。')
214
+ lines.push('- 整个窗口不滚,侧栏、列表、详情各自滚(`overflow: auto`);最小窗口尺寸照设计里最小的那种(多尺寸里的 1024×680)设。', '')
215
+ }
216
+
210
217
  lines.push('## 屏幕和路由', '', '| 屏 | 设计文件 | 建议路由 | 截图(handoff/shots/) | 跳到 |', '| --- | --- | --- | --- | --- |')
211
218
  for (const r of rows) {
212
219
  const to = [...new Set((r.links || []).map((l) => keyOfLink(l.path)).filter((k) => k && k !== r.key))]
package/lib/index.js CHANGED
@@ -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)、miniprogram 微信小程序(375×812,外框带右上角胶囊)、tablet 平板、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
133
+ description: '设计类型:web 网页(默认)、app 手机 App 界面(390×844)、miniprogram 微信小程序(375×812,外框带右上角胶囊)、tablet 平板、desktop 桌面软件(1280×800 的 Mac 窗口;Windows 软件在页面 meta 写 desktop windows)、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
134
134
  },
135
135
  size: { type: 'string', description: '画幅尺寸,可选,如 1080x1920;不填按类型的默认尺寸' },
136
136
  screens: {
package/lib/look.js CHANGED
@@ -9,9 +9,9 @@
9
9
  import fs from 'node:fs'
10
10
  import path from 'node:path'
11
11
 
12
- export const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
12
+ export 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] }
13
13
  const KIND_LIST = Object.keys(KIND_SIZE)
14
- 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' }
14
+ 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' }
15
15
  // 只给宽度时的高:常见真机的屏幕高;别的宽度按这一类默认的宽高比
16
16
  const PHONE_H = { 320: 568, 360: 780, 375: 667, 390: 844, 393: 852, 402: 874, 412: 915, 414: 896, 428: 926, 430: 932, 440: 956 }
17
17
  const TABLET_H = { 744: 1133, 768: 1024, 810: 1080, 820: 1180, 834: 1194, 1024: 1366, 1032: 1376 }
@@ -140,6 +140,7 @@ export async function measureAll(browser, { screens, sizes, kind, dark, fetcher
140
140
  const { info, darkOk } = await prepare(page, dark)
141
141
  if (info && info.title) row.title = String(info.title).slice(0, 40)
142
142
  if (dark && !darkOk) row.noDark = true
143
+ if (page.pseudo) await page.pseudo().catch(() => 0) // 伪元素的真实框(量不到就按样式推)
143
144
  const r = await page.eval(`window.__dshDesign.check(${JSON.stringify(kind)})`)
144
145
  row.issues = Array.isArray(r && r.issues) ? r.issues : []
145
146
  row.metrics = (r && r.metrics) || null
@@ -208,6 +209,7 @@ export async function shootAll(browser, { screens, size, at, selector, dark, kin
208
209
  // 小程序:每次截图前重画胶囊(滚动后顶栏可能变了色,胶囊黑白跟着重量)
209
210
  const shoot = async (opts) => {
210
211
  if (kind === 'miniprogram') await page.eval("window.__dshDesign.capsule('solid')").catch(() => null)
212
+ if (kind === 'desktop') await page.eval("window.__dshDesign.winctl('solid')").catch(() => null)
211
213
  return page.shot(opts)
212
214
  }
213
215
  const title = info && info.title ? String(info.title).slice(0, 40) : ''
@@ -253,7 +255,7 @@ export async function shootAll(browser, { screens, size, at, selector, dark, kin
253
255
  }
254
256
 
255
257
  // 值得附图的问题(严重的、看图比看数字明白),越靠前越先截
256
- const CROP_ORDER = ['covered', 'capsule', 'overlap', 'clipped', 'overflow', 'hscroll', 'img', 'contrast']
258
+ const CROP_ORDER = ['covered', 'capsule', 'island', 'winctl', 'homebar', 'overlap', 'clipped', 'overflow', 'hscroll', 'img', 'contrast']
257
259
 
258
260
  /** 两条问题挨得近(同一屏同一尺寸,元素上下差不到 48px、左右差不到 160px):截一张图就都看得到 */
259
261
  function nearby(a, b) {
@@ -311,6 +313,9 @@ function labelOf(it) {
311
313
  switch (it.type) {
312
314
  case 'covered': return `压住 ${it.px}px`
313
315
  case 'capsule': return `挡住 ${it.px}px`
316
+ case 'island': return `挡住 ${it.px}px`
317
+ case 'winctl': return `挡住 ${it.px}px`
318
+ case 'homebar': return `伸进 ${it.px}px`
314
319
  case 'overlap': return it.px ? `叠了 ${it.px}px` : ''
315
320
  case 'clipped': return `切掉 ${it.px}px`
316
321
  case 'overflow': return `伸出 ${it.px}px`
@@ -347,6 +352,7 @@ export async function cropIssues(browser, rows, { screens, dark, kind = '', max
347
352
  if (!at || !(at.w >= 1 && at.h >= 1)) continue
348
353
  // 小程序:滚到问题那儿以后再画胶囊(半透明,透出被挡住的东西;黑白按这时的顶栏)
349
354
  if (kind === 'miniprogram') await page.eval("window.__dshDesign.capsule('ghost')").catch(() => null)
355
+ if (kind === 'desktop') await page.eval("window.__dshDesign.winctl('ghost')").catch(() => null)
350
356
  c.jpg = await page.shot({ clip: { x: at.x, y: at.y, width: at.w, height: at.h } })
351
357
  // 挨着的那几条:真在图里才算「看图几」
352
358
  c.ks = [c.k, ...c.also.filter((_, i) => at.inside && at.inside[i]).map((x) => x.k)]
@@ -364,7 +370,8 @@ const TYPE = {
364
370
  hscroll: '超出屏幕', clipped: '文字被截断', overflow: '文字超出框', overlap: '文字叠在一起', contrast: '对比度不够', tap: '点击区太小',
365
371
  tiny: '字太小', img: '图片没加载出来', fonts: '字号太杂', distort: '图片被拉变形', long: '一行太长', wrap: '文字被挤断行',
366
372
  covered: '被固定栏压住', tight: '离固定栏太近', jserr: '页面脚本报错', missing: '文件没载出来', deadlink: '跳到的页面不存在',
367
- capsule: '被小程序胶囊挡住',
373
+ capsule: '被小程序胶囊挡住', island: '被灵动岛挡住', homebar: '伸进底部横条区', tabbar: '标签栏项太多', navbar: '没有导航栏',
374
+ winctl: '被窗口按钮挡住',
368
375
  }
369
376
  const EDGE = { top: '顶栏', bottom: '底栏' }
370
377
  const WHAT = { css: '样式表', script: '脚本', font: '字体', link: '文件', video: '视频', audio: '音频', source: '媒体', iframe: '内嵌页' }
@@ -407,6 +414,13 @@ export function detailOf(it) {
407
414
  case 'covered': return `${it.edge === 'top' ? '没滚时' : '滚到底还'}被${EDGE[it.edge] || ''} ${it.bar} 压住 ${it.px}px(给内容留出栏的高度:${it.edge === 'top' ? '顶上' : '底部'}至少再留 ${it.px + 16}px,用 ${it.edge === 'top' ? 'padding-top' : 'padding-bottom'},栏是 fixed 时也可以用 scroll-padding)`
408
415
  case 'tight': return `离${EDGE[it.edge] || ''} ${it.bar} 只有 ${it.px}px(留 16px 以上)`
409
416
  case 'capsule': return `伸进右上角胶囊 ${it.px}px(胶囊是微信画的、页面盖不住,在 y 48–80、x ${it.x} 以右:顶栏右边空出这块,标题居中或靠左)`
417
+ case 'island': return `被灵动岛挡住 ${it.px}px(灵动岛在屏幕顶上正中、y 11–45:状态栏中间空着,标题、按钮放到状态栏下面)`
418
+ case 'homebar': return `底栏 ${it.bar} 里的字或图标伸进底部横条区 ${it.px}px(屏幕最下面 34px 留给横条:栏的底色铺下去,内容抬到它上面,设计稿里栏写 padding-bottom: 34px——预览里 env(safe-area-inset-bottom) 是 0,靠它让不出来,做成真代码时再换成 env())`
419
+ case 'tabbar': return `标签栏有 ${it.n} 项(最多 5 项;小程序 tabBar 超过 5 项微信直接报错):留最常用的几项,其余放进「我的」或首页入口`
420
+ case 'navbar': return `顶上 y ${it.t}–${it.b}(导航栏那条)是空的:小程序每屏都有导航栏——一级页放标题或品牌 / 搜索(让开右上角胶囊),二级页标题居中、左边加返回;启动页、全屏大图可以不管`
421
+ case 'winctl': return it.os === 'win'
422
+ ? `伸进右上角的最小化 / 最大化 / 关闭按钮 ${it.px}px(外框画的、盖在页面上面,在最右边 138px、y 0–32:标题栏右边空出这块)`
423
+ : `伸进左上角红绿灯 ${it.px}px(外框画的、盖在页面上面,在 x 20–72、y 20–32:工具栏 / 侧栏顶上左边空出 80px,标题、按钮从 x 80 以后放)`
410
424
  case 'jserr': return `${it.msg}${it.src ? `(${it.src}${it.line ? `:${it.line}` : ''})` : ''}`
411
425
  case 'missing': return `${WHAT[it.what] || '文件'} ${it.src} 载不出来(路径写错了,或者文件不在)`
412
426
  default: return ''
@@ -549,6 +563,7 @@ export function formatShots(shots, { where, missing = [], more = 0, kind = '' })
549
563
  if (skipped > 0) lines.push(`${key} 一共 ${mine[0].of} 段,中间 ${skipped} 段没截(要看就单截这一屏,或用 selector 截那一块)`)
550
564
  }
551
565
  if (kind === 'miniprogram' && n) lines.push('右上角画上了微信的胶囊(真机上它盖在页面上面,页面里别再画)。')
566
+ if (kind === 'desktop' && n) lines.push('画上了窗口按钮(Mac 左上角红绿灯 / Windows 右上角三个按钮,真窗口里系统画的,页面里别再画)。')
552
567
  if (missing.length) lines.push(`没找到这几屏:${missing.join('、')}`)
553
568
  if (more) lines.push(`还有 ${more} 屏没截(一次最多 ${MAX_SHOTS} 张,用 screens 挑着截)`)
554
569
  return lines.join('\n')