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/README.en.md +21 -99
- package/README.md +23 -179
- package/lib/client.js +401 -89
- package/lib/picker.js +86 -18
- package/lib/serve.js +8 -1
- package/package.json +1 -1
- package/skills/ui-design/SKILL.md +10 -4
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
|
-
|
|
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
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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
|
-
|
|
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
|
@@ -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>`
|
|
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
|
|
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` 实测这几项)
|