dsh-mesh-ui 0.0.16 → 0.0.18

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.
Files changed (2) hide show
  1. package/lib/client.js +156 -31
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -313,12 +313,22 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
313
313
  .dm-details > summary::-webkit-details-marker { display:none; }
314
314
  .dm-details > summary::before { content:'▸'; font-size:10px; opacity:.5; transition:transform .12s; }
315
315
  .dm-details[open] > summary::before { transform:rotate(90deg); }
316
- .dm-details > .dm-body { padding:0 12px 11px; }
316
+ .dm-details > .dm-details-body { padding:0 12px 11px; }
317
317
  .dm-table { width:100%; border-collapse:collapse; }
318
318
  .dm-table th { text-align:left; font-weight:600; font-size:11px; opacity:.5; padding:2px 8px 5px 0; border-bottom:1px solid rgba(128,128,128,.2); white-space:nowrap; }
319
319
  .dm-table td { padding:5px 8px 5px 0; border-bottom:1px solid rgba(128,128,128,.1); font-size:12px; }
320
320
  .dm-table tr:last-child td { border-bottom:none; }
321
- .dm-trunc { max-width:190px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:bottom; }
321
+ /* 2026-11-10:委托台账表改为固定列宽 + 单行省略;非固定布局下 5 列会把「节点/任务」压到
322
+ 每行一个字,就是用户看到的竖排。完整内容看 title 悬浮提示。 */
323
+ .dm-details .dm-table { table-layout:fixed; min-width:420px; }
324
+ .dm-table-wrap { overflow-x:auto; }
325
+ .dm-details .dm-table th, .dm-details .dm-table td { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
326
+ .dm-details .dm-table th:nth-child(1), .dm-details .dm-table td:nth-child(1) { width:16%; }
327
+ .dm-details .dm-table th:nth-child(2), .dm-details .dm-table td:nth-child(2) { width:18%; }
328
+ .dm-details .dm-table th:nth-child(3), .dm-details .dm-table td:nth-child(3) { width:22%; }
329
+ .dm-details .dm-table th:nth-child(4), .dm-details .dm-table td:nth-child(4) { width:10%; }
330
+ /* 目标列不写死百分比:让它吃掉剩余空间(单元格右内边距不参与百分比,写死 34% 会溢出 1 条横向滚动条) */
331
+ .dm-trunc { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:bottom; }
322
332
  .dm-instrow { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:6px 0; border-bottom:1px solid rgba(128,128,128,.1); }
323
333
  .dm-instrow:last-child { border-bottom:none; }
324
334
  .dm-alertrow { display:flex; gap:8px; align-items:baseline; padding:5px 0; border-bottom:1px dashed rgba(128,128,128,.16); font-size:12px; }
@@ -563,10 +573,24 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
563
573
  var s0 = useState({ scale: 1, x: 0, y: 0 }); var view = s0[0]; var setView = s0[1]
564
574
  var s1 = useState(false); var dragging = s1[0]; var setDragging = s1[1]
565
575
  // 2026-11-09:三通道筛选(局域网/公网/樱花)
566
- var filterState = useState({ lan: true, public: true, sakura: true })
576
+ // 2026-11-10:筛选状态持久化;刷新/切页面后不再丢。
577
+ var FILTER_STORE_KEY = 'dsh-mesh-ui.filters.v1'
578
+ var filterState = useState(function () {
579
+ try {
580
+ var saved = JSON.parse(window.localStorage.getItem(FILTER_STORE_KEY) || '{}') || {}
581
+ return { lan: saved.lan !== false, public: saved.public !== false, sakura: saved.sakura !== false }
582
+ } catch (_) { return { lan: true, public: true, sakura: true } }
583
+ })
567
584
  var showFilters = filterState[0]
568
585
  var setShowFilters = filterState[1]
569
- function toggleFilter(k) { setShowFilters(function (prev) { var n = Object.assign({}, prev); n[k] = !n[k]; return n }) }
586
+ function toggleFilter(k) {
587
+ setShowFilters(function (prev) {
588
+ var n = Object.assign({}, prev)
589
+ n[k] = !n[k]
590
+ try { window.localStorage.setItem(FILTER_STORE_KEY, JSON.stringify(n)) } catch (_) { /* ignore */ }
591
+ return n
592
+ })
593
+ }
570
594
  // fit 用最新布局的 ref(拖拽中不重算视角;只有初始化 / 窗口变化 / ⟲ 重置 / ⤢ 自适应才重算)
571
595
  var effRef = useRef(effLayout)
572
596
  effRef.current = effLayout
@@ -1412,9 +1436,24 @@ function deviceLinkState(data, peer, lanMap) {
1412
1436
  }
1413
1437
 
1414
1438
  function Details(props) {
1415
- return h('details', { className: 'dm-details', open: Boolean(props.open) },
1439
+ var storeKey = 'dsh-mesh-ui.details.' + encodeURIComponent(String(props.title || ''))
1440
+ var sOpen = useState(function () {
1441
+ try {
1442
+ var saved = window.localStorage.getItem(storeKey)
1443
+ return saved == null ? Boolean(props.open) : saved === '1'
1444
+ } catch (_) { return Boolean(props.open) }
1445
+ })
1446
+ var open = sOpen[0]; var setOpen = sOpen[1]
1447
+ function onToggle(event) {
1448
+ var next = Boolean(event.currentTarget.open)
1449
+ setOpen(next)
1450
+ try { window.localStorage.setItem(storeKey, next ? '1' : '0') } catch (_) { /* ignore */ }
1451
+ }
1452
+ return h('details', { className: 'dm-details', open: open, onToggle: onToggle },
1416
1453
  h('summary', null, props.title, props.badge != null ? h('span', { className: 'dm-pill muted' }, String(props.badge)) : null),
1417
- h('div', { className: 'dm-body' }, props.children)
1454
+ // 2026-11-10:折叠面板正文用独立容器整栏铺满;原先借用 .dm-body(62% 双栏 grid)
1455
+ // 会把「委托台账」这类单列表格压到 300px,中文被逐字折成竖排。
1456
+ h('div', { className: 'dm-details-body' }, props.children)
1418
1457
  )
1419
1458
  }
1420
1459
 
@@ -1445,17 +1484,20 @@ function deviceLinkState(data, peer, lanMap) {
1445
1484
  if (!list.length) return h('div', { className: 'dm-empty' }, '暂无委托记录')
1446
1485
  return h('div', null,
1447
1486
  h('div', { className: 'dm-sub', style: { marginBottom: 5 } }, '共 ' + (d.total || 0) + ' 条 · ' + Object.keys(stats).map(function (k) { return k + ' ' + stats[k] }).join(' · ')),
1448
- h('table', { className: 'dm-table' },
1449
- h('thead', null, h('tr', null, h('th', null, '状态'), h('th', null, '节点'), h('th', null, '任务'), h('th', null, '耗时'), h('th', null, '目标'))),
1450
- h('tbody', null, list.map(function (task) {
1451
- return h('tr', { key: task.taskId },
1452
- h('td', null, stateChip(task.state)),
1453
- h('td', null, nodeLabel(task.node)),
1454
- h('td', { className: 'dm-mono', style: { fontSize: 11 } }, String(task.taskId || '').replace(/^T-\d{8}-/, 'T-…')),
1455
- h('td', { className: 'dm-mono' }, dur(task.durationSec)),
1456
- h('td', null, h('span', { className: 'dm-trunc', title: task.goal }, task.goal || ''))
1457
- )
1458
- }))
1487
+ // 2026-11-10:外层给横向滚动兜底——栏窄到连固定列宽都放不下时滚动,而不是压缩成竖排。
1488
+ h('div', { className: 'dm-table-wrap' },
1489
+ h('table', { className: 'dm-table' },
1490
+ h('thead', null, h('tr', null, h('th', null, '状态'), h('th', null, '节点'), h('th', null, '任务'), h('th', null, '耗时'), h('th', null, '目标'))),
1491
+ h('tbody', null, list.map(function (task) {
1492
+ return h('tr', { key: task.taskId },
1493
+ h('td', null, stateChip(task.state)),
1494
+ h('td', { title: nodeLabel(task.node) }, nodeLabel(task.node)),
1495
+ h('td', { className: 'dm-mono', style: { fontSize: 11 }, title: String(task.taskId || '') }, String(task.taskId || '').replace(/^T-\d{8}-/, 'T-…')),
1496
+ h('td', { className: 'dm-mono' }, dur(task.durationSec)),
1497
+ h('td', null, h('span', { className: 'dm-trunc', title: task.goal }, task.goal || ''))
1498
+ )
1499
+ }))
1500
+ )
1459
1501
  )
1460
1502
  )
1461
1503
  }
@@ -2414,17 +2456,26 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2414
2456
  }
2415
2457
 
2416
2458
  // 2026-11-10:AI 网络现在是左侧栏主页面,不再受设置面板的窄容器约束。
2417
- // 主区域自身没有内边距,这里给页面统一留白并限制最大阅读宽度。
2459
+ // 2026-11-10(二次):内边距从 .dm-page 挪到滚动容器上——滚动条属于滚动容器,只有在滚动容器
2460
+ // 铺满整页时才会像 DSH 对话区那样贴到最右边;内容留白靠滚动容器自己的 padding 提供。
2418
2461
  var PAGE_CSS = `
2419
- .dm-page { box-sizing:border-box; width:100%; height:100%; overflow:hidden; padding:clamp(16px, 2.2vw, 30px) clamp(18px, 2.6vw, 42px) 0; }
2462
+ .dm-page { box-sizing:border-box; width:100%; height:100%; overflow:hidden; padding:0; --dm-pad-x:clamp(18px, 2.6vw, 42px); --dm-pad-y:clamp(16px, 2.2vw, 30px); --dm-scroll-inset:2px; }
2420
2463
  .dm-content-shell { width:100%; height:100%; position:relative; --dm-content-w:920px; }
2421
- .dm-content-scroll { height:100%; overflow:auto; padding-bottom:36px; scrollbar-gutter:stable; }
2464
+ /* 纵向滚动条:对齐 DSH 对话区 wSkVaW_scrollBody 的观感——滚动容器 margin-right:2px
2465
+ (滚动条离面板边 2px,不贴死),滑块 track 留 2px(滑块上下不顶到边)。
2466
+ 滚动条只占右侧会把内容挤得偏左,所以左侧补等宽内边距(--dm-sb-pad)。 */
2467
+ /* border-box 必须加:height:100% + 上下 padding 否则把滚动容器撑出面板高度,
2468
+ 底部 padding 和页脚会被 .dm-page 的 overflow:hidden 裁掉(2026-11-10 实测 1051 > 985)。 */
2469
+ .dm-content-scroll { box-sizing:border-box; height:100%; overflow:auto; scrollbar-gutter:stable; margin-right:var(--dm-scroll-inset); padding:var(--dm-pad-y) var(--dm-pad-x) 36px; padding-left:calc(var(--dm-pad-x) + var(--dm-sb-pad, 0px) + var(--dm-scroll-inset)); }
2470
+ .dm-content-scroll::-webkit-scrollbar-track { margin:2px; }
2422
2471
  .dm-content { width:min(100%, var(--dm-content-w)); max-width:100%; margin:0 auto; box-sizing:border-box; container-type:inline-size; container-name:dm-content; }
2423
2472
  .dm-content > .dm-root { width:100%; max-width:100%; margin:0 auto; }
2424
- /* 与 DSH 对话区一致的宽度手柄:位于内容两侧外 24px,平时隐藏,hover/拖拽显示 2px 渐变线 */
2425
- .dm-width-handle { z-index:0; position:absolute; top:0; bottom:0; cursor:col-resize; width:clamp(0px, calc((100% - var(--dm-content-w)) / 2 - 48px), 10px); }
2426
- .dm-width-handle[data-side="left"] { right:calc(50% + var(--dm-content-w) / 2 + 24px); }
2427
- .dm-width-handle[data-side="right"] { left:calc(50% + var(--dm-content-w) / 2 + 24px); }
2473
+ /* 宽度手柄:位置由 JS 实测的「内容边距 + 页面留白」算出高亮线中心点,写进 --dm-bar-l / --dm-bar-r。
2474
+ 高亮线在 10px 手柄内偏内侧 5px(::after 4px + 2px 线宽的一半),所以手柄 = 中心点 ∓ 5px。
2475
+ 这样留白窄时(拖到最大)高亮线自动居中在「内容边 ↔ 页面边」中间,留白宽时保持 24px 间隙。 */
2476
+ .dm-width-handle { z-index:1; position:absolute; top:0; bottom:0; cursor:col-resize; width:10px; }
2477
+ .dm-width-handle[data-side="left"] { left:calc(var(--dm-bar-l, -100px) - 5px); }
2478
+ .dm-width-handle[data-side="right"] { right:calc(var(--dm-bar-r, -100px) - 5px); }
2428
2479
  .dm-width-handle::after { content:''; pointer-events:none; opacity:0; position:absolute; top:0; bottom:0; width:2px; border-radius:2px; background:linear-gradient(to bottom, transparent calc(var(--dm-width-handle-pointer-y, 50%) - 36px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) - 8px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) + 8px), transparent calc(var(--dm-width-handle-pointer-y, 50%) + 36px)); }
2429
2480
  .dm-width-handle[data-side="left"]::after { right:4px; }
2430
2481
  .dm-width-handle[data-side="right"]::after { left:4px; }
@@ -2454,12 +2505,29 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2454
2505
  })
2455
2506
  var pref = prefState[0]; var setPref = prefState[1]
2456
2507
  var shellState = useState(0); var containerW = shellState[0]; var setContainerW = shellState[1]
2508
+ // 滚动条实际占位宽度(px):左侧补等宽内边距,让内容在 shell 上真正居中。
2509
+ var sbState = useState(0); var sbPad = sbState[0]; var setSbPad = sbState[1]
2510
+ // 手柄几何:内容元素到 shell 左右边的实测 px(null = 还没量到)。
2511
+ var geoState = useState(null); var geo = geoState[0]; var setGeo = geoState[1]
2457
2512
  var shellRef = useRef(null)
2458
2513
 
2514
+ // 2026-11-10:宽度基准 =「内容可用宽度」= 滚动容器内容盒宽(clientWidth - 左右 padding);
2515
+ // 滚动条贴页面右边,其占位由 CSS 在左侧补等宽 padding 抵消(--dm-sb-pad)。
2459
2516
  useEffect(function () {
2460
2517
  var el = shellRef.current
2461
2518
  if (!el) return
2462
- function measure() { setContainerW(el.clientWidth || 0) }
2519
+ function measure() {
2520
+ var scroller = el.querySelector('.dm-content-scroll')
2521
+ if (!scroller) { setContainerW(el.clientWidth || 0); return }
2522
+ var sb = Math.max(0, (scroller.offsetWidth || 0) - (scroller.clientWidth || 0))
2523
+ var scs = getComputedStyle(scroller)
2524
+ var padL = parseFloat(scs.paddingLeft) || 0
2525
+ var padR = parseFloat(scs.paddingRight) || 0
2526
+ // 内容盒宽 = clientWidth - 实际左右 padding(左侧含滚动条补偿与 2px 滚动条外留白)
2527
+ var avail = Math.max(0, (scroller.clientWidth || 0) - padL - padR)
2528
+ setContainerW(avail)
2529
+ setSbPad(function (prev) { return Math.abs(prev - sb) >= 0.5 ? sb : prev })
2530
+ }
2463
2531
  measure()
2464
2532
  if (typeof ResizeObserver !== 'undefined') {
2465
2533
  var observer = new ResizeObserver(measure)
@@ -2468,19 +2536,67 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2468
2536
  }
2469
2537
  window.addEventListener('resize', measure)
2470
2538
  return function () { window.removeEventListener('resize', measure) }
2471
- }, [])
2539
+ }, [sbPad])
2540
+
2541
+ // 手柄定位:高亮线在「内容边 ↔ 页面边」的留白里自动居中(留白宽时退回 24px 间隙)。
2542
+ // 滚动容器已铺满整页,所以内容到 shell 左右边的距离就是完整留白(右侧含滚动条)。 */
2543
+ useEffect(function () {
2544
+ var el = shellRef.current
2545
+ if (!el) return
2546
+ var raf = 0
2547
+ function sync() {
2548
+ var content = el.querySelector('.dm-content')
2549
+ if (!content) return
2550
+ var sr = el.getBoundingClientRect()
2551
+ var cr = content.getBoundingClientRect()
2552
+ // 留白量到 shell(页面)边:滚动条外那 2px 也算留白,高亮线才居中在视觉留白里
2553
+ var offL = cr.left - sr.left
2554
+ var offR = sr.right - cr.right
2555
+ // 高亮线中心到内容边的距离:留白的一半,但夹在 [12, 29](29 = 24px 间隙 + 5px 线中心偏移)
2556
+ var gapL = Math.max(12, Math.min(29, offL / 2))
2557
+ var gapR = Math.max(12, Math.min(29, offR / 2))
2558
+ var bl = Math.round((offL - gapL) * 10) / 10
2559
+ var br = Math.round((offR - gapR) * 10) / 10
2560
+ setGeo(function (prev) {
2561
+ if (prev && Math.abs(prev.l - bl) < 0.5 && Math.abs(prev.r - br) < 0.5) return prev
2562
+ return { l: bl, r: br }
2563
+ })
2564
+ }
2565
+ sync()
2566
+ raf = window.requestAnimationFrame(sync)
2567
+ if (typeof ResizeObserver !== 'undefined') {
2568
+ var observer = new ResizeObserver(sync)
2569
+ var content = el.querySelector('.dm-content')
2570
+ if (content) observer.observe(content)
2571
+ observer.observe(el)
2572
+ return function () { window.cancelAnimationFrame(raf); observer.disconnect() }
2573
+ }
2574
+ return function () { window.cancelAnimationFrame(raf) }
2575
+ }, [contentW, sbPad])
2472
2576
 
2473
- // DSH 对话区公式:默认 clamp(680, 64% 容器, 920);用户值 [640, 容器-176]。
2577
+ // 默认宽度 = 内容可用宽度 x DEFAULT_W_RATIO;拖拽限制在 [640, 可用宽度],
2578
+ // 双击手柄清掉偏好后回到默认。想留一点内边距就把 DEFAULT_W_RATIO 调到 0.98/0.96。
2579
+ var DEFAULT_W_RATIO = 1
2474
2580
  var widthNow = containerW || 920
2475
- var maxW = Math.max(640, widthNow - 176)
2476
- var contentW = pref > 0 ? Math.min(Math.max(pref, 640), maxW) : Math.max(680, Math.min(widthNow * 0.64, 920))
2581
+ var maxW = Math.max(640, widthNow)
2582
+ var contentW = pref > 0 ? Math.min(Math.max(pref, 640), maxW) : Math.round(widthNow * DEFAULT_W_RATIO)
2477
2583
 
2478
2584
  function beginWidthDrag(side, event) {
2479
2585
  event.preventDefault()
2480
2586
  var handle = event.currentTarget
2481
2587
  var shell = handle.closest ? handle.closest('.dm-content-shell') : (handle.parentElement && handle.parentElement.parentElement)
2482
2588
  if (!shell) return
2483
- var max = Math.max(640, (shell.clientWidth || 920) - 176)
2589
+ var scroller = shell.querySelector('.dm-content-scroll')
2590
+ var max = 920
2591
+ if (scroller) {
2592
+ var scs2 = getComputedStyle(scroller)
2593
+ var padL2 = parseFloat(scs2.paddingLeft) || 0
2594
+ var padR2 = parseFloat(scs2.paddingRight) || 0
2595
+ max = Math.max(0, (scroller.clientWidth || 0) - padL2 - padR2)
2596
+ } else if (shell.clientWidth) {
2597
+ max = shell.clientWidth
2598
+ }
2599
+ max = Math.max(640, max || 920)
2484
2600
  var base = contentW
2485
2601
  var originX = event.clientX
2486
2602
  var latest = contentW
@@ -2517,7 +2633,16 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2517
2633
 
2518
2634
  return h('div', { className: 'dm-page' },
2519
2635
  h('style', null, PAGE_CSS),
2520
- h('div', { className: 'dm-content-shell', ref: shellRef, style: { '--dm-content-w': contentW + 'px' } },
2636
+ h('div', {
2637
+ className: 'dm-content-shell',
2638
+ ref: shellRef,
2639
+ style: {
2640
+ '--dm-content-w': contentW + 'px',
2641
+ '--dm-sb-pad': sbPad + 'px',
2642
+ '--dm-bar-l': (geo ? geo.l : -100) + 'px',
2643
+ '--dm-bar-r': (geo ? geo.r : -100) + 'px'
2644
+ }
2645
+ },
2521
2646
  h('div', { className: 'dm-width-handle', 'data-side': 'left', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('left', event) }, onDoubleClick: resetContentWidth }),
2522
2647
  h('div', { className: 'dm-width-handle', 'data-side': 'right', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('right', event) }, onDoubleClick: resetContentWidth }),
2523
2648
  h('div', { className: 'dm-content-scroll' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mesh-ui",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "DSH sidebar page for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the installed dsh-mesh-node package.",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",