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/client.js
CHANGED
|
@@ -196,6 +196,7 @@ window.__ModuleLoader__.load({
|
|
|
196
196
|
dark: '深色模式',
|
|
197
197
|
variants: '再出两版',
|
|
198
198
|
pickThis: '就要这个:其他版本挪进 drafts/,接着做全套',
|
|
199
|
+
pickShort: '就要这个',
|
|
199
200
|
pickText: '〔选定方向〕就用「{name}」({where})。先把同组的 {others} 挪进 drafts/ 子目录(别删),再把它改名成 {base}.html;它的设计变量搬进 tokens.css(没有就新建)、改用 tokens.css。然后接着做全套。\n',
|
|
200
201
|
pickKeep: '〔选定方向〕留原版「{name}」({where})。同组的 {others} 挪进 drafts/ 子目录(别删),其他不动。\n',
|
|
201
202
|
variantText: '给「{name}」再出两版不同方向的设计(布局或视觉风格明显不同),存成同目录下的 {b}-b.html 和 {b}-c.html,会紧挨着原版排在画布上。原版和 tokens.css 别动;新版要换配色就在自己文件里覆盖变量。\n{where}\n',
|
|
@@ -297,6 +298,10 @@ window.__ModuleLoader__.load({
|
|
|
297
298
|
step1: '让模型做一个网页、App 界面或海报',
|
|
298
299
|
step2: '点「选择」,点元素或拖出一块区域',
|
|
299
300
|
step3: '在输入框补一句要怎么改,发出去',
|
|
301
|
+
tryHead: '不知道怎么开口?点一句试试',
|
|
302
|
+
try1: '帮我设计一个社区健身房的会员 App',
|
|
303
|
+
try2: '做一个精品咖啡店的落地页',
|
|
304
|
+
try3: '做一张周末市集的活动海报',
|
|
300
305
|
inserted: '已放进输入框,补一句要怎么改',
|
|
301
306
|
insertedSend: '已放进输入框,发出去就行',
|
|
302
307
|
copied: '已复制,粘贴到输入框里',
|
|
@@ -504,6 +509,7 @@ window.__ModuleLoader__.load({
|
|
|
504
509
|
dark: 'Dark mode',
|
|
505
510
|
variants: 'Two more takes',
|
|
506
511
|
pickThis: 'Use this one: move the other takes to drafts/ and build the rest',
|
|
512
|
+
pickShort: 'Use this',
|
|
507
513
|
pickText: '[Direction picked] Use "{name}" ({where}). First move the other takes in its group ({others}) into a drafts/ subfolder (don\'t delete them), then rename it to {base}.html; move its design tokens into tokens.css (create it if missing) and switch it to tokens.css. Then build the rest.\n',
|
|
508
514
|
pickKeep: '[Direction picked] Keep the original "{name}" ({where}). Move the other takes in its group ({others}) into a drafts/ subfolder (don\'t delete them) and leave everything else as is.\n',
|
|
509
515
|
variantText: 'Make two alternative designs of "{name}" in clearly different directions (layout or visual style). Save them next to it as {b}-b.html and {b}-c.html; they will sit right after the original on the canvas. Leave the original and tokens.css unchanged; override variables inside the new files if they need other colors.\n{where}\n',
|
|
@@ -605,6 +611,10 @@ window.__ModuleLoader__.load({
|
|
|
605
611
|
step1: 'Ask the model for a web page, app screen or poster',
|
|
606
612
|
step2: 'Click Select, then an element or drag a region',
|
|
607
613
|
step3: 'Say what to change in the input and send',
|
|
614
|
+
tryHead: 'Not sure where to start? Try one',
|
|
615
|
+
try1: 'Design a member app for a neighborhood gym',
|
|
616
|
+
try2: 'Make a landing page for a specialty coffee shop',
|
|
617
|
+
try3: 'Make a poster for a weekend street market',
|
|
608
618
|
inserted: 'Added to the input. Say what to change',
|
|
609
619
|
insertedSend: 'Added to the input. Just send it',
|
|
610
620
|
copied: 'Copied. Paste it into the input',
|
|
@@ -899,6 +909,11 @@ window.__ModuleLoader__.load({
|
|
|
899
909
|
.ddp-sc .pv .ln{position:absolute;left:6px;top:6px;width:46%;height:3px;border-radius:2px;opacity:.85}
|
|
900
910
|
.ddp-sc .pv .ln.s{top:12px;width:30%;opacity:.6}
|
|
901
911
|
.ddp-sc .nm{font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
912
|
+
.ddp-tries{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:440px}
|
|
913
|
+
.ddp-tries>span{flex-basis:100%;font-size:11px;line-height:16px}
|
|
914
|
+
.ddp-try{height:28px;padding:0 12px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:999px;background:transparent;
|
|
915
|
+
color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
|
|
916
|
+
.ddp-try:hover{border-color:${ACCENT};color:${ACCENT}}
|
|
902
917
|
.ddp-stylepick{width:100%;max-width:440px;margin:4px 0 6px;text-align:left}
|
|
903
918
|
.ddp-stylepick>b{display:block;margin-bottom:2px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
904
919
|
.ddp-stylepick>p{margin:0 0 10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -927,10 +942,12 @@ window.__ModuleLoader__.load({
|
|
|
927
942
|
.ddp-ver span{font-size:12px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
928
943
|
.ddp-ver small{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
929
944
|
.ddp-flows{position:absolute;left:0;top:0;overflow:visible;pointer-events:none;z-index:20}
|
|
930
|
-
.ddp-flows
|
|
945
|
+
.ddp-flows .halo{fill:none;stroke:var(--dsw-alias-bg-layer-2);stroke-linecap:round;stroke-linejoin:round}
|
|
946
|
+
.ddp-flows .ln{fill:none;stroke:${ACCENT};stroke-linecap:round;stroke-linejoin:round}
|
|
931
947
|
.ddp-flows .hd{fill:${ACCENT};stroke:none}
|
|
932
|
-
.ddp-flows
|
|
933
|
-
.ddp-flows
|
|
948
|
+
.ddp-flows .dot{fill:${ACCENT};stroke:#fff}
|
|
949
|
+
.ddp-flows .hot{fill:rgba(77,107,254,.1);stroke:${ACCENT}}
|
|
950
|
+
.ddp-flows .hot.nav{fill:none;stroke-opacity:.75}
|
|
934
951
|
.ddp-demo{position:absolute;inset:0;overflow:hidden}
|
|
935
952
|
.ddp-demo .lay{position:absolute;inset:0;transition:transform .42s ${EASE},opacity .42s ${EASE}}
|
|
936
953
|
.ddp-demo .lay.wait{opacity:0;pointer-events:none}
|
|
@@ -966,6 +983,14 @@ window.__ModuleLoader__.load({
|
|
|
966
983
|
.ddp-cmp .tag.l{left:10px}
|
|
967
984
|
.ddp-cmp .tag.r{right:10px;background:${ACCENT}}
|
|
968
985
|
.ddp-act{margin-left:auto;display:flex;gap:1px;flex:none}
|
|
986
|
+
.ddp-take{flex:none;display:inline-flex;align-items:center;gap:3px;height:20px;padding:0 8px 0 6px;border:0;border-radius:10px;
|
|
987
|
+
background:rgba(77,107,254,.12);color:${ACCENT};font:inherit;font-size:11px;font-weight:500;cursor:pointer}
|
|
988
|
+
.ddp-take:hover{background:rgba(77,107,254,.2)}
|
|
989
|
+
.ddp-take svg{width:12px;height:12px;flex:none}
|
|
990
|
+
/* 画布缩得很小时只留 ✓,屏名先保住 */
|
|
991
|
+
.ddp-lab{container-type:inline-size}
|
|
992
|
+
.ddp-lab>span:first-child{min-width:3em}
|
|
993
|
+
@container (max-width:170px){.ddp-take{padding:0 4px}.ddp-take span{display:none}}
|
|
969
994
|
.ddp-act .ddp-btn{height:22px;min-width:22px;padding:0 4px;color:${ACCENT}}
|
|
970
995
|
.ddp-act .ddp-btn svg{width:13px;height:13px}
|
|
971
996
|
@container (max-width:500px){.ddp-btn.pick span,.ddp-kind>.ddp-btn span{display:none}}
|
|
@@ -1206,6 +1231,56 @@ window.__ModuleLoader__.load({
|
|
|
1206
1231
|
return lines.join('\n') + '\n'
|
|
1207
1232
|
}
|
|
1208
1233
|
|
|
1234
|
+
// 插进输入框的字前后怎么接。prev / next 是光标前后的字(读不到为 null),start 是光标位置。
|
|
1235
|
+
// 光标前有字、又不在行首 → 先换行;open(要用户接着说要求)且字以换行结尾 → 末尾多留一个空行给光标:
|
|
1236
|
+
// 输入框里末尾单个换行显示不出来,光标会回到上一行末尾,接着打的字就粘在那行后面。
|
|
1237
|
+
// 不以换行结尾的(「照这个风格来做:」)光标就跟在后面接着写
|
|
1238
|
+
function padInsert(text, prev, next, start, open) {
|
|
1239
|
+
const lead = start > 0 && !(prev != null && prev.endsWith('\n')) ? '\n' : ''
|
|
1240
|
+
let body = text
|
|
1241
|
+
let back = 0
|
|
1242
|
+
if (open && body.endsWith('\n')) {
|
|
1243
|
+
body = body.replace(/\n+$/, '') + '\n'
|
|
1244
|
+
if (!(next != null && next.startsWith('\n'))) { body += '\n'; back = 1 }
|
|
1245
|
+
}
|
|
1246
|
+
return { text: lead + body, back }
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// DSH 的输入框(只认一个;分屏开着多个时不碰)
|
|
1250
|
+
function composerEl() {
|
|
1251
|
+
try {
|
|
1252
|
+
const els = document.querySelectorAll('[data-lexical-editor="true"]')
|
|
1253
|
+
return els.length === 1 ? els[0] : null
|
|
1254
|
+
} catch { return null }
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// 输入框的纯文本,段落按换行连起来(和 captureInsertion 的位置对得上);里面有引用块等别的东西时对不上,给 null
|
|
1258
|
+
function composerText(el) {
|
|
1259
|
+
if (!el || el.querySelector(':scope > :not(p), p :not(span[data-lexical-text]):not(br)')) return null
|
|
1260
|
+
return Array.from(el.children).map((p) => p.textContent).join('\n')
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// 插完把键盘交回输入框,光标放在刚插的字后面(再往回 back 个字):用户直接接着说要改什么,或者直接回车发出
|
|
1264
|
+
function focusAfter(el, text, back) {
|
|
1265
|
+
if (!el) return
|
|
1266
|
+
el.focus({ preventScroll: true })
|
|
1267
|
+
const nodes = []
|
|
1268
|
+
let all = ''
|
|
1269
|
+
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
1270
|
+
for (let n = w.nextNode(); n; n = w.nextNode()) { nodes.push([n, all.length]); all += n.data }
|
|
1271
|
+
const at = all.lastIndexOf(text)
|
|
1272
|
+
const r = document.createRange()
|
|
1273
|
+
if (at < 0) { r.selectNodeContents(el); r.collapse(false) } else {
|
|
1274
|
+
const end = at + text.length - back
|
|
1275
|
+
const hit = nodes.find(([n, o]) => o + n.data.length >= end) || nodes[nodes.length - 1]
|
|
1276
|
+
r.setStart(hit[0], end - hit[1])
|
|
1277
|
+
r.collapse(true)
|
|
1278
|
+
}
|
|
1279
|
+
const s = window.getSelection()
|
|
1280
|
+
s.removeAllRanges()
|
|
1281
|
+
s.addRange(r)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1209
1284
|
// ── 设计变量 ─────────────────────────────────────────────────────────────
|
|
1210
1285
|
|
|
1211
1286
|
const TOKEN_GROUPS = ['color', 'font', 'size', 'space', 'radius', 'shadow', 'other']
|
|
@@ -1910,6 +1985,7 @@ window.__ModuleLoader__.load({
|
|
|
1910
1985
|
|
|
1911
1986
|
/**
|
|
1912
1987
|
* n 屏排成网格:试每种列数,挑缩进舞台后最大的那种。
|
|
1988
|
+
* 一样大时(高度先到顶了)多放几列:拉宽右侧栏就横着多排几屏,像网页卡片换行;但不让最后一行孤零零剩一屏。
|
|
1913
1989
|
* 行距 = 按比例的空隙 + 标题的高度(标题不缩放,所以换算回画布坐标是 LAB / z)。
|
|
1914
1990
|
*/
|
|
1915
1991
|
function layoutGrid(n, dw, dh, sw, sh) {
|
|
@@ -1924,7 +2000,8 @@ window.__ModuleLoader__.load({
|
|
|
1924
2000
|
const zw = (W - (cols - 1) * GAP) / (cols * dw + (cols - 1) * gx)
|
|
1925
2001
|
const zh = (H - (rows - 1) * LAB) / (rows * dh + (rows - 1) * gy)
|
|
1926
2002
|
const z = Math.min(zw, zh)
|
|
1927
|
-
|
|
2003
|
+
const tie = best && Math.abs(z - best.z) <= best.z * 1e-6
|
|
2004
|
+
if (!best || z > best.z * (1 + 1e-6) || (tie && (rows === 1 || count - (rows - 1) * cols > 1))) best = { cols, rows, z }
|
|
1928
2005
|
}
|
|
1929
2006
|
const zl = clampZ(Math.min(1, best.z))
|
|
1930
2007
|
const stepX = dw + gx + GAP / zl
|
|
@@ -1974,102 +2051,307 @@ window.__ModuleLoader__.load({
|
|
|
1974
2051
|
return k
|
|
1975
2052
|
}
|
|
1976
2053
|
|
|
1977
|
-
/**
|
|
2054
|
+
/**
|
|
2055
|
+
* 每屏报上来的跳转 → 一屏到另一屏一条,hots 是这一对所有的按钮。
|
|
2056
|
+
* nav:「导航」按钮(底栏、返回这种好几屏同一个位置都有、跳到同一屏的),画布上只描框、不画线
|
|
2057
|
+
*/
|
|
1978
2058
|
function flowsOf(screens, linksOf, file) {
|
|
1979
2059
|
const keys = new Set(screens.map((s) => s.key))
|
|
1980
|
-
const
|
|
2060
|
+
const spot = (to, l) => to + '|' + Math.round(l.x / 12) + '|' + Math.round(l.y / 12)
|
|
2061
|
+
const where = new Map() // 同一个位置、跳到同一屏的按钮出现在几屏上
|
|
2062
|
+
const per = screens.map((sc) => (linksOf(sc) || []).map((l) => ({ ...l, to: keyOfPath(l.path, file) })))
|
|
2063
|
+
per.forEach((ls) => { for (const k of new Set(ls.map((l) => spot(l.to, l)))) where.set(k, (where.get(k) || 0) + 1) })
|
|
1981
2064
|
const out = []
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
if (to === sc.key || !keys.has(to)) continue
|
|
1986
|
-
const
|
|
1987
|
-
if (
|
|
1988
|
-
|
|
1989
|
-
out.push({ from: sc.key, to, x: l.x, y: l.y, w: l.w, h: l.h })
|
|
2065
|
+
screens.forEach((sc, i) => {
|
|
2066
|
+
const pairs = new Map()
|
|
2067
|
+
for (const l of per[i]) {
|
|
2068
|
+
if (l.to === sc.key || !keys.has(l.to)) continue
|
|
2069
|
+
const hot = { x: l.x, y: l.y, w: l.w, h: l.h, nav: (where.get(spot(l.to, l)) || 0) >= 3 }
|
|
2070
|
+
if (!pairs.has(l.to)) pairs.set(l.to, [])
|
|
2071
|
+
pairs.get(l.to).push(hot)
|
|
1990
2072
|
}
|
|
1991
|
-
|
|
2073
|
+
for (const [to, hots] of pairs) out.push({ from: sc.key, to, ...hots[0], hots })
|
|
2074
|
+
})
|
|
1992
2075
|
return out
|
|
1993
2076
|
}
|
|
1994
2077
|
|
|
1995
2078
|
/**
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
2079
|
+
* 选中那一屏「通向哪」的连线(画布坐标)。线只走屏和屏之间的缝(列间的竖缝、行间的横缝、外圈),不穿过别的屏:
|
|
2080
|
+
* 从按钮边上出发,就近出屏,在缝里直角转弯(圆角),落到目标屏的边上。几条线各走一道(错开几像素),不叠在一起。
|
|
2081
|
+
* 每种出 / 进的边都试一遍,挑最短、拐弯最少、在屏里走得最少的。
|
|
2082
|
+
* 只有导航按钮连着的(nav)不画线,只给按钮描虚线框。opts.z 是画布缩放(换算几像素的间距),opts.lift 是屏上面标题那一条的高度。
|
|
1998
2083
|
*/
|
|
1999
2084
|
function flowLines(flows, keys, pos, kind, size, opts = {}) {
|
|
2000
2085
|
const f = FRAME[kind] || FRAME.web
|
|
2001
2086
|
const [dw, dh] = deviceBox(kind, size)
|
|
2087
|
+
const z = opts.z || 0.4
|
|
2088
|
+
const px = (n) => n / z
|
|
2089
|
+
const lift = opts.lift || 0
|
|
2002
2090
|
const idx = new Map(keys.map((k, i) => [k, i]))
|
|
2003
|
-
const incoming = new Map()
|
|
2004
|
-
const out = []
|
|
2005
|
-
const lift = opts.lift || 0 // 屏上面标题那一条有多高(世界坐标):绕行的线不压标题
|
|
2006
|
-
// 一屏占的地方(连上面的标题);曲线要是穿过别的屏,就改走屏和屏之间的空当
|
|
2007
|
-
const boxes = pos.map((p) => p && { x0: p.x, y0: p.y - lift, x1: p.x + dw, y1: p.y + dh })
|
|
2008
2091
|
const xs = [...new Set(pos.filter(Boolean).map((p) => p.x))].sort((m, n) => m - n)
|
|
2009
2092
|
const ys = [...new Set(pos.filter(Boolean).map((p) => p.y))].sort((m, n) => m - n)
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2093
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v))
|
|
2094
|
+
// 竖缝:第 j 道在第 j 列左边(0 = 最左外圈,xs.length = 最右外圈)
|
|
2095
|
+
const vLane = (j) => {
|
|
2096
|
+
if (j <= 0) return { c: xs[0] - px(14), hw: px(8) }
|
|
2097
|
+
if (j >= xs.length) return { c: xs[xs.length - 1] + dw + px(14), hw: px(8) }
|
|
2098
|
+
const a = xs[j - 1] + dw
|
|
2099
|
+
return { c: (a + xs[j]) / 2, hw: (xs[j] - a) / 2 }
|
|
2100
|
+
}
|
|
2101
|
+
// 横缝:第 i 道在第 i 排上面(标题再上面);0 = 最上外圈,ys.length = 最下外圈
|
|
2102
|
+
const hLane = (i) => {
|
|
2103
|
+
if (i <= 0) return { c: ys[0] - lift - px(9), hw: px(6) }
|
|
2104
|
+
if (i >= ys.length) return { c: ys[ys.length - 1] + dh + px(14), hw: px(9) }
|
|
2105
|
+
const a = ys[i - 1] + dh
|
|
2106
|
+
const b = ys[i] - lift
|
|
2107
|
+
return { c: (a + b) / 2, hw: Math.max(px(4), (b - a) / 2) }
|
|
2108
|
+
}
|
|
2109
|
+
const at = (lane, o) => lane.c + clamp(o, -lane.hw + px(2), lane.hw - px(2))
|
|
2110
|
+
const cellOf = (i) => ({ x0: pos[i].x, y0: pos[i].y, x1: pos[i].x + dw, y1: pos[i].y + dh, col: xs.indexOf(pos[i].x), row: ys.indexOf(pos[i].y) })
|
|
2111
|
+
|
|
2112
|
+
/** 一条线:出边 E、进边 N、错开 o → 拐点 */
|
|
2113
|
+
function route(S, T, hot, E, N, o) {
|
|
2114
|
+
const cx = hot.x + hot.w / 2
|
|
2115
|
+
const cy = hot.y + hot.h / 2
|
|
2116
|
+
const pts = []
|
|
2117
|
+
let cur // 现在在哪道缝里:{ v: true/false, lane, p: 另一个坐标 }
|
|
2118
|
+
if (E === 'L' || E === 'R') {
|
|
2119
|
+
const lane = at(vLane(E === 'L' ? S.col : S.col + 1), o)
|
|
2120
|
+
pts.push([E === 'L' ? hot.x : hot.x + hot.w, cy], [lane, cy])
|
|
2121
|
+
cur = { v: true, c: lane, p: cy }
|
|
2122
|
+
} else {
|
|
2123
|
+
const lane = at(hLane(E === 'T' ? S.row : S.row + 1), o)
|
|
2124
|
+
pts.push([cx, E === 'T' ? hot.y : hot.y + hot.h], [cx, lane])
|
|
2125
|
+
cur = { v: false, c: lane, p: cx }
|
|
2126
|
+
}
|
|
2127
|
+
const side = N === 'L' || N === 'R'
|
|
2128
|
+
const tLane = side ? at(vLane(N === 'L' ? T.col : T.col + 1), o) : at(hLane(N === 'T' ? T.row : T.row + 1), o)
|
|
2129
|
+
// 落点:侧边上下留 20%;顶边避开左边的标题字,落在右半边
|
|
2130
|
+
// 从上 / 下进同一列的两屏时落点分开(往上进的靠里一点),不在缝里交叉成十字
|
|
2131
|
+
const yIn = (y) => clamp(y, T.y0 + dh * 0.2, T.y0 + dh * 0.8)
|
|
2132
|
+
const xIn = (x) => {
|
|
2133
|
+
const right = x > T.x0 + dw / 2
|
|
2134
|
+
if (N === 'T') return clamp(x, T.x0 + dw * 0.55, T.x0 + dw * (right ? 0.88 : 0.6))
|
|
2135
|
+
return clamp(x, T.x0 + dw * (right ? 0.4 : 0.14), T.x0 + dw * (right ? 0.72 : 0.6))
|
|
2136
|
+
}
|
|
2137
|
+
const edge = (p) => (N === 'L' ? [T.x0, p] : N === 'R' ? [T.x1, p] : N === 'T' ? [p, T.y0] : [p, T.y1])
|
|
2138
|
+
if (cur.v && side) {
|
|
2139
|
+
if (Math.abs(cur.c - tLane) < 0.5) {
|
|
2140
|
+
const y = yIn(cur.p)
|
|
2141
|
+
pts.push([cur.c, y], edge(y))
|
|
2142
|
+
} else {
|
|
2143
|
+
// 两道竖缝之间:找一道横缝过去
|
|
2144
|
+
let k = 0
|
|
2145
|
+
let bestD = Infinity
|
|
2146
|
+
for (let i = 0; i <= ys.length; i++) {
|
|
2147
|
+
const d = Math.abs(hLane(i).c - cur.p) + Math.abs(hLane(i).c - (T.y0 + dh / 2))
|
|
2148
|
+
if (d < bestD) { bestD = d; k = i }
|
|
2149
|
+
}
|
|
2150
|
+
const hy = at(hLane(k), o)
|
|
2151
|
+
const y = yIn(hy)
|
|
2152
|
+
pts.push([cur.c, hy], [tLane, hy], [tLane, y], edge(y))
|
|
2153
|
+
}
|
|
2154
|
+
} else if (cur.v && !side) {
|
|
2155
|
+
const x = xIn(cur.c)
|
|
2156
|
+
pts.push([cur.c, tLane], [x, tLane], edge(x))
|
|
2157
|
+
} else if (!cur.v && side) {
|
|
2158
|
+
const y = yIn(cur.c)
|
|
2159
|
+
pts.push([tLane, cur.c], [tLane, y], edge(y))
|
|
2160
|
+
} else if (Math.abs(cur.c - tLane) < 0.5) {
|
|
2161
|
+
const x = xIn(cur.p)
|
|
2162
|
+
pts.push([x, cur.c], edge(x))
|
|
2163
|
+
} else {
|
|
2164
|
+
// 两道横缝之间:找一道竖缝过去
|
|
2165
|
+
let k = 0
|
|
2166
|
+
let bestD = Infinity
|
|
2167
|
+
for (let j = 0; j <= xs.length; j++) {
|
|
2168
|
+
const d = Math.abs(vLane(j).c - cur.p) + Math.abs(vLane(j).c - (T.x0 + dw / 2))
|
|
2169
|
+
if (d < bestD) { bestD = d; k = j }
|
|
2170
|
+
}
|
|
2171
|
+
const vx = at(vLane(k), o)
|
|
2172
|
+
const x = xIn(vx)
|
|
2173
|
+
pts.push([vx, cur.c], [vx, tLane], [x, tLane], edge(x))
|
|
2174
|
+
}
|
|
2175
|
+
// 去掉重复、共线的点
|
|
2176
|
+
const clean = []
|
|
2177
|
+
for (const p of pts) {
|
|
2178
|
+
const q = clean[clean.length - 1]
|
|
2179
|
+
if (q && Math.abs(q[0] - p[0]) < 0.01 && Math.abs(q[1] - p[1]) < 0.01) continue
|
|
2180
|
+
const r = clean[clean.length - 2]
|
|
2181
|
+
if (q && r && ((Math.abs(r[0] - q[0]) < 0.01 && Math.abs(q[0] - p[0]) < 0.01) || (Math.abs(r[1] - q[1]) < 0.01 && Math.abs(q[1] - p[1]) < 0.01))) clean.pop()
|
|
2182
|
+
clean.push(p)
|
|
2183
|
+
}
|
|
2184
|
+
return clean
|
|
2185
|
+
}
|
|
2186
|
+
const lenOf = (pts) => pts.slice(1).reduce((m, p, i) => m + Math.abs(p[0] - pts[i][0]) + Math.abs(p[1] - pts[i][1]), 0)
|
|
2187
|
+
// 屏里那一小段压过别的按钮(底栏、别的行)分不清是谁的线,宁可绕一点
|
|
2188
|
+
const hits = (a, b, rs, src) => rs.filter((r) => r !== src && Math.min(a[0], b[0]) < r.x + r.w - 1 && Math.max(a[0], b[0]) > r.x + 1 && Math.min(a[1], b[1]) < r.y + r.h - 1 && Math.max(a[1], b[1]) > r.y + 1).length
|
|
2189
|
+
const costOf = (pts, E, N, rs, src) => {
|
|
2190
|
+
const inS = Math.abs(pts[1][0] - pts[0][0]) + Math.abs(pts[1][1] - pts[0][1])
|
|
2191
|
+
return lenOf(pts) + (pts.length - 2) * px(46) + inS * 2 + hits(pts[0], pts[1], rs, src) * px(300) + (E === 'T' ? px(30) : 0) + (N === 'T' ? px(24) : 0)
|
|
2026
2192
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
let
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2193
|
+
// 两条线的横竖段相交几次(共用端点不算)
|
|
2194
|
+
const segs = (pts) => pts.slice(1).map((p, i) => [pts[i], p])
|
|
2195
|
+
const cross = (A, B) => {
|
|
2196
|
+
let n = 0
|
|
2197
|
+
for (const [a, b] of A) {
|
|
2198
|
+
for (const [c, d] of B) {
|
|
2199
|
+
const av = Math.abs(a[0] - b[0]) < 0.01
|
|
2200
|
+
const cv = Math.abs(c[0] - d[0]) < 0.01
|
|
2201
|
+
if (av === cv) continue
|
|
2202
|
+
const [v0, v1, h0, h1] = av ? [a, b, c, d] : [c, d, a, b]
|
|
2203
|
+
const x = v0[0]
|
|
2204
|
+
const y = h0[1]
|
|
2205
|
+
const inV = y > Math.min(v0[1], v1[1]) + 0.01 && y < Math.max(v0[1], v1[1]) - 0.01
|
|
2206
|
+
const inH = x > Math.min(h0[0], h1[0]) + 0.01 && x < Math.max(h0[0], h1[0]) - 0.01
|
|
2207
|
+
if (inV && inH) n++
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return n
|
|
2211
|
+
}
|
|
2212
|
+
/** 拐点 → 圆角折线;末尾留出箭头的长度 */
|
|
2213
|
+
const pathOf = (pts, from, a) => {
|
|
2214
|
+
const P = pts.slice(from)
|
|
2215
|
+
const last = P[P.length - 1]
|
|
2216
|
+
const prev = P[P.length - 2]
|
|
2217
|
+
const ux = Math.sign(last[0] - prev[0])
|
|
2218
|
+
const uy = Math.sign(last[1] - prev[1])
|
|
2219
|
+
P[P.length - 1] = [last[0] - ux * a, last[1] - uy * a]
|
|
2220
|
+
let d = `M${P[0][0]} ${P[0][1]}`
|
|
2221
|
+
for (let i = 1; i < P.length - 1; i++) {
|
|
2222
|
+
const [x0, y0] = P[i - 1]
|
|
2223
|
+
const [x1, y1] = P[i]
|
|
2224
|
+
const [x2, y2] = P[i + 1]
|
|
2225
|
+
const r = Math.min(px(12), Math.hypot(x1 - x0, y1 - y0) / 2, Math.hypot(x2 - x1, y2 - y1) / 2)
|
|
2226
|
+
const ax = x1 - Math.sign(x1 - x0) * r
|
|
2227
|
+
const ay = y1 - Math.sign(y1 - y0) * r
|
|
2228
|
+
const bx = x1 + Math.sign(x2 - x1) * r
|
|
2229
|
+
const by = y1 + Math.sign(y2 - y1) * r
|
|
2230
|
+
d += `L${ax} ${ay}Q${x1} ${y1} ${bx} ${by}`
|
|
2036
2231
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2232
|
+
return d + `L${P[P.length - 1][0]} ${P[P.length - 1][1]}`
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
const out = []
|
|
2236
|
+
const todo = []
|
|
2237
|
+
// 每屏看得见的按钮框(挑出发点时别从别的按钮上穿过去)
|
|
2238
|
+
const shown = new Map()
|
|
2239
|
+
const spots = (fl, S) => {
|
|
2240
|
+
const ox = S.x0 + f.pad
|
|
2241
|
+
const oy = S.y0 + f.pad + f.top
|
|
2242
|
+
return (fl.hots || [fl]).filter((l) => l.y >= -1 && l.y + l.h <= size[1] + 1 && l.x + l.w > 0 && l.x < size[0])
|
|
2243
|
+
.map((l) => ({ x: ox + Math.max(0, l.x), y: oy + l.y, w: Math.min(l.w, size[0] - Math.max(0, l.x)), h: l.h }))
|
|
2244
|
+
}
|
|
2245
|
+
for (const fl of flows) {
|
|
2246
|
+
const a = idx.get(fl.from)
|
|
2247
|
+
if (a === undefined || !pos[a]) continue
|
|
2248
|
+
if (!shown.has(fl.from)) shown.set(fl.from, [])
|
|
2249
|
+
shown.get(fl.from).push(...spots(fl, cellOf(a)))
|
|
2041
2250
|
}
|
|
2042
2251
|
for (const fl of flows) {
|
|
2043
2252
|
const a = idx.get(fl.from)
|
|
2044
2253
|
const b = idx.get(fl.to)
|
|
2045
2254
|
if (a === undefined || b === undefined || a === b || !pos[a] || !pos[b]) continue
|
|
2046
|
-
const
|
|
2047
|
-
const
|
|
2048
|
-
const ox =
|
|
2049
|
-
const oy =
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
const
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2255
|
+
const S = cellOf(a)
|
|
2256
|
+
const T = cellOf(b)
|
|
2257
|
+
const ox = S.x0 + f.pad
|
|
2258
|
+
const oy = S.y0 + f.pad + f.top
|
|
2259
|
+
// 看得见的按钮(在屏幕里、没被往下滚出去):导航的只描框;别的按钮里挑线最顺的那个出发。
|
|
2260
|
+
// 只有看不见的:从屏幕底边出发,画虚线
|
|
2261
|
+
const seen = (l) => l.y >= -1 && l.y + l.h <= size[1] + 1 && l.x + l.w > 0 && l.x < size[0]
|
|
2262
|
+
const rect = (l) => ({ x: ox + Math.max(0, l.x), y: oy + l.y, w: Math.min(l.w, size[0] - Math.max(0, l.x)), h: l.h })
|
|
2263
|
+
const hots = fl.hots || [fl]
|
|
2264
|
+
for (const l of hots) if (l.nav && seen(l)) out.push({ from: fl.from, to: fl.to, nav: true, hot: rect(l) })
|
|
2265
|
+
const cand = hots.filter((l) => !l.nav && seen(l)).map(rect)
|
|
2266
|
+
if (!cand.length && hots.some((l) => seen(l))) continue
|
|
2267
|
+
if (!cand.length && !hots.some((l) => !l.nav)) continue
|
|
2268
|
+
const rs = shown.get(fl.from) || []
|
|
2269
|
+
const same = (r, q) => Math.abs(r.x - q.x) < 0.5 && Math.abs(r.y - q.y) < 0.5 && Math.abs(r.w - q.w) < 0.5
|
|
2270
|
+
let best = null
|
|
2271
|
+
for (const src of cand.length ? cand : [{ x: ox + size[0] / 2 - 1, y: oy + size[1] - 2, w: 2, h: 2 }]) {
|
|
2272
|
+
const own = rs.find((r) => same(r, src))
|
|
2273
|
+
for (const E of cand.length ? ['L', 'R', 'B', 'T'] : ['B']) {
|
|
2274
|
+
for (const N of ['L', 'R', 'T', 'B']) {
|
|
2275
|
+
const c = costOf(route(S, T, src, E, N, 0), E, N, rs, own)
|
|
2276
|
+
if (!best || c < best.c) best = { c, E, N, src }
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
const hot = cand.length ? best.src : null
|
|
2281
|
+
todo.push({ fl, S, T, hot, src: best.src, E: best.E, N: best.N })
|
|
2282
|
+
}
|
|
2283
|
+
// 各走一道,错开几像素;先按出发点从左上到右下排,线不多时再试各种先后,挑交叉最少的
|
|
2284
|
+
todo.sort((p, q) => p.src.x + p.src.y - (q.src.x + q.src.y))
|
|
2285
|
+
const sp = px(6)
|
|
2286
|
+
const offs = (ord) => ord.map((k) => (k - (todo.length - 1) / 2) * sp)
|
|
2287
|
+
const crossings = (ord) => {
|
|
2288
|
+
const o = offs(ord)
|
|
2289
|
+
const ss = todo.map((it, i) => segs(route(it.S, it.T, it.src, it.E, it.N, o[i])))
|
|
2290
|
+
let n = 0
|
|
2291
|
+
for (let i = 0; i < ss.length; i++) for (let j = i + 1; j < ss.length; j++) n += cross(ss[i], ss[j])
|
|
2292
|
+
return n
|
|
2293
|
+
}
|
|
2294
|
+
let ord = todo.map((_, i) => i)
|
|
2295
|
+
if (todo.length > 1 && todo.length <= 6) {
|
|
2296
|
+
let low = crossings(ord)
|
|
2297
|
+
const perm = (arr, k) => {
|
|
2298
|
+
if (!low) return
|
|
2299
|
+
if (k === arr.length) {
|
|
2300
|
+
const n = crossings(arr)
|
|
2301
|
+
if (n < low) { low = n; ord = arr.slice() }
|
|
2302
|
+
return
|
|
2303
|
+
}
|
|
2304
|
+
for (let i = k; i < arr.length; i++) {
|
|
2305
|
+
;[arr[k], arr[i]] = [arr[i], arr[k]]
|
|
2306
|
+
perm(arr, k + 1)
|
|
2307
|
+
;[arr[k], arr[i]] = [arr[i], arr[k]]
|
|
2308
|
+
}
|
|
2067
2309
|
}
|
|
2068
|
-
|
|
2310
|
+
perm(ord.slice(), 0)
|
|
2069
2311
|
}
|
|
2312
|
+
const os = offs(ord)
|
|
2313
|
+
todo.forEach((it, k) => {
|
|
2314
|
+
const pts = route(it.S, it.T, it.src, it.E, it.N, os[k])
|
|
2315
|
+
const a = px(7)
|
|
2316
|
+
const q = pts[pts.length - 1]
|
|
2317
|
+
const pr = pts[pts.length - 2]
|
|
2318
|
+
out.push({
|
|
2319
|
+
from: it.fl.from, to: it.fl.to, hot: it.hot, off: !it.hot,
|
|
2320
|
+
sx: pts[0][0], sy: pts[0][1], ex: q[0], ey: q[1], ux: Math.sign(q[0] - pr[0]), uy: Math.sign(q[1] - pr[1]),
|
|
2321
|
+
d: pathOf(pts, 0, a * 0.7),
|
|
2322
|
+
// 衬底从出了屏幕那一刻开始(屏里那段不用)
|
|
2323
|
+
halo: pathOf([it.E === 'L' ? [it.S.x0, pts[0][1]] : it.E === 'R' ? [it.S.x1, pts[0][1]] : [pts[0][0], it.E === 'T' ? it.S.y0 : it.S.y1], ...pts.slice(1)], 0, a * 0.7),
|
|
2324
|
+
pts,
|
|
2325
|
+
})
|
|
2326
|
+
})
|
|
2070
2327
|
return out
|
|
2071
2328
|
}
|
|
2072
2329
|
|
|
2330
|
+
/** 连线画成什么(和框架无关的描述,画布用 h() 画出来) */
|
|
2331
|
+
function flowMarks(lines, z) {
|
|
2332
|
+
const px = (n) => n / z
|
|
2333
|
+
const marks = []
|
|
2334
|
+
for (const ln of lines) {
|
|
2335
|
+
if (ln.hot) {
|
|
2336
|
+
const r = Math.min(px(6), ln.hot.w / 2, ln.hot.h / 2)
|
|
2337
|
+
marks.push(['rect', { className: 'hot' + (ln.nav ? ' nav' : ''), x: ln.hot.x, y: ln.hot.y, width: ln.hot.w, height: ln.hot.h, rx: r, strokeWidth: px(1.5), strokeDasharray: ln.nav ? `${px(3)} ${px(2.5)}` : undefined }])
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
for (const ln of lines) {
|
|
2341
|
+
if (ln.nav) continue
|
|
2342
|
+
const a = px(7)
|
|
2343
|
+
const bx = ln.ex - ln.ux * a
|
|
2344
|
+
const by = ln.ey - ln.uy * a
|
|
2345
|
+
const vx = -ln.uy * a * 0.56
|
|
2346
|
+
const vy = ln.ux * a * 0.56
|
|
2347
|
+
marks.push(['path', { className: 'halo', d: ln.halo, strokeWidth: px(5.5) }])
|
|
2348
|
+
marks.push(['path', { className: 'ln' + (ln.off ? ' off' : ''), d: ln.d, strokeWidth: px(1.75), strokeDasharray: ln.off ? `${px(4)} ${px(3)}` : undefined }])
|
|
2349
|
+
marks.push(['path', { className: 'hd', d: `M${ln.ex} ${ln.ey}L${bx + vx} ${by + vy}L${bx - vx} ${by - vy}Z` }])
|
|
2350
|
+
marks.push(['circle', { className: 'dot', cx: ln.sx, cy: ln.sy, r: px(3.2), strokeWidth: px(1.4) }])
|
|
2351
|
+
}
|
|
2352
|
+
return marks
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2073
2355
|
// ── 小部件 ───────────────────────────────────────────────────────────────
|
|
2074
2356
|
|
|
2075
2357
|
function Skeleton({ gone }) {
|
|
@@ -2334,7 +2616,7 @@ window.__ModuleLoader__.load({
|
|
|
2334
2616
|
const seed = L.seeds.get(id) || L.seedOf.get(id) || null
|
|
2335
2617
|
L.seeds.delete(id)
|
|
2336
2618
|
L.seedOf.delete(id)
|
|
2337
|
-
postTo(id, { type: 'init', seed })
|
|
2619
|
+
postTo(id, { type: 'init', seed, relay: true }) // 改动通知由右侧栏转告(见下面「本机预览」),页面别自己开 SSE
|
|
2338
2620
|
break
|
|
2339
2621
|
}
|
|
2340
2622
|
case 'ready':
|
|
@@ -2470,6 +2752,21 @@ window.__ModuleLoader__.load({
|
|
|
2470
2752
|
refresh()
|
|
2471
2753
|
}, [chg])
|
|
2472
2754
|
|
|
2755
|
+
// 本机预览:同样按轮询到的改动转告页面,页面自己判断跟它有没有关系(有就交指纹、这里载新的一帧)。
|
|
2756
|
+
// 不让每屏各开一条 SSE:画布上到 6 屏就占满浏览器对同一地址的 6 条连接,之后的刷新全卡住
|
|
2757
|
+
const lchg = (!rem && props.remote && props.remote.changes) || []
|
|
2758
|
+
const lN = lchg.length ? lchg[lchg.length - 1].n : 0
|
|
2759
|
+
const lSeen = useRef(lN)
|
|
2760
|
+
useEffect(() => {
|
|
2761
|
+
const from = lSeen.current
|
|
2762
|
+
lSeen.current = lN
|
|
2763
|
+
if (rem || lN === from || !curRef.current) return
|
|
2764
|
+
const fresh = lchg.filter((c) => c.n > from)
|
|
2765
|
+
const unknown = lN < from || !fresh.length || lchg[0].n > from + 1 || fresh.some((c) => !(c.files || []).length)
|
|
2766
|
+
const files = unknown ? [] : [...new Set(fresh.flatMap((c) => c.files || []))]
|
|
2767
|
+
postTo(curRef.current, { type: 'files', file: files[files.length - 1] || '', files })
|
|
2768
|
+
}, [lN])
|
|
2769
|
+
|
|
2473
2770
|
// 页面没装上脚本(极少数,比如不是 HTML):载完等一会儿也换上来
|
|
2474
2771
|
const onFrameLoad = (id) => {
|
|
2475
2772
|
setTimeout(() => {
|
|
@@ -3021,7 +3318,7 @@ window.__ModuleLoader__.load({
|
|
|
3021
3318
|
// 拖背景 / 挡板 = 平移;没拖动就算点了一下
|
|
3022
3319
|
const onPointerDown = (e) => {
|
|
3023
3320
|
if (e.button !== 0 || !camRef.current) return
|
|
3024
|
-
if (e.target.closest && e.target.closest('.ddp-zoom, .ddp-act')) return
|
|
3321
|
+
if (e.target.closest && e.target.closest('.ddp-zoom, .ddp-act, .ddp-take')) return
|
|
3025
3322
|
const keyEl = e.target.closest ? e.target.closest('[data-key]') : null
|
|
3026
3323
|
const d = { x: e.clientX, y: e.clientY, cam: camRef.current, moved: false, key: keyEl ? keyEl.getAttribute('data-key') : '' }
|
|
3027
3324
|
const move = (ev) => {
|
|
@@ -3074,7 +3371,7 @@ window.__ModuleLoader__.load({
|
|
|
3074
3371
|
className: 'ddp-cell' + (on ? ' sel' : ''),
|
|
3075
3372
|
style: { left: L.pos[i].x, top: L.pos[i].y, width: cw, height: ch },
|
|
3076
3373
|
},
|
|
3077
|
-
lab(sc.key, [h('span', { key: 's' }, labelOf(sc)), on && !picking && props.actions ? h('span', { key: 'a', className: 'ddp-act' }, ...props.actions(sc)) : null], cw),
|
|
3374
|
+
lab(sc.key, [h('span', { key: 's' }, labelOf(sc)), !picking && props.badge ? props.badge(sc) : null, on && !picking && props.actions ? h('span', { key: 'a', className: 'ddp-act' }, ...props.actions(sc)) : null], cw),
|
|
3078
3375
|
props.render(sc, z, i),
|
|
3079
3376
|
picking ? null : h('div', { className: 'ddp-shield', 'data-key': sc.key, title: items ? '' : t('openScreen'), style: { borderRadius: fr.r } })))
|
|
3080
3377
|
})
|
|
@@ -3087,15 +3384,11 @@ window.__ModuleLoader__.load({
|
|
|
3087
3384
|
}
|
|
3088
3385
|
let flowSvg = null
|
|
3089
3386
|
if (cam && L && props.flows && props.flows.length && !picking) {
|
|
3090
|
-
|
|
3387
|
+
// 只画选中那屏通向哪(不画哪些通向它),底栏、返回这种每屏都有的导航只描框
|
|
3388
|
+
const lines = sel ? flowLines(props.flows.filter((x) => x.from === sel), screens.map((x) => x.key), L.pos, kind, size, { lift: LAB / z, z }) : []
|
|
3091
3389
|
if (lines.length) {
|
|
3092
|
-
const sw = 2 / z
|
|
3093
|
-
const a = 9 / z
|
|
3094
3390
|
flowSvg = h('svg', { key: '__flows', className: 'ddp-flows', width: 1, height: 1 },
|
|
3095
|
-
...lines.map((
|
|
3096
|
-
h('path', { d: ln.d, strokeWidth: sw }),
|
|
3097
|
-
h('path', { className: 'hd', d: `M${ln.ex} ${ln.ey}l${-a * ln.tip} ${-a * 0.62}v${a * 1.24}z` }),
|
|
3098
|
-
h('circle', { cx: ln.sx, cy: ln.sy, r: 4.5 / z, strokeWidth: 1.5 / z }))))
|
|
3391
|
+
...flowMarks(lines, z).map(([tag, attrs], i) => h(tag, { key: i, ...attrs })))
|
|
3099
3392
|
}
|
|
3100
3393
|
}
|
|
3101
3394
|
return h('div', {
|
|
@@ -3213,6 +3506,12 @@ window.__ModuleLoader__.load({
|
|
|
3213
3506
|
return () => clearTimeout(id)
|
|
3214
3507
|
}, [fresh, writingNew, M.lastNew])
|
|
3215
3508
|
const labelOf = (sc) => (infoOf(ik(sc)) || {}).title || sc.label
|
|
3509
|
+
// 画布上的标题去掉几屏都带的产品名(「首页 · 梅林练」→「首页」);产品名留给导出的展示图做大标题
|
|
3510
|
+
const shortOf = (sc) => {
|
|
3511
|
+
const sp = splitTitles(shown.map(labelOf))
|
|
3512
|
+
const i = shown.indexOf(sc)
|
|
3513
|
+
return (sp.brand && i >= 0 && sp.labels[i]) || labelOf(sc)
|
|
3514
|
+
}
|
|
3216
3515
|
const anyDark = Object.keys(info).some((k) => k.startsWith(site + '|') && info[k].dark)
|
|
3217
3516
|
const fileTarget = !!(st && st.target && st.target.kind === 'file')
|
|
3218
3517
|
// 远程看(手机走外网等):本地文件的预览由服务端打包传过来(见 Screen),目录里改了哪些文件也一起带过去
|
|
@@ -3337,9 +3636,15 @@ window.__ModuleLoader__.load({
|
|
|
3337
3636
|
try {
|
|
3338
3637
|
if (ia && typeof ia.insertText === 'function') {
|
|
3339
3638
|
const span = typeof ia.captureInsertion === 'function' ? ia.captureInsertion() : undefined
|
|
3340
|
-
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3639
|
+
const el = composerEl()
|
|
3640
|
+
const all = composerText(el)
|
|
3641
|
+
const at = span ? span.start : 0
|
|
3642
|
+
const ok = all != null && span && span.end <= all.length
|
|
3643
|
+
const p = padInsert(text, ok ? all.slice(0, at) : null, ok ? all.slice(span.end) : null, at, done === 'inserted')
|
|
3644
|
+
if (ia.insertText(p.text, span)) {
|
|
3645
|
+
try { focusAfter(el, p.text, p.back) } catch { /* 用户自己点一下输入框也行 */ }
|
|
3646
|
+
return say(t(done))
|
|
3647
|
+
}
|
|
3343
3648
|
}
|
|
3344
3649
|
} catch (e) {
|
|
3345
3650
|
console.warn('[dsh-design-studio] insertText 失败', e)
|
|
@@ -3676,8 +3981,12 @@ window.__ModuleLoader__.load({
|
|
|
3676
3981
|
insert(g.letter ? t('pickText', { name: labelOf(sc), where, others, base: g.stem }) : t('pickKeep', { name: labelOf(sc), where, others }), 'insertedSend')
|
|
3677
3982
|
}
|
|
3678
3983
|
|
|
3984
|
+
// 几个方向并排时每屏标题旁都挂着「就要这个」,不用先点中那一屏才看得到
|
|
3985
|
+
const badge = (sc) => !sc.v && takesOf(shown.map((x) => x.key), sc.key)
|
|
3986
|
+
? h('button', { key: 'p', className: 'ddp-take', title: t('pickThis'), onClick: () => pickDirection(sc) }, h(I.check), h('span', null, t('pickShort')))
|
|
3987
|
+
: null
|
|
3988
|
+
|
|
3679
3989
|
const actions = (sc) => [
|
|
3680
|
-
!sc.v && takesOf(shown.map((x) => x.key), sc.key) ? h('button', { key: 'p', className: 'ddp-btn', title: t('pickThis'), onClick: () => pickDirection(sc) }, h(I.check)) : null,
|
|
3681
3990
|
h('button', { key: 'x', className: 'ddp-btn', title: t('exThis'), onClick: () => doExport(FRAMED[kind] ? 'frame' : 'raw', sc.key) }, h(I.download)),
|
|
3682
3991
|
sc.v ? null : h('button', { key: 'v', className: 'ddp-btn', title: t('variants'), onClick: () => askVariants(sc) }, h(I.sparkle)),
|
|
3683
3992
|
h('button', { key: 'o', className: 'ddp-btn', title: t('openScreen'), onClick: () => change({ view: 'single', focus: sc.key }) }, h(I.single)),
|
|
@@ -3984,6 +4293,8 @@ window.__ModuleLoader__.load({
|
|
|
3984
4293
|
onClick: () => post('open', { session: sid, target: st.hint }).then((s) => { if (poll) poll.set(s) }, (e) => say(t('openFailed', { msg: e.message }))),
|
|
3985
4294
|
}, t('hintOpen', { name: st.hint.split(/[\\/]/).pop() })) : null,
|
|
3986
4295
|
h('ol', { className: 'ddp-steps' }, h('li', null, t('step1')), h('li', null, t('step2')), h('li', null, t('step3'))),
|
|
4296
|
+
h('div', { className: 'ddp-tries' }, h('span', null, t('tryHead')),
|
|
4297
|
+
['try1', 'try2', 'try3'].map((k) => h('button', { key: k, className: 'ddp-try', onClick: () => insert(t(k), 'insertedSend') }, t(k)))),
|
|
3987
4298
|
h('div', { className: 'ddp-stylepick' }, h('b', null, t('stylesHead')), h('p', null, t('stylesTip')), h(StyleCards, { t, onPick: (x) => insert(formatStyle(t, x, false)) })),
|
|
3988
4299
|
h('p', null, t('emptyHint')),
|
|
3989
4300
|
h(AddressForm, { t, sid, className: 'ddp-form', onDone: () => {} }),
|
|
@@ -4091,12 +4402,13 @@ window.__ModuleLoader__.load({
|
|
|
4091
4402
|
t,
|
|
4092
4403
|
screens: shown,
|
|
4093
4404
|
actions,
|
|
4405
|
+
badge,
|
|
4094
4406
|
kind,
|
|
4095
4407
|
size,
|
|
4096
4408
|
ghost,
|
|
4097
4409
|
picking: picking || textEdit || styleEdit,
|
|
4098
4410
|
sel: sel || focus,
|
|
4099
|
-
labelOf,
|
|
4411
|
+
labelOf: shortOf,
|
|
4100
4412
|
reveal,
|
|
4101
4413
|
inset: ck || styleEdit || panel || styles || pane ? 296 : 0, // 右边的面板 276 宽,离边 10
|
|
4102
4414
|
render: (sc, z) => renderScreen(sc, z, false),
|
|
@@ -4244,7 +4556,7 @@ window.__ModuleLoader__.load({
|
|
|
4244
4556
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
4245
4557
|
exports.inject = INJECT
|
|
4246
4558
|
// 测试用
|
|
4247
|
-
exports._test = { STYLES, formatStyle, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
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 }
|
|
4248
4560
|
return module.exports
|
|
4249
4561
|
},
|
|
4250
4562
|
})
|