dsh-design-studio 0.6.2 → 0.7.1

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/picker.js CHANGED
@@ -28,6 +28,14 @@
28
28
  const fitsParent = (o) => o === anc || (o.endsWith(':*') && anc.startsWith(o.slice(0, -1)) && /^\d+$/.test(anc.slice(o.length - 1)))
29
29
  const parentOrigins = window.parent === window ? [] : anc ? (allowed.some(fitsParent) ? [anc] : []) : allowed.filter((o) => !o.endsWith(':*'))
30
30
  const parentWin = parentOrigins.length ? window.parent : null
31
+ // 在右侧栏里不画滚动条(和手机上一样):电脑设成「总是显示滚动条」时它占掉 15px,App 按 375 排版而不是 390,
32
+ // 右侧栏的体检就和 check_design(无头浏览器不画滚动条)对不上
33
+ if (parentWin) {
34
+ const s = document.createElement('style')
35
+ s.setAttribute('data-dsh-design', '')
36
+ s.textContent = '*{scrollbar-width:none}*::-webkit-scrollbar{display:none}'
37
+ ;(document.head || document.documentElement).appendChild(s)
38
+ }
31
39
  const SEED_KEY = '__dsh_design_seed__'
32
40
  // 远程看(右侧栏把这一屏打包放进沙盒 iframe 的 srcdoc,location 是 about:srcdoc):这一屏原本的地址由右侧栏给
33
41
  const HERE = cfg.remote && typeof cfg.href === 'string' ? cfg.href : ''
@@ -1131,8 +1139,9 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1131
1139
 
1132
1140
  const LINK_SEL = 'a[href], [data-href], [data-to], [onclick]'
1133
1141
 
1134
- /** 页面里跳到别的屏的地方:路径 + 在视口里的位置(画布上从这里画一条线到那一屏) */
1135
- function linksOf() {
1142
+ /** 页面里跳到别的屏的地方:路径 + 在视口里的位置(画布上从这里画一条线到那一屏)
1143
+ * shown:只要这会儿露在外面的(画布连线用;导出、交接要全部) */
1144
+ function linksOf(shown) {
1136
1145
  const out = []
1137
1146
  const seen = new Set()
1138
1147
  for (const el of document.querySelectorAll(LINK_SEL)) {
@@ -1146,6 +1155,9 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1146
1155
  // 正在播动画(生成动画、页面自己的淡入)的按钮这会儿透明度是 0,但它是看得见的按钮
1147
1156
  const moving = typeof el.getAnimations === 'function' && el.getAnimations().length > 0
1148
1157
  if (cs.visibility === 'hidden' || cs.display === 'none' || (!moving && Number(cs.opacity) === 0)) continue
1158
+ // 被固定的底栏 / 顶栏盖住的按钮(滚到它下面了)看不见,画布上别从它出线
1159
+ const top = shown ? document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2) : null
1160
+ if (top && top !== el && !el.contains(top) && !(host && host.contains(top))) continue
1149
1161
  const k = p + '|' + Math.round(r.left / 8) + '|' + Math.round(r.top / 8)
1150
1162
  if (seen.has(k)) continue
1151
1163
  seen.add(k)
@@ -1160,14 +1172,14 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1160
1172
  let linkSig = ''
1161
1173
  let linkTimer = 0
1162
1174
  function firstLinks() {
1163
- const links = linksOf()
1175
+ const links = linksOf(true)
1164
1176
  linkSig = JSON.stringify(links)
1165
1177
  addEventListener('resize', () => { clearTimeout(linkTimer); linkTimer = setTimeout(resendLinks, 120) })
1166
1178
  setTimeout(resendLinks, 600)
1167
1179
  return links
1168
1180
  }
1169
1181
  function resendLinks() {
1170
- const links = linksOf()
1182
+ const links = linksOf(true)
1171
1183
  const sig = JSON.stringify(links)
1172
1184
  if (sig === linkSig) return
1173
1185
  linkSig = sig
@@ -1432,6 +1444,35 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1432
1444
  const families = new Set()
1433
1445
  const jobs = []
1434
1446
  let seq = 0
1447
+ // <use href="icons.svg#x">:画成图时(SVG 当图片)不会去载别的文件,图标全没了。
1448
+ // 把用到的那个图标复制进画出来的页面,href 改成指它(和 bundle.js 远程看时一样的道理)
1449
+ const sprites = document.createElementNS(SVGNS, 'svg')
1450
+ sprites.setAttribute('style', 'position:absolute;width:0;height:0;overflow:hidden')
1451
+ const spriteDocs = new Map()
1452
+ const spriteIds = new Map()
1453
+ const spriteOf = (abs) => {
1454
+ if (spriteIds.has(abs)) return spriteIds.get(abs)
1455
+ const u = new URL(abs)
1456
+ const frag = decodeURIComponent(u.hash.slice(1))
1457
+ u.hash = ''
1458
+ if (!spriteDocs.has(u.href)) {
1459
+ spriteDocs.set(u.href, fetch(u.href, { credentials: 'same-origin' })
1460
+ .then((r) => (r.ok ? r.text() : ''))
1461
+ .then((t) => (t ? new DOMParser().parseFromString(t, 'image/svg+xml') : null))
1462
+ .catch(() => null))
1463
+ }
1464
+ const p = spriteDocs.get(u.href).then((doc) => {
1465
+ const src = doc && frag ? doc.getElementById(frag) : null
1466
+ if (!src || src.namespaceURI !== SVGNS) return ''
1467
+ const id = 'ddpu' + (++seq)
1468
+ const copy = document.importNode(src, true)
1469
+ copy.setAttribute('id', id)
1470
+ sprites.appendChild(copy)
1471
+ return id
1472
+ })
1473
+ spriteIds.set(abs, p)
1474
+ return p
1475
+ }
1435
1476
 
1436
1477
  function walk(node) {
1437
1478
  if (node.nodeType === 3) return document.createTextNode(node.nodeValue)
@@ -1486,6 +1527,18 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1486
1527
  const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
1487
1528
  if (href) jobs.push(inline(new URL(href, here()).href).then((d) => { if (d) c.setAttribute('href', d) }))
1488
1529
  }
1530
+ if (el.namespaceURI === SVGNS && el.localName === 'use') {
1531
+ const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''
1532
+ let abs = ''
1533
+ try { abs = href && !href.startsWith('#') ? new URL(href, here()).href : '' } catch { /* 怪地址 */ }
1534
+ if (abs && abs.includes('#')) {
1535
+ jobs.push(spriteOf(abs).then((id) => {
1536
+ if (!id) return
1537
+ c.removeAttributeNS('http://www.w3.org/1999/xlink', 'href')
1538
+ c.setAttribute('href', '#' + id)
1539
+ }))
1540
+ }
1541
+ }
1489
1542
  if (tag === 'INPUT') {
1490
1543
  if (el.type === 'checkbox' || el.type === 'radio') { if (el.checked) c.setAttribute('checked', '') } else c.setAttribute('value', el.value)
1491
1544
  }
@@ -1549,6 +1602,7 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
1549
1602
  style.textContent = fontCss + '\n' + pseudoRules.join('\n') + '\n*::-webkit-scrollbar{display:none}'
1550
1603
  const body = [...root.childNodes].find((n) => n.localName === 'body')
1551
1604
  ;(body || root).insertBefore(style, (body || root).firstChild)
1605
+ if (sprites.childNodes.length) (body || root).appendChild(sprites)
1552
1606
  if (!full && (scrollX || scrollY) && body) {
1553
1607
  body.setAttribute('style', body.getAttribute('style') + `transform:translate(${-scrollX}px,${-scrollY}px);`)
1554
1608
  }
@@ -2378,7 +2432,8 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
2378
2432
  if (!d || d.dshDesign !== 1) return
2379
2433
  if (d.type === 'pick') start(d.hint !== false)
2380
2434
  else if (d.type === 'cancel') { if (!styleOn) stop(true) }
2381
- else if (d.type === 'init') boot(d.seed || null)
2435
+ else if (d.type === 'init') { if (d.relay) relay(); boot(d.seed || null) }
2436
+ else if (d.type === 'files') { relay(); onFiles(String(d.file || ''), Array.isArray(d.files) ? d.files : null) }
2382
2437
  else if (d.type === 'reveal') reveal(d)
2383
2438
  else if (d.type === 'zoom') setK(1 / (Number(d.z) || 1))
2384
2439
  else if (d.type === 'snapshot') post({ type: 'snap', seed: seedNow() })
@@ -2512,21 +2567,34 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
2512
2567
  return files.every((f) => /\.html?$/i.test(f) && String(f).replace(/\\/g, '/') !== me)
2513
2568
  }
2514
2569
 
2570
+ /** 目录里有文件改了(file 是最后一个,files 是这一批全部;空的 = 不知道改了啥) */
2571
+ function onFiles(file, files) {
2572
+ if (!shown) return
2573
+ if (othersOnly(files)) return
2574
+ if (parentWin) post({ type: 'changed', file, seed: seedNow() })
2575
+ else selfReload()
2576
+ }
2577
+
2578
+ // 改动通知:在右侧栏里由右侧栏转告(init 里说 relay,之后发 files)。每屏各开一条 SSE 的话,
2579
+ // 画布上到 6 屏就占满浏览器对同一地址的 6 条连接,之后刷新要载的页面、样式表全排着队,预览从此不再变。
2580
+ // 不在右侧栏里(手机上看、单独打开),或右侧栏是老版本不转告(等 3 秒没说),才自己开 SSE。
2515
2581
  // 旧版本(/__dsh_design__/v/…)只读,不跟着目录改动刷新
2516
- if (cfg.live && window.EventSource && !location.pathname.startsWith('/__dsh_design__/v/')) {
2517
- const es = new EventSource('/__dsh_design__/events')
2582
+ const LIVE = !!cfg.live && !!window.EventSource && !location.pathname.startsWith('/__dsh_design__/v/')
2583
+ let es = null
2584
+ let relayed = false
2585
+ const openEvents = () => {
2586
+ if (es || relayed || !LIVE) return
2587
+ es = new EventSource('/__dsh_design__/events')
2518
2588
  es.addEventListener('change', (e) => {
2519
- if (!shown) return
2520
- let file = ''
2521
- let files = null
2522
- try {
2523
- const d = JSON.parse(e.data)
2524
- file = d.file || ''
2525
- files = d.files || null
2526
- } catch { /* 没带文件名 */ }
2527
- if (othersOnly(files)) return
2528
- if (parentWin) post({ type: 'changed', file, seed: seedNow() })
2529
- else selfReload()
2589
+ let d = {}
2590
+ try { d = JSON.parse(e.data) || {} } catch { /* 没带文件名 */ }
2591
+ onFiles(d.file || '', d.files || null)
2530
2592
  })
2531
2593
  }
2594
+ function relay() {
2595
+ relayed = true
2596
+ if (es) { es.close(); es = null }
2597
+ }
2598
+ if (!parentWin) openEvents()
2599
+ else setTimeout(openEvents, 3000)
2532
2600
  })()
package/lib/serve.js CHANGED
@@ -526,5 +526,12 @@ export function createHub({ log = () => {}, onChange = () => {}, history = null,
526
526
  return false
527
527
  }
528
528
 
529
- return { open, close, closeAll, alive, size: () => servers.size }
529
+ /** 现在一共连着几条改动通知(SSE):右侧栏里的页面靠转告,不该自己开(浏览器对同一地址最多 6 条连接) */
530
+ function listeners() {
531
+ let n = 0
532
+ for (const e of servers.values()) n += e.clients.size
533
+ return n
534
+ }
535
+
536
+ return { open, close, closeAll, alive, listeners, size: () => servers.size }
530
537
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-design-studio",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1 test/*.mjs"
@@ -31,7 +31,7 @@
31
31
 
32
32
  2. **先写设计变量**:在设计目录建 `tokens.css`(起手模板 `tokens.css` 就在这个技能目录里,复制过去再按品牌改;有默认的品牌设计系统就复制它的 `tokens.css` 和 `components.css`),颜色、字体、字号阶梯、间距、圆角、阴影都从这里取。每屏都 `<link rel="stylesheet" href="tokens.css">`。之后要统一改风格,改这一个文件,所有屏一起变。
33
33
 
34
- 3. **一屏一个 HTML 文件,放同一个目录**,按用户会看的顺序一屏一屏写(画布按创建顺序排:登录 → 首页 → 详情 → …)。文件名用英文小写(`login.html`、`home.html`),`<title>` 写这屏的中文名(画布上显示它)。临时试验的页面用 `_` 开头(`_test.html`),不会上画布。每屏 `<head>` 里写:
34
+ 3. **一屏一个 HTML 文件,放同一个目录**,按用户会看的顺序一屏一屏写(画布按创建顺序排:登录 → 首页 → 详情 → …)。文件名用英文小写(`login.html`、`home.html`),`<title>` 写「屏名 · 产品名」,用界面的语言(如「首页 · 梅林练」「Home · Pedal」,每屏的产品名写成一样的):画布上只显示屏名,导出整套展示图时产品名做大标题。临时试验的页面用 `_` 开头(`_test.html`),不会上画布。每屏 `<head>` 里写:
35
35
 
36
36
  ```html
37
37
  <meta name="dsh-design" content="app"> <!-- 类型;海报可带尺寸:poster 1080x1920 -->
@@ -109,7 +109,11 @@
109
109
 
110
110
  **形状和层次**:圆角一套(如 8 / 12 / 16 / 999),同类元素圆角一致,嵌套时内圆角 = 外圆角 − 内边距。阴影最多两三档,用低透明度、大模糊、略带主色调的阴影;深色背景用亮一点的表面色区分层级,不靠阴影。
111
111
 
112
- **真实内容**:不要 Lorem ipsum、「标题一」「示例文字」。写像真产品的中文文案、合理的数字和价格、真实感的人名地名。图片位置用 SVG 插画、几何构图或带色块的占位,不引用外网图片(预览离线也要好看)。
112
+ **真实内容**:不要 Lorem ipsum、「标题一」「示例文字」。写像真产品的文案、合理的数字和价格、真实感的人名地名。
113
+
114
+ **界面语言跟着用户走**:用户用什么语言提需求,界面文案就用什么语言(英文提问做英文界面,中文提问做中文界面);用户点名了语言或市场(「给日本用户」「做英文版」)就照他说的。货币、日期格式、人名地名也跟着那个市场。跟用户说话仍用他的语言。
115
+
116
+ **图片**:图片位置用 SVG 插画、几何构图或带色块的占位,不引用外网图片(预览离线也要好看)。
113
117
 
114
118
  **图标**:用一套线性 SVG 图标(同样的线宽 1.75–2、圆角端点、24 网格),内联写进 HTML。不要用 emoji 当图标。
115
119
 
@@ -119,7 +123,7 @@
119
123
 
120
124
  **可访问性**:语义标签(`header` `nav` `main` `button`),图片有 `alt`,焦点样式看得见(`:focus-visible`),可点的区域至少 44×44。
121
125
 
122
- ## 中文排版
126
+ ## 中文排版(中文界面)
123
127
 
124
128
  - 字体栈:`"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, sans-serif`;数字和英文想更精致可以把 `"Inter"` / `-apple-system` 放在前面。
125
129
  - 中文不用斜体;字重用 400 / 500 / 600,标题最多 700。
@@ -127,6 +131,8 @@
127
131
  - 中文行高比英文大一点(正文 1.6–1.75);段落之间用间距分开,不用首行缩进。
128
132
  - 标题不要一行只剩一两个字:调整宽度或用 `text-wrap: balance`。
129
133
 
134
+ 英文界面:字体把 `"Inter"` / `-apple-system` 放最前;标题大小写全套统一一种(句首大写或每词首字母大写);正文一行 `65ch` 左右;标点用半角。
135
+
130
136
  ## 别有「AI 味」
131
137
 
132
138
  一眼像 AI 生成的通病,避开:
@@ -145,7 +151,7 @@
145
151
  - [ ] 一屏只有一个焦点;主按钮只有一个
146
152
  - [ ] 间距都是 4 / 8 的倍数,同类元素间距一致,左边缘对齐
147
153
  - [ ] 正文对比度 ≥ 4.5:1,灰字没有太浅
148
- - [ ] 文案是真实的中文内容,没有占位文字、没有错别字
154
+ - [ ] 文案是真实内容、用的是用户的语言,没有占位文字、没有错别字
149
155
  - [ ] 图标同一套风格,没有 emoji 图标
150
156
  - [ ] 画幅对:App 留出顶部 54px 状态栏和底部 34px 横条;海报 / 幻灯片内容在安全区内不溢出
151
157
  - [ ] 文字没有被截断、没有伸出框、没有叠在一起;页面没有比屏幕宽;滚到底最后的内容没被底栏压住(`check_design` 实测这几项)