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/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 path{fill:none;stroke:${ACCENT};stroke-linecap:round;transition:opacity .2s}
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 circle{fill:${ACCENT};stroke:#fff}
933
- .ddp-flows g.dim{opacity:.45}
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
- if (!best || z > best.z + 1e-9) best = { cols, rows, z }
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 seen = new Set()
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
- for (const sc of screens) {
1983
- for (const l of linksOf(sc) || []) {
1984
- const to = keyOfPath(l.path, file)
1985
- if (to === sc.key || !keys.has(to)) continue
1986
- const k = sc.key + '>' + to
1987
- if (seen.has(k)) continue
1988
- seen.add(k)
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 gaps = xs.slice(1).map((x, i) => x - xs[i] - dw).filter((x) => x > 0)
2011
- const g = (gaps.length ? Math.min(...gaps) : 64) / 2 // 竖着走的线放在两列中间
2012
- const r = Math.min(g * 0.7, 24) // 拐角圆一点
2013
- const crosses = (pts, skip) => boxes.some((b, i) => b && !skip.includes(i) && pts.some(([x, y]) => x > b.x0 + 2 && x < b.x1 - 2 && y > b.y0 + 2 && y < b.y1 - 2))
2014
- const bez = (p0, p1, p2, p3) => Array.from({ length: 23 }, (_, i) => {
2015
- const t = (i + 1) / 24
2016
- const u = 1 - t
2017
- return [0, 1].map((j) => u * u * u * p0[j] + 3 * u * u * t * p1[j] + 3 * u * t * t * p2[j] + t * t * t * p3[j])
2018
- })
2019
- /** 横着走那一道的高度:同一排 → 这一排标题上面;不同排 → 下面那排和上一排之间的空当 */
2020
- function laneOf(ya, yb) {
2021
- const R = Math.max(ya, yb)
2022
- const prev = ys.filter((y) => y < R - 1).pop()
2023
- if (prev === undefined) return R - lift - Math.max(16, g * 0.6)
2024
- const bottom = prev + dh
2025
- return bottom + Math.max(R - lift - bottom, 12) / 2
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
- function detour(sx, sy, hOut, cx1, lane, cx2, ey, hIn, ex) {
2029
- const sgn = (v) => (v > 0 ? 1 : v < 0 ? -1 : 0)
2030
- let d = `M${sx} ${sy}H${cx1 - hOut * r}`
2031
- const corner = (x, y, dx, dy) => `Q${x} ${y} ${x + dx * r} ${y + dy * r}`
2032
- const hl = sgn(cx2 - cx1)
2033
- if (!hl) {
2034
- const v = sgn(ey - sy)
2035
- return d + corner(cx1, sy, 0, v) + `V${ey - v * r}` + corner(cx1, ey, hIn, 0) + `H${ex}`
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
- const v1 = sgn(lane - sy)
2038
- const v2 = sgn(ey - lane)
2039
- return d + corner(cx1, sy, 0, v1) + `V${lane - v1 * r}` + corner(cx1, lane, hl, 0)
2040
- + `H${cx2 - hl * r}` + corner(cx2, lane, 0, v2) + `V${ey - v2 * r}` + corner(cx2, ey, hIn, 0) + `H${ex}`
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 pa = pos[a]
2047
- const pb = pos[b]
2048
- const ox = pa.x + f.pad
2049
- const oy = pa.y + f.pad + f.top
2050
- const dir = pb.x > pa.x + 1 ? 1 : pb.x < pa.x - 1 ? -1 : 0
2051
- const n = incoming.get(fl.to) || 0
2052
- incoming.set(fl.to, n + 1)
2053
- const sy = oy + Math.max(0, Math.min(size[1], fl.y + fl.h / 2))
2054
- const ey = pb.y + f.pad + f.top + size[1] * Math.min(0.85, 0.32 + n * 0.09)
2055
- const sx = ox + Math.max(0, Math.min(size[0], dir > 0 ? fl.x + fl.w : fl.x))
2056
- // 先在这一屏里水平走到边上(不斜着划过画面),出了屏再弯到目标
2057
- const edge = dir > 0 ? pa.x + dw : pa.x
2058
- const ex = dir < 0 ? pb.x + dw : pb.x
2059
- const k = Math.max(90, Math.abs(ex - edge) * 0.42)
2060
- const c1 = dir > 0 ? edge + k : edge - k - (dir ? 0 : 60)
2061
- const c2 = dir < 0 ? ex + k : ex - k - (dir ? 0 : 60)
2062
- let d = `M${sx} ${sy}H${edge}C${c1} ${sy} ${c2} ${ey} ${ex} ${ey}`
2063
- if (crosses(bez([edge, sy], [c1, sy], [c2, ey], [ex, ey]), [a, b])) {
2064
- const hOut = dir || -1
2065
- const hIn = dir || 1
2066
- d = detour(sx, sy, hOut, edge + hOut * g, laneOf(pa.y, pb.y), ex - hIn * g, ey, hIn, ex)
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
- out.push({ from: fl.from, to: fl.to, sx, sy, ex, ey, tip: dir < 0 ? -1 : 1, d })
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
- const lines = flowLines(props.flows, screens.map((x) => x.key), L.pos, kind, size, { lift: LAB / z }).filter((ln) => !sel || ln.from === sel || ln.to === sel)
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((ln, i) => h('g', { key: i, className: ln.from === sel ? '' : 'dim' },
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 lead = span && span.start > 0 ? '\n' : ''
3342
- if (ia.insertText(lead + text, span)) return say(t(done))
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
  })