dsh-declutter 0.1.2 → 0.1.3

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/package.json +4 -4
  2. package/src/client.js +253 -175
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-declutter",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -44,11 +44,11 @@
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
47
- "url": "https://github.com/DDDMUC/dsh-sidebar-declutter.git"
47
+ "url": "https://github.com/DDDMUC/dsh-declutter.git"
48
48
  },
49
- "homepage": "https://github.com/DDDMUC/dsh-sidebar-declutter#readme",
49
+ "homepage": "https://github.com/DDDMUC/dsh-declutter#readme",
50
50
  "bugs": {
51
- "url": "https://github.com/DDDMUC/dsh-sidebar-declutter/issues"
51
+ "url": "https://github.com/DDDMUC/dsh-declutter/issues"
52
52
  },
53
53
  "description": "隐藏侧栏与会话菜单行,按 id 管理面板,可拖动排序。纯样式注入,卸载即还原。"
54
54
  }
package/src/client.js CHANGED
@@ -36,37 +36,37 @@ window.__ModuleLoader__.load({
36
36
  const ENABLE_ROW_CONFIG = true
37
37
 
38
38
  /** Row-config key: `<bundle package>#<row id from the bundle patch>`. */
39
- /** Sidebar entry that opens this plugin's settings page. */
40
- const PANEL_ENTRY_LABEL = '隐藏与整理'
41
- /** Panel id: the URL segment (#/panel/<id>) and the main slot key. */
42
- const PANEL_ENTRY_ID = 'hide-rows'
43
- /** The sidebar draws the row; this slot contributes the icon in front of it. */
44
- function PanelEntry() {
45
- return h(
46
- 'svg',
47
- {
48
- width: 16,
49
- height: 16,
50
- viewBox: '0 0 16 16',
51
- fill: 'none',
52
- 'aria-hidden': 'true',
53
- },
54
- h('path', {
55
- d: 'M1.8 8c1.6-2.5 3.8-3.9 6.2-3.9S12.6 5.5 14.2 8c-1.6 2.5-3.8 3.9-6.2 3.9S3.4 10.5 1.8 8Z',
56
- stroke: 'currentColor',
57
- strokeWidth: 1.3,
58
- strokeLinejoin: 'round',
59
- }),
60
- h('circle', { cx: 8, cy: 8, r: 1.9, stroke: 'currentColor', strokeWidth: 1.3 }),
61
- h('path', {
62
- d: 'M3 13 13 3',
63
- stroke: 'currentColor',
64
- strokeWidth: 1.3,
65
- strokeLinecap: 'round',
66
- }),
67
- )
68
- }
69
-
39
+ /** Sidebar entry that opens this plugin's settings page. */
40
+ const PANEL_ENTRY_LABEL = '隐藏与整理'
41
+ /** Panel id: the URL segment (#/panel/<id>) and the main slot key. */
42
+ const PANEL_ENTRY_ID = 'hide-rows'
43
+ /** The sidebar draws the row; this slot contributes the icon in front of it. */
44
+ function PanelEntry() {
45
+ return h(
46
+ 'svg',
47
+ {
48
+ width: 16,
49
+ height: 16,
50
+ viewBox: '0 0 16 16',
51
+ fill: 'none',
52
+ 'aria-hidden': 'true',
53
+ },
54
+ h('path', {
55
+ d: 'M1.8 8c1.6-2.5 3.8-3.9 6.2-3.9S12.6 5.5 14.2 8c-1.6 2.5-3.8 3.9-6.2 3.9S3.4 10.5 1.8 8Z',
56
+ stroke: 'currentColor',
57
+ strokeWidth: 1.3,
58
+ strokeLinejoin: 'round',
59
+ }),
60
+ h('circle', { cx: 8, cy: 8, r: 1.9, stroke: 'currentColor', strokeWidth: 1.3 }),
61
+ h('path', {
62
+ d: 'M3 13 13 3',
63
+ stroke: 'currentColor',
64
+ strokeWidth: 1.3,
65
+ strokeLinecap: 'round',
66
+ }),
67
+ )
68
+ }
69
+
70
70
  const ROW_CONFIG_KEY = 'dsh-declutter#hide-rows'
71
71
  const STORE_KEY = 'dsh-sidebar-declutter'
72
72
  /** Key this bundle stored its settings under before the rename. */
@@ -83,11 +83,11 @@ window.__ModuleLoader__.load({
83
83
  kind: 'hide',
84
84
  title: '左栏',
85
85
  panels: true,
86
- columns: 4,
86
+ columns: 4,
87
87
  fields: [
88
88
  { key: 'brand', title: '顶部 logo' },
89
89
  { key: 'newSession', title: '「新会话」按钮' },
90
- { key: 'sidebarMore', title: '「更多」按钮' },
90
+ { key: 'sidebarMore', title: '「更多」按钮' },
91
91
  { key: 'topGap', title: '顶部空隙' },
92
92
  ],
93
93
  },
@@ -110,9 +110,9 @@ window.__ModuleLoader__.load({
110
110
  inline: true,
111
111
  options: [
112
112
  { value: 'stack', title: '一列' },
113
- { value: 'two', title: '两列' },
113
+ { value: 'two', title: '两列' },
114
114
  { value: 'three', title: '三列' },
115
- { value: 'wrap', title: '自动紧凑' },
115
+ { value: 'wrap', title: '自动紧凑' },
116
116
  { value: 'icon', title: '图标显示' },
117
117
  ],
118
118
  },
@@ -233,8 +233,20 @@ window.__ModuleLoader__.load({
233
233
  { key: '取消归档', accel: '', rule: /^(取消归档|Unarchive)/, aria: [] },
234
234
  { key: '置顶会话', accel: '', rule: /^(置顶会话|Pin)/, aria: [] },
235
235
  { key: '取消置顶', accel: '', rule: /^(取消置顶|Unpin)/, aria: [] },
236
+ // Also shipped by the product: menu.fork / menu.sessionId / menu.openIn
237
+ // in the shell's own locale. They used to fall through to discovery and
238
+ // were mislabelled as community rows.
239
+ { key: '分叉会话', accel: 'Alt+Meta+F', rule: /^(分叉会话|Fork)/, aria: ['Alt+Meta+F'] },
240
+ { key: '复制 Session ID', accel: '', rule: /^(复制 Session ID|Copy Session ID)/, aria: [] },
241
+ { key: '在应用中打开', accel: '', rule: /^(在应用中打开|Open in App)/, aria: [] },
236
242
  ]
237
243
 
244
+ /** Fold a stored/discovered label onto its official key when it matches. */
245
+ function canonicalMenuKey(key) {
246
+ const preset = MENU_PRESETS.find((one) => one.rule.test(key))
247
+ return preset === undefined ? key : preset.key
248
+ }
249
+
238
250
  /**
239
251
  * Rows that carry an accelerator can be hidden by CSS, before the menu is ever
240
252
  * painted — no timing to lose. Rows without one still go through the DOM sweep.
@@ -258,12 +270,17 @@ window.__ModuleLoader__.load({
258
270
  const parsed = JSON.parse(localStorage.getItem(MENU_STORE_KEY) || 'null')
259
271
  if (!parsed) return
260
272
  const hidden = Array.isArray(parsed.hidden)
261
- ? parsed.hidden.filter((one) => typeof one === 'string')
273
+ ? [...new Set(parsed.hidden.filter((one) => typeof one === 'string').map(canonicalMenuKey))]
262
274
  : []
263
275
  // v1 recorded rows from every menu on the desktop — including the system
264
276
  // "open with" list — so its `seen` is thrown away, keeping the switches.
265
277
  const seen = parsed.version === MENU_STORE_VERSION && Array.isArray(parsed.seen)
266
- ? parsed.seen.filter((one) => one && typeof one.key === 'string')
278
+ ? parsed.seen
279
+ .filter((one) => one && typeof one.key === 'string')
280
+ .reduce((list, one) => {
281
+ const key = canonicalMenuKey(one.key)
282
+ return list.some((entry) => entry.key === key) ? list : [...list, { ...one, key }]
283
+ }, [])
267
284
  : []
268
285
  menuState = { hidden, seen }
269
286
  if (parsed.version !== MENU_STORE_VERSION) writeMenuState()
@@ -346,7 +363,26 @@ window.__ModuleLoader__.load({
346
363
  let menuSweepQueued = false
347
364
 
348
365
  /** Coalesce a burst of mutations into one sweep, before the next paint. */
349
- function menuQueueSweep() {
366
+ function menuQueueSweep(records) {
367
+ // The 400ms watch overlaps the chat stream. Only a mutation that actually
368
+ // mounted a menu node deserves a document-wide sweep; per-token text and
369
+ // message-row updates do not. The timed sweeps below still cover menus
370
+ // that mount without adding a role-bearing node.
371
+ if (records && records.length) {
372
+ let menuish = false
373
+ for (const rec of records) {
374
+ for (const node of rec.addedNodes || []) {
375
+ if (node.nodeType !== 1) continue
376
+ if (node.matches?.('[role="menu"],[role="menuitem"]') ||
377
+ node.querySelector?.('[role="menu"],[role="menuitem"]')) {
378
+ menuish = true
379
+ break
380
+ }
381
+ }
382
+ if (menuish) break
383
+ }
384
+ if (!menuish) return
385
+ }
350
386
  if (menuSweepQueued) return
351
387
  menuSweepQueued = true
352
388
  const run = () => {
@@ -382,6 +418,11 @@ window.__ModuleLoader__.load({
382
418
 
383
419
  /** Menus mount a frame or two after the click that opens them. */
384
420
  function menuScheduleSweep(fromSessionRow) {
421
+ // A click that cannot open a session menu and has no hidden rows to
422
+ // enforce makes menuSweep() a no-op: added stays false and every item
423
+ // hits the hidden guard. Skip the four document-wide scans, the 400ms
424
+ // watch and all timers -- this is the hot path on every ordinary click.
425
+ if (!fromSessionRow && !menuFromSessionRow && menuState.hidden.length === 0) return
385
426
  if (fromSessionRow) menuFromSessionRow = true
386
427
  menuSweep()
387
428
  menuWatchForMount()
@@ -433,56 +474,96 @@ window.__ModuleLoader__.load({
433
474
 
434
475
  function hiddenRules() {
435
476
  const rules = []
477
+ /**
478
+ * Platform split. The desktop shell marks <html data-platform>: darwin on
479
+ * macOS, win32 on Windows; a plain browser carries no marker at all (see
480
+ * the shell's own macDesktopWindow / dsh-power-menu-mac). Linux desktop is
481
+ * neither darwin nor a browser and stays on the generic branch. macOS is
482
+ * the only build with native traffic lights floating over the sidebar and
483
+ * no in-page title bar strip.
484
+ */
485
+ const isMacDesktop = () =>
486
+ typeof document !== 'undefined' &&
487
+ document.documentElement?.getAttribute('data-platform') === 'darwin'
436
488
  if (values.brand) {
437
- // The logo row is a flex child of the column, so dropping it closes the
438
- // gap. It holds the collapse toggle on Windows and Linux, though, and that
439
- // toggle must survive: hide the whole row only where it has no toggle, and
440
- // otherwise hide just the brand inside it. `data-sidebar-toggle` is the
441
- // sidebar's own attribute, stabler than any CSS-module class name.
442
- rules.push(
443
- '[class*="_logoRow"]:not(:has([data-sidebar-toggle])){display:none!important}',
444
- '[class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_brand"]{display:none!important}',
445
- // Where the row does hold the toggle, drop its height instead of the
446
- // row: zero height, no padding, and the button keeps the same place,
447
- // drawn on top of whatever now starts where the row used to be. Pure
448
- // CSS — no probe, no timer, no forced layout.
449
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:has([data-sidebar-toggle])'
450
- + '{height:0!important;min-height:0!important;padding:0!important;overflow:visible!important'
451
- + ';align-items:flex-start!important;justify-content:flex-end!important}',
452
- // Web only: a shell that draws a Windows title bar already pins this
453
- // button up there (`[data-windows-titlebar] .toggle{position:fixed;
454
- // left:12px}`), and this selector is more specific, so it has to stay
455
- // out of that layout entirely. In the browser there is no title bar,
456
- // and the button belongs in the gutter just right of the sidebar.
457
- 'html:not([data-windows-titlebar]) [class*="_root"]:not([class*="_collapsed"])'
458
- + ' [class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_toggle"]'
459
- + '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
460
- // The collapsed rail keeps a normal row, or there is no way back out.
461
- '[class*="_root"][class*="_collapsed"] [class*="_logoRow"]'
462
- + '{height:28px!important;min-height:0!important;padding-block:0!important;align-items:center!important}',
463
- // Newer builds put the collapse toggle in a 52px `topStrip` above the
464
- // logo row, so hiding the row alone leaves that strip showing as a band
465
- // of empty space. Drop it too: on Windows the toggle is fixed inside the
466
- // title bar anyway, and the browser build re-parks it in the gutter.
467
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_topStrip"]'
468
- + '{height:0!important;min-height:0!important;margin:0!important;padding:0!important'
469
- + ';overflow:visible!important;border:0!important}',
470
- 'html:not([data-windows-titlebar]) [class*="_root"]:not([class*="_collapsed"])'
471
- + ' [class*="_topStrip"] [class*="_toggle"]'
472
- + '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
473
- )
489
+ if (isMacDesktop()) {
490
+ // macOS has native traffic lights and no in-page title bar: the
491
+ // _topStrip is the only thing keeping sidebar content below them.
492
+ // Collapse the _logoRow (that is the space we reclaim) but leave the
493
+ // strip alone -- dropping both, as the Windows/browser branch does,
494
+ // is what pulled "New session" into the traffic lights. No global
495
+ // padding fallback here: [class*="_root"] is reused by many shell
496
+ // components and padding every one of them tears the column apart.
497
+ rules.push(
498
+ '[class*="_logoRow"] [class*="_brand"]{display:none!important}',
499
+ '[class*="_topStrip"] [class*="_brand"]{display:none!important}',
500
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:not(:has([data-sidebar-toggle]))'
501
+ + '{display:none!important}',
502
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:has([data-sidebar-toggle])'
503
+ + '{height:0!important;min-height:0!important;padding:0!important;overflow:visible!important}',
504
+ )
505
+ } else {
506
+ // The logo row is a flex child of the column, so dropping it closes the
507
+ // gap. It holds the collapse toggle on Windows and Linux, though, and that
508
+ // toggle must survive: hide the whole row only where it has no toggle, and
509
+ // otherwise hide just the brand inside it. `data-sidebar-toggle` is the
510
+ // sidebar's own attribute, stabler than any CSS-module class name.
511
+ rules.push(
512
+ '[class*="_logoRow"]:not(:has([data-sidebar-toggle])){display:none!important}',
513
+ '[class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_brand"]{display:none!important}',
514
+ // Where the row does hold the toggle, drop its height instead of the
515
+ // row: zero height, no padding, and the button keeps the same place,
516
+ // drawn on top of whatever now starts where the row used to be. Pure
517
+ // CSS — no probe, no timer, no forced layout.
518
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:has([data-sidebar-toggle])'
519
+ + '{height:0!important;min-height:0!important;padding:0!important;overflow:visible!important'
520
+ + ';align-items:flex-start!important;justify-content:flex-end!important}',
521
+ // Never macOS: its native traffic lights sit over the sidebar, so
522
+ // the toggle must stay in the shell's own layout. Shells that draw
523
+ // a Windows title bar are excluded by the first guard; everything
524
+ // else keeps the gutter behaviour.
525
+ 'html:not([data-windows-titlebar]):not([data-platform="darwin"]) [class*="_root"]:not([class*="_collapsed"])'
526
+ + ' [class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_toggle"]'
527
+ + '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
528
+ // The collapsed rail keeps a normal row, or there is no way back out.
529
+ '[class*="_root"][class*="_collapsed"] [class*="_logoRow"]'
530
+ + '{height:28px!important;min-height:0!important;padding-block:0!important;align-items:center!important}',
531
+ // Newer builds put the collapse toggle in a 52px `topStrip` above the
532
+ // logo row, so hiding the row alone leaves that strip showing as a band
533
+ // of empty space. Drop it too: on Windows the toggle is fixed inside the
534
+ // title bar anyway, and the browser build re-parks it in the gutter.
535
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_topStrip"]'
536
+ + '{height:0!important;min-height:0!important;margin:0!important;padding:0!important'
537
+ + ';overflow:visible!important;border:0!important}',
538
+ 'html:not([data-windows-titlebar]):not([data-platform="darwin"]) [class*="_root"]:not([class*="_collapsed"])'
539
+ + ' [class*="_topStrip"] [class*="_toggle"]'
540
+ + '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
541
+ )
542
+ }
543
+ }
544
+ // 顶部空隙: ONE checkbox on all three platforms, different behaviour.
545
+ // Windows / web keep the original three rules verbatim. On macOS the
546
+ // topStrip is 52px tall with a 28px toggle centred in it, so its lower
547
+ // part is empty: shrink it to 38px (reclaim 14px) and keep the toggle
548
+ // 11px from the top -- the same offset centring gave it -- so the button
549
+ // stays at exactly the same screen position while everything below moves
550
+ // up 14px. Zeroing _root padding is never done on macOS: that would
551
+ // shove the toggle into the native traffic lights.
552
+ if (values.topGap) {
553
+ if (isMacDesktop()) {
554
+ rules.push(
555
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_topStrip"]'
556
+ + '{height:38px!important;min-height:38px!important;align-items:flex-start!important'
557
+ + ';padding-top:11px!important;padding-bottom:0!important;box-sizing:border-box!important}',
558
+ )
559
+ } else {
560
+ rules.push(
561
+ '[class*="_root"]{padding-top:0!important}',
562
+ '[class*="_scrollRegion"]{margin-top:0!important}',
563
+ '[class*="_scrollArea"]{grid-template-rows:0 max-content 2px max-content!important}',
564
+ )
565
+ }
474
566
  }
475
- // Tighten the 18px that the sidebar itself adds above the panel list:
476
- // root padding (6), the scroll region's top margin (4) and the first grid
477
- // row of the scroll area (8). The 40px above them belongs to the native
478
- // window menu bar, so it is left alone.
479
- if (values.topGap) {
480
- rules.push(
481
- '[class*="_root"]{padding-top:0!important}',
482
- '[class*="_scrollRegion"]{margin-top:0!important}',
483
- '[class*="_scrollArea"]{grid-template-rows:0 max-content 2px max-content!important}',
484
- )
485
- }
486
567
  if (values.newSession) {
487
568
  rules.push('button[class*="_newSession"]{display:none!important}')
488
569
  }
@@ -498,34 +579,34 @@ window.__ModuleLoader__.load({
498
579
  + '{display:grid!important;grid-template-columns:1fr 1fr;gap:2px}',
499
580
  )
500
581
  }
501
- if (values.panelLayout === 'three') {
502
- // Three fixed columns: same idea as the two-column grid, one more lane.
503
- // Long labels keep the shipped ellipsis, and rows still line up.
504
- rules.push(
505
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
506
- + '{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:2px}',
507
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
508
- + '{width:auto!important;min-width:0!important;max-width:100%}',
509
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
510
- + '{padding-left:6px!important;padding-right:6px!important}',
511
- )
512
- }
513
- if (values.panelLayout === 'icon') {
514
- // Icons only: the label is dropped, rows shrink to their glyph, and the
515
- // list packs them side by side. The unpin control is hidden too, or it
516
- // would show up as a second glyph per entry.
517
- rules.push(
518
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
519
- + '{flex-direction:row!important;flex-wrap:wrap!important;gap:4px!important}',
520
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
521
- + '{flex:0 0 auto!important;width:auto!important;min-width:0!important;max-width:100%}',
522
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelTitle"]{display:none!important}',
523
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
524
- + '{width:auto!important;min-width:0!important;padding-left:8px!important;'
525
- + 'padding-right:8px!important;justify-content:center!important;gap:0!important}',
526
- '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelUnpin"]{display:none!important}',
527
- )
528
- }
582
+ if (values.panelLayout === 'three') {
583
+ // Three fixed columns: same idea as the two-column grid, one more lane.
584
+ // Long labels keep the shipped ellipsis, and rows still line up.
585
+ rules.push(
586
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
587
+ + '{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:2px}',
588
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
589
+ + '{width:auto!important;min-width:0!important;max-width:100%}',
590
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
591
+ + '{padding-left:6px!important;padding-right:6px!important}',
592
+ )
593
+ }
594
+ if (values.panelLayout === 'icon') {
595
+ // Icons only: the label is dropped, rows shrink to their glyph, and the
596
+ // list packs them side by side. The unpin control is hidden too, or it
597
+ // would show up as a second glyph per entry.
598
+ rules.push(
599
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
600
+ + '{flex-direction:row!important;flex-wrap:wrap!important;gap:4px!important}',
601
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
602
+ + '{flex:0 0 auto!important;width:auto!important;min-width:0!important;max-width:100%}',
603
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelTitle"]{display:none!important}',
604
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
605
+ + '{width:auto!important;min-width:0!important;padding-left:8px!important;'
606
+ + 'padding-right:8px!important;justify-content:center!important;gap:0!important}',
607
+ '[class*="_root"]:not([class*="_collapsed"]) [class*="_panelUnpin"]{display:none!important}',
608
+ )
609
+ }
529
610
  if (values.panelLayout === 'wrap') {
530
611
  // Content-sized chips that wrap greedily, not fixed columns: two short
531
612
  // names (插件 / 鱼塘) pack three or four to a line while a long one
@@ -578,7 +659,7 @@ window.__ModuleLoader__.load({
578
659
  // The bulk layer only: every panel row plus the list's More launcher. The
579
660
  // individual switches and the checkbox list stay untouched.
580
661
  rules.push(
581
- '[class*="_panelList"] [class*="_panelEntry"]{display:none!important}',
662
+ '[class*="_panelList"] [class*="_panelEntry"]{display:none!important}',
582
663
  '[class*="_panelList"] > button[aria-label]{display:none!important}',
583
664
  '[class*="_panelList"] [class*="_moreAnchor"]{display:none!important}',
584
665
  '[class*="_panelList"] button[aria-haspopup="menu"]{display:none!important}',
@@ -590,13 +671,13 @@ window.__ModuleLoader__.load({
590
671
  // Primary: the id stamped on the row, which keeps two same-named panels
591
672
  // apart. Fallback: the name, used only before the first stamping pass and
592
673
  // only when no other panel carries it.
593
- // Newer builds wrap every row in a `_panelEntry` box; hiding only the
594
- // button leaves that box occupying its grid cell, which is the empty slot
595
- // seen when a two-column list loses one panel. Hide the box itself.
596
- rules.push(
597
- '[class*="_panelEntry"]:has(button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"])'
598
- + '{display:none!important}',
599
- 'button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"]{display:none!important}',
674
+ // Newer builds wrap every row in a `_panelEntry` box; hiding only the
675
+ // button leaves that box occupying its grid cell, which is the empty slot
676
+ // seen when a two-column list loses one panel. Hide the box itself.
677
+ rules.push(
678
+ '[class*="_panelEntry"]:has(button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"])'
679
+ + '{display:none!important}',
680
+ 'button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"]{display:none!important}',
600
681
  )
601
682
  const shared = panelEntries.some((one) => one.id !== id && one.label === entry.label)
602
683
  if (!shared) {
@@ -624,7 +705,7 @@ window.__ModuleLoader__.load({
624
705
  '.dhns-sectionHint{font-size:11px;line-height:1.4;color:var(--dsw-alias-label-tertiary)}',
625
706
  '.dhns-rows{display:flex;flex-direction:column;gap:8px}',
626
707
  '.dhns-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}',
627
- '.dhns-grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}',
708
+ '.dhns-grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}',
628
709
  '.dhns-grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}',
629
710
  '.dhns-grid .dhns-row{padding:6px 8px;align-items:center;border-radius:6px}',
630
711
  '.dhns-grid .dhns-title{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
@@ -685,8 +766,8 @@ window.__ModuleLoader__.load({
685
766
  'border-radius:6px;border:1px solid var(--dsw-alias-border-secondary,rgba(128,128,128,.3));',
686
767
  'background:transparent;color:var(--dsw-alias-label-secondary)}',
687
768
  '.dhns-subheadBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
688
- '.dhns-subheadBtn:disabled{opacity:.4;cursor:default}',
689
- '.dhns-page{padding:24px 28px;max-width:820px}',
769
+ '.dhns-subheadBtn:disabled{opacity:.4;cursor:default}',
770
+ '.dhns-page{padding:24px 28px;max-width:820px}',
690
771
  '.dhns-pageTitle{margin:0 0 16px;font-size:16px;font-weight:600}',
691
772
  ].join('')
692
773
 
@@ -1120,29 +1201,26 @@ window.__ModuleLoader__.load({
1120
1201
  * row. `summary` is the one-liner the row shows when collapsed; `page` is the
1121
1202
  * form the row's configure control opens.
1122
1203
  */
1123
- /** The page #/panel/hide-rows renders: the same form as the plugin row card. */
1124
- function SettingsPage() {
1125
- return h(
1126
- 'div',
1127
- { className: 'dhns-page' },
1128
- h('h2', { className: 'dhns-pageTitle' }, PANEL_ENTRY_LABEL),
1129
- h(SettingsCard, { view: 'full' }),
1130
- )
1131
- }
1132
-
1204
+ /** The page #/panel/hide-rows renders: the same form as the plugin row card. */
1205
+ function SettingsPage() {
1206
+ return h(
1207
+ 'div',
1208
+ { className: 'dhns-page' },
1209
+ h('h2', { className: 'dhns-pageTitle' }, PANEL_ENTRY_LABEL),
1210
+ h(SettingsCard, { view: 'full' }),
1211
+ )
1212
+ }
1213
+
1133
1214
  function SettingsCard(props) {
1134
1215
  const view = props?.view
1135
1216
  const [, forceRender] = React.useReducer((count) => count + 1, 0)
1136
1217
  const [dragId, setDragId] = React.useState(null)
1137
1218
  React.useEffect(() => subscribe(forceRender), [])
1138
1219
 
1139
- if (view === 'summary') {
1140
- // Only the hide section belongs in the one-liner; the layout switch is
1141
- // not something the row can meaningfully report as "hidden".
1142
- const on = FIELDS.filter((field) => field.kind === 'hide' && values[field.key] === true)
1143
- .map((field) => field.title)
1144
- return h('span', null, on.length === 0 ? '未隐藏任何项' : '已隐藏:' + on.join('、'))
1145
- }
1220
+ // The host renders this card with view="summary" as the one-line
1221
+ // description under the plugin row. Return nothing so no description
1222
+ // is drawn at all; the full form is unchanged.
1223
+ if (view === 'summary') return null
1146
1224
 
1147
1225
  const rowFor = (field) =>
1148
1226
  h(
@@ -1355,7 +1433,7 @@ window.__ModuleLoader__.load({
1355
1433
  ),
1356
1434
  ),
1357
1435
  values.picked.length === 0
1358
- ? h('p', { className: 'dhns-hint', key: 'empty' }, '还没有点选任何元素。')
1436
+ ? null
1359
1437
  : h(
1360
1438
  'div',
1361
1439
  { className: 'dhns-rows', key: 'list' },
@@ -1452,13 +1530,13 @@ window.__ModuleLoader__.load({
1452
1530
  : h(
1453
1531
  'div',
1454
1532
  {
1455
- className: section.columns === 2
1456
- ? 'dhns-grid dhns-grid-2'
1457
- : section.columns === 3
1458
- ? 'dhns-grid'
1459
- : section.columns === 4
1460
- ? 'dhns-grid dhns-grid-4'
1461
- : 'dhns-rows',
1533
+ className: section.columns === 2
1534
+ ? 'dhns-grid dhns-grid-2'
1535
+ : section.columns === 3
1536
+ ? 'dhns-grid'
1537
+ : section.columns === 4
1538
+ ? 'dhns-grid dhns-grid-4'
1539
+ : 'dhns-rows',
1462
1540
  },
1463
1541
  section.fields.map(renderField),
1464
1542
  ),
@@ -1556,31 +1634,31 @@ window.__ModuleLoader__.load({
1556
1634
  ),
1557
1635
  )
1558
1636
 
1559
- // Same two-slot pattern the shipped panels use: the sidebar row only
1560
- // carries the icon and label, and the main slot with the same key is the
1561
- // page the shell routes #/panel/<id> to. Without the second registration
1562
- // the click falls back to whatever panel was open before.
1563
- if (ctx.slots && typeof ctx.slots.inject === 'function') {
1564
- ctx.effect(() =>
1565
- ctx.slots.inject('sidebar.panellist', () =>
1566
- ctx.slots.register(
1567
- {
1568
- name: 'sidebar.panellist',
1569
- id: PANEL_ENTRY_ID,
1570
- order: 20,
1571
- label: () => PANEL_ENTRY_LABEL,
1572
- },
1573
- PanelEntry,
1574
- ),
1575
- ),
1576
- )
1577
- ctx.effect(() =>
1578
- ctx.slots.inject('main', () =>
1579
- ctx.slots.register({ name: 'main', key: PANEL_ENTRY_ID }, SettingsPage),
1580
- ),
1581
- )
1582
- }
1583
-
1637
+ // Same two-slot pattern the shipped panels use: the sidebar row only
1638
+ // carries the icon and label, and the main slot with the same key is the
1639
+ // page the shell routes #/panel/<id> to. Without the second registration
1640
+ // the click falls back to whatever panel was open before.
1641
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
1642
+ ctx.effect(() =>
1643
+ ctx.slots.inject('sidebar.panellist', () =>
1644
+ ctx.slots.register(
1645
+ {
1646
+ name: 'sidebar.panellist',
1647
+ id: PANEL_ENTRY_ID,
1648
+ order: 20,
1649
+ label: () => PANEL_ENTRY_LABEL,
1650
+ },
1651
+ PanelEntry,
1652
+ ),
1653
+ ),
1654
+ )
1655
+ ctx.effect(() =>
1656
+ ctx.slots.inject('main', () =>
1657
+ ctx.slots.register({ name: 'main', key: PANEL_ENTRY_ID }, SettingsPage),
1658
+ ),
1659
+ )
1660
+ }
1661
+
1584
1662
  if (ENABLE_ROW_CONFIG) {
1585
1663
  ctx.effect(() =>
1586
1664
  ctx.slots.inject('plugins.row.config', () =>