dsh-panel-control 0.8.3 → 0.9.2
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.md +12 -4
- package/client.js +343 -16
- package/docs/bridged-settings-pages.md +2 -2
- package/docs/renaming-panels.md +75 -0
- package/index.js +160 -2
- package/package.json +5 -2
- package/scripts/host-smoke.mjs +61 -3
- package/scripts/layout-smoke.mjs +11 -0
- package/scripts/names-smoke.mjs +425 -0
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- **排序** — 拖动行首把手任意换位;一键按名称 A→Z;一键恢复注册时的默认顺序。
|
|
8
8
|
- **隐藏** — 行右侧的眼睛图标。隐藏项仍留在列表里,随时放回;不影响插件是否加载,也不写插件自己的配置。
|
|
9
|
+
- **重命名** — 行右侧的铅笔图标把名称改成你熟悉的叫法,`Enter` 或失焦保存,`Esc` 取消,清空即恢复原名。改名只改侧栏显示,不动插件自己的配置;宽侧栏下改过名的行会在最右侧显示插件包名小标签(省略 `dsh-` 前缀,可在「侧栏布局」里关掉),避免时间久了忘记它原本是谁。
|
|
9
10
|
- **分组** — 新建分组(如「创作」「系统」)并把入口分进去,侧栏会在组与组之间插入带标题的分隔线。组的先后由设置里的分组列表拖动决定,与注册顺序、分类结果无关。
|
|
10
11
|
- **分组折叠** — 点分组标题收起该组,只留标题与计数;`Enter` / 空格同样可切换,状态写进配置。侧栏收成图标条时不显示标题,也不折叠。
|
|
11
12
|
- **侧栏溢出兜底** — 导航项变多时,官方侧栏会把下方的会话/工作区列表压到 0 高且无法滚动([讨论 #12083](https://github.com/deepseek-ai/deepseek-harness/discussions/12083))。本插件给导航列表加上限高与滚动,给下方区域留最小高度。
|
|
@@ -25,13 +26,17 @@
|
|
|
25
26
|
|
|
26
27
|
补出来的设置页入口也出现在同一个侧栏列表里,排在普通入口之后、本插件自己之前;标题沿用设置页自己的名称,重复时附带设置页 id 以区分。
|
|
27
28
|
|
|
29
|
+
改过名的入口仍把原来的注册名留在按钮属性里,引擎靠它把自己映射回同一条注册,所以宿主的排序、重渲染和后续重扫都不受影响;行尾标签显示的是插件包名(由 Host 从安装目录的 `package.json` 解析,`dsh-` 前缀已省略),屏幕阅读器与工具提示读到的则是你设定的显示名。
|
|
30
|
+
|
|
28
31
|
## 安装
|
|
29
32
|
|
|
33
|
+
要求 DeepSeek Harness `>= 0.2.1-rc.3`(更旧的版本缺少本插件依赖的槽与模块图接口,宿主会拒绝加载)。
|
|
34
|
+
|
|
30
35
|
从 npm 安装(推荐):
|
|
31
36
|
|
|
32
37
|
```bash
|
|
33
38
|
cd C:/Users/<you>/.dsh/profiles/desktop
|
|
34
|
-
npm install dsh-panel-control@0.
|
|
39
|
+
npm install dsh-panel-control@0.9.2
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
把包名加进同一目录 `package.json` 的 `dsh.profile.bundles`:
|
|
@@ -68,11 +73,12 @@ cmd //c mklink /J node_modules\dsh-panel-control D:\Github\dsh-panel-control
|
|
|
68
73
|
- 高度兜底开关,以及 35 / 45 / 55 / 不限 四档上限;
|
|
69
74
|
- 分组折叠开关,以及「全部收起 / 全部展开」;
|
|
70
75
|
- 设置页入口开关(默认开)。关掉后补出来的行会连同注册一起撤掉,不只是藏起来;
|
|
76
|
+
- 包名标签开关(默认开)。关掉后改名仍然生效,只是行尾不再显示包名;
|
|
71
77
|
- 折叠状态按分组 id 记在配置里,重启后保留。
|
|
72
78
|
|
|
73
79
|
## 配置与存储
|
|
74
80
|
|
|
75
|
-
布局写在 Host 侧:`~/.dsh/panel-control/prefs.json`(`~` 是这个 profile
|
|
81
|
+
布局写在 Host 侧:`~/.dsh/panel-control/prefs.json`(`~` 是这个 profile 的用户目录)。包含顺序、隐藏项、分组、自定义名称、折叠状态、布局开关、设置页入口开关、包名标签开关与分类结果。
|
|
76
82
|
|
|
77
83
|
页面 origin 是 Host 的 loopback 端口,每次启动都会换端口,`localStorage` 会因此"看起来每次清空"——所以真实存储放在 Host 侧,`localStorage` 只留镜像;读不到 Host 时才回退到镜像。配置按安装维度生效,不跟随工作区。
|
|
78
84
|
|
|
@@ -86,7 +92,7 @@ cmd //c mklink /J node_modules\dsh-panel-control D:\Github\dsh-panel-control
|
|
|
86
92
|
## 开发与测试
|
|
87
93
|
|
|
88
94
|
```bash
|
|
89
|
-
npm run check # 语法检查 + 宿主路由 smoke +
|
|
95
|
+
npm run check # 语法检查 + 宿主路由 smoke + 布局/桥接/改名静态与行为断言
|
|
90
96
|
npm run smoke # 只跑宿主路由用例
|
|
91
97
|
npm run doctor # 体检 profile 安装状态
|
|
92
98
|
```
|
|
@@ -95,14 +101,16 @@ npm run doctor # 体检 profile 安装状态
|
|
|
95
101
|
- `scripts/host-smoke.mjs`:驱动宿主侧路由,覆盖设置读写、分类请求与 bundle 兜底。
|
|
96
102
|
- `scripts/layout-smoke.mjs`:断言侧栏布局的 CSS 钩子与关键数值,并校验 `BUILD_STAMP` 与版本号一致。
|
|
97
103
|
- `scripts/bridge-smoke.mjs`:在假 ctx 上跑 `client.js`,断言只给「有设置页、没有侧栏入口」的第三方插件补行,且成对注册 main、开关关闭后整体撤回、桥接页经自有子槽交给宿主渲染并能返回对话。
|
|
104
|
+
- `scripts/names-smoke.mjs`:用迷你 DOM 驱动真实引擎,断言改名覆盖宿主标题与 `aria-label`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包不显示标签,以及 `dispose` 后 DOM 原样归还。
|
|
98
105
|
|
|
99
|
-
进一步材料:[docs/sidebar-layout.md](docs/sidebar-layout.md)(布局与折叠实现)、[docs/bridged-settings-pages.md](docs/bridged-settings-pages.md)(设置页补入口的识别与渲染)、[docs/ui-design-notes.md](docs/ui-design-notes.md)(界面规范与踩坑)、[docs/discussion-bundle-evaluated-twice.md](docs/discussion-bundle-evaluated-twice.md)(bundle 被求值两次的事故复盘)。
|
|
106
|
+
进一步材料:[docs/sidebar-layout.md](docs/sidebar-layout.md)(布局与折叠实现)、[docs/renaming-panels.md](docs/renaming-panels.md)(重命名与包名标签)、[docs/bridged-settings-pages.md](docs/bridged-settings-pages.md)(设置页补入口的识别与渲染)、[docs/ui-design-notes.md](docs/ui-design-notes.md)(界面规范与踩坑)、[docs/discussion-bundle-evaluated-twice.md](docs/discussion-bundle-evaluated-twice.md)(bundle 被求值两次的事故复盘)。
|
|
100
107
|
|
|
101
108
|
## 边界
|
|
102
109
|
|
|
103
110
|
- 自动分类会把入口名称发给当前 profile 配置的模型服务商;不想要就关掉开关,分组仍可手动编辑。
|
|
104
111
|
- 分类结果只是建议,可以整体重排或改名。
|
|
105
112
|
- 依赖宿主侧栏的 DOM 结构(`[class*="panelList"]` 与面板按钮的 `aria-label`),官方大改版时需要跟进。
|
|
113
|
+
- 重命名只覆盖侧栏的显示层:宿主标题文本、`aria-label` 与包名标签都是本插件写的,插件自己的配置和入口 id 不变;宿主重渲染后由下一次扫描补回。图标条模式没有标题文本,只改可访问名称,也不显示包名标签。包名由 Host 从模块所在安装目录的 `package.json` 解析(模块 id 不等于包名,例如 `dsh-enhance-ui` → `@wufeng0105/dsh-enhance`),接口不可用或解析不到时该行不显示标签。
|
|
106
114
|
- 只写 `style.order`、`style.display` 与 `data-dpc-*` 属性,不移动 React 节点,不改宿主代码。
|
|
107
115
|
- 排序、隐藏、分组都只影响本机这份安装。
|
|
108
116
|
- 设置页入口按「section id / label 是否已被某个面板占用」识别归属,不依赖插件声明的注册者(client runner 会把多个插件报成同一个名字);代价是启发式:与面板同名的第三方设置页会被视为已有入口,新增内置设置页需要更新 `CORE_SECTION_IDS`。第三方设置页按宿主渲染器原位渲染,若它自己依赖设置弹窗的私有上下文,可能出现布局差异。
|
package/client.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Everything below therefore lives inside this closure. Re-evaluation must stay possible: when the
|
|
6
6
|
// plugin is switched off the loader drops our factory, so switching it back on re-executes this
|
|
7
7
|
// script and needs the registration that follows.
|
|
8
|
-
const BUILD_STAMP = 'dsh-panel-control/0.
|
|
8
|
+
const BUILD_STAMP = 'dsh-panel-control/0.9.2';
|
|
9
9
|
try { window.__dpcEvaluated = BUILD_STAMP; } catch { /* ignore */ }
|
|
10
10
|
/**
|
|
11
11
|
* Client half of dsh-panel-control.
|
|
@@ -348,9 +348,10 @@ function build(require) {
|
|
|
348
348
|
diagYes: { zh: '是', en: 'yes' },
|
|
349
349
|
diagNo: { zh: '否', en: 'no' },
|
|
350
350
|
hint: {
|
|
351
|
-
zh: '
|
|
352
|
-
en: 'Drag the handle at the start of a row to reorder it; a hidden entry stays in this list and can come back at any time.',
|
|
351
|
+
zh: '拖行首的把手排序;点铅笔可以改显示名,改过名的入口会在侧栏右侧显示包名。隐藏的入口仍留在列表里,随时可以放回来。',
|
|
352
|
+
en: 'Drag the handle at the start of a row to reorder it; use the pencil to rename an entry, and a renamed row shows its package name on the right in the sidebar. A hidden entry stays in this list and can come back at any time.',
|
|
353
353
|
},
|
|
354
|
+
renameIt: { zh: '修改显示名', en: 'Rename entry' },
|
|
354
355
|
dragHint: { zh: '拖动排序', en: 'Drag to reorder' },
|
|
355
356
|
locked: { zh: '固定显示', en: 'always shown' },
|
|
356
357
|
lockedHint: {
|
|
@@ -395,6 +396,11 @@ function build(require) {
|
|
|
395
396
|
bridgeFrom: { zh: '来自 {p}', en: 'From {p}' },
|
|
396
397
|
bridgeMissing: { zh: '这个设置页已经不可用。', en: 'This settings page is no longer available.' },
|
|
397
398
|
diagBridge: { zh: '补全入口', en: 'bridged entries' },
|
|
399
|
+
layoutTags: { zh: '包名标签', en: 'Package tag' },
|
|
400
|
+
layoutTagsHint: {
|
|
401
|
+
zh: '改过显示名的入口会在侧栏行右侧显示插件包名(省略 dsh- 前缀);关掉后全部隐藏。',
|
|
402
|
+
en: 'A renamed entry shows its plugin package name (with the dsh- prefix trimmed) in a small tag on the right of its sidebar row. Turn this off to hide them all.',
|
|
403
|
+
},
|
|
398
404
|
layoutCollapseAll: { zh: '全部收起', en: 'Fold all' },
|
|
399
405
|
layoutExpandAll: { zh: '全部展开', en: 'Unfold all' },
|
|
400
406
|
layoutCollapseOne: { zh: '点此收起', en: 'click to fold' },
|
|
@@ -487,6 +493,8 @@ function build(require) {
|
|
|
487
493
|
guard: true,
|
|
488
494
|
collapse: true,
|
|
489
495
|
bridge: true,
|
|
496
|
+
names: {},
|
|
497
|
+
tags: true,
|
|
490
498
|
panelMax: 45,
|
|
491
499
|
});
|
|
492
500
|
|
|
@@ -498,7 +506,17 @@ function build(require) {
|
|
|
498
506
|
* would then build that many separators. The numbers here are far above any
|
|
499
507
|
* real sidebar and turn a hostile or corrupted store into a cheap no-op.
|
|
500
508
|
*/
|
|
501
|
-
const LIMITS = { order: 200, hidden: 200, autoFiled: 400, collapsed: 200, groups: 24, assign: 400, name: 40, key: 120 };
|
|
509
|
+
const LIMITS = { order: 200, hidden: 200, autoFiled: 400, collapsed: 200, groups: 24, assign: 400, names: 200, name: 40, key: 120 };
|
|
510
|
+
|
|
511
|
+
/** Trim, flatten and cap a user-visible name. Returns '' when it is unusable. */
|
|
512
|
+
function cleanName(value) {
|
|
513
|
+
if (typeof value !== 'string') return '';
|
|
514
|
+
return value
|
|
515
|
+
.replace(/[\u0000-\u001f\u007f]/g, '')
|
|
516
|
+
.replace(/\s+/g, ' ')
|
|
517
|
+
.trim()
|
|
518
|
+
.slice(0, LIMITS.name);
|
|
519
|
+
}
|
|
502
520
|
|
|
503
521
|
/** Dedupe, drop anything that is not a usable id, and stop at `cap`. */
|
|
504
522
|
function uniqueIds(raw, cap) {
|
|
@@ -553,6 +571,20 @@ function build(require) {
|
|
|
553
571
|
kept += 1;
|
|
554
572
|
}
|
|
555
573
|
}
|
|
574
|
+
if (raw.names && typeof raw.names === 'object') {
|
|
575
|
+
let kept = 0;
|
|
576
|
+
for (const [panelId, value] of Object.entries(raw.names)) {
|
|
577
|
+
if (kept >= LIMITS.names) break;
|
|
578
|
+
if (typeof panelId !== 'string' || panelId === '' || panelId.length > LIMITS.key) continue;
|
|
579
|
+
const name = cleanName(value);
|
|
580
|
+
if (!name) continue;
|
|
581
|
+
out.names[panelId] = name;
|
|
582
|
+
kept += 1;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Renames are opt-in per entry, so a missing field means "no custom names".
|
|
586
|
+
// The tag switch is the opposite: old files never had it and must get it on.
|
|
587
|
+
out.tags = raw.tags !== false;
|
|
556
588
|
out.auto = raw.auto === true;
|
|
557
589
|
return out;
|
|
558
590
|
}
|
|
@@ -587,6 +619,8 @@ function build(require) {
|
|
|
587
619
|
prefs.guard === false ||
|
|
588
620
|
prefs.collapse === false ||
|
|
589
621
|
prefs.bridge === false ||
|
|
622
|
+
prefs.tags === false ||
|
|
623
|
+
Object.keys(prefs.names || {}).length > 0 ||
|
|
590
624
|
(typeof prefs.panelMax === 'number' && prefs.panelMax !== 45));
|
|
591
625
|
|
|
592
626
|
/** One call to the Host half, or null when this Client cannot reach it. */
|
|
@@ -898,6 +932,32 @@ function build(require) {
|
|
|
898
932
|
return fallback;
|
|
899
933
|
}
|
|
900
934
|
|
|
935
|
+
/** The user's own name for an entry, or '' when it was never renamed. */
|
|
936
|
+
function customName(prefs, id) {
|
|
937
|
+
const names = prefs && prefs.names;
|
|
938
|
+
if (!names || typeof names !== 'object') return '';
|
|
939
|
+
const value = names[id];
|
|
940
|
+
return typeof value === 'string' ? cleanName(value) : '';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** What the sidebar and the manager row should show for this entry. */
|
|
944
|
+
function displayName(prefs, id, fallback) {
|
|
945
|
+
return customName(prefs, id) || fallback;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* A package name shortened into a tag: scope gone, `dsh-` gone, `client-ui-`
|
|
950
|
+
* gone. `dsh-panel-control` and `dshmarket` both read as their product name.
|
|
951
|
+
*/
|
|
952
|
+
function packageLabel(name) {
|
|
953
|
+
if (typeof name !== 'string') return '';
|
|
954
|
+
return name
|
|
955
|
+
.replace(/^@[^/]+\//, '')
|
|
956
|
+
.replace(/^dsh-?/, '')
|
|
957
|
+
.replace(/^client-ui-/, '')
|
|
958
|
+
.trim();
|
|
959
|
+
}
|
|
960
|
+
|
|
901
961
|
/**
|
|
902
962
|
* The sidebar's own list: every registered entry, sorted exactly the way the
|
|
903
963
|
* sidebar sorts it (`order`, then registration order).
|
|
@@ -963,6 +1023,86 @@ function build(require) {
|
|
|
963
1023
|
unsub: null,
|
|
964
1024
|
};
|
|
965
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Panel/section id -> the package that registers it.
|
|
1028
|
+
*
|
|
1029
|
+
* The slot entry has no package field and its `registrant` is shared by most
|
|
1030
|
+
* plugins, so the Host resolves ownership from its module graph. The map stays
|
|
1031
|
+
* empty until that answer arrives; the tag falls back to the id meanwhile.
|
|
1032
|
+
*/
|
|
1033
|
+
const packageState = {
|
|
1034
|
+
map: Object.create(null),
|
|
1035
|
+
loading: false,
|
|
1036
|
+
key: '',
|
|
1037
|
+
pending: '',
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
function packageTagFor(id) {
|
|
1041
|
+
// No fallback to the row id: a bridge row's id is `dpc-bridge-*` and an
|
|
1042
|
+
// unresolved panel id is not a package name. An empty tag is better than
|
|
1043
|
+
// a second, wrong spelling of the same plugin.
|
|
1044
|
+
return packageLabel(packageState.map[id]);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Ask the Host which package each visible row belongs to.
|
|
1049
|
+
*
|
|
1050
|
+
* Scans are de-duplicated by the exact id set, so the engine's many passes and
|
|
1051
|
+
* every bridge re-sync cost one call per set rather than one per pass.
|
|
1052
|
+
*/
|
|
1053
|
+
async function ensurePackages(force) {
|
|
1054
|
+
const records = [];
|
|
1055
|
+
const seen = new Set();
|
|
1056
|
+
const add = (id, slot) => {
|
|
1057
|
+
if (typeof id !== 'string' || id === '' || seen.has(slot + ':' + id)) return;
|
|
1058
|
+
seen.add(slot + ':' + id);
|
|
1059
|
+
records.push({ id, slot });
|
|
1060
|
+
};
|
|
1061
|
+
for (const entry of store.state.entries) {
|
|
1062
|
+
if (entry.id.startsWith(BRIDGE_PREFIX)) continue;
|
|
1063
|
+
add(entry.id, SLOT_PANELS);
|
|
1064
|
+
}
|
|
1065
|
+
for (const record of bridgeState.entries.values()) {
|
|
1066
|
+
if (record && typeof record.sectionId === 'string') add(record.sectionId, SLOT_SETTINGS);
|
|
1067
|
+
}
|
|
1068
|
+
if (records.length === 0) return;
|
|
1069
|
+
records.sort((left, right) => (left.slot + ':' + left.id).localeCompare(right.slot + ':' + right.id));
|
|
1070
|
+
const key = records.map((row) => row.slot + ':' + row.id).join('|');
|
|
1071
|
+
if (!force && key === packageState.key) return;
|
|
1072
|
+
if (packageState.loading) {
|
|
1073
|
+
packageState.pending = key;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
packageState.key = key;
|
|
1077
|
+
packageState.loading = true;
|
|
1078
|
+
try {
|
|
1079
|
+
const value = await hostCall('packages', { ids: records });
|
|
1080
|
+
const packages = value && value.packages;
|
|
1081
|
+
if (!packages || typeof packages !== 'object') return;
|
|
1082
|
+
let changed = false;
|
|
1083
|
+
for (const [id, pkg] of Object.entries(packages)) {
|
|
1084
|
+
if (typeof pkg !== 'string' || pkg === '') continue;
|
|
1085
|
+
if (packageState.map[id] === pkg) continue;
|
|
1086
|
+
packageState.map[id] = pkg;
|
|
1087
|
+
changed = true;
|
|
1088
|
+
}
|
|
1089
|
+
// A bridge row's own id is synthetic; its package is the section's.
|
|
1090
|
+
for (const [bridgeId, record] of bridgeState.entries) {
|
|
1091
|
+
const pkg = record && packageState.map[record.sectionId];
|
|
1092
|
+
if (pkg && packageState.map[bridgeId] !== pkg) {
|
|
1093
|
+
packageState.map[bridgeId] = pkg;
|
|
1094
|
+
changed = true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (changed && engineRef) engineRef.schedule();
|
|
1098
|
+
} finally {
|
|
1099
|
+
packageState.loading = false;
|
|
1100
|
+
const next = packageState.pending;
|
|
1101
|
+
packageState.pending = '';
|
|
1102
|
+
if (next && next !== packageState.key) void ensurePackages();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
966
1106
|
function registrantOf(entry) {
|
|
967
1107
|
const options = (entry && entry.options) || {};
|
|
968
1108
|
const value = (entry && entry.registrant) || options.registrant;
|
|
@@ -1111,7 +1251,7 @@ function build(require) {
|
|
|
1111
1251
|
for (const candidate of candidates) {
|
|
1112
1252
|
if (bridgeState.entries.has(candidate.bridgeId)) continue;
|
|
1113
1253
|
const slotName = BRIDGE_SLOT_PREFIX + candidate.sectionId;
|
|
1114
|
-
const record = { disposers: [] };
|
|
1254
|
+
const record = { sectionId: candidate.sectionId, disposers: [] };
|
|
1115
1255
|
try {
|
|
1116
1256
|
record.disposers.push(
|
|
1117
1257
|
slots.register(
|
|
@@ -1189,6 +1329,9 @@ function build(require) {
|
|
|
1189
1329
|
// re-render it would keep showing the value from before the sync.
|
|
1190
1330
|
emit();
|
|
1191
1331
|
}
|
|
1332
|
+
// New bridge rows (or a first scan that finally sees the real rows) need
|
|
1333
|
+
// their package names; the call is keyed, so repeated scans are free.
|
|
1334
|
+
void ensurePackages();
|
|
1192
1335
|
} catch (error) {
|
|
1193
1336
|
console.warn(`[${PACKAGE}] bridge sync failed`, error);
|
|
1194
1337
|
} finally {
|
|
@@ -1316,7 +1459,9 @@ function build(require) {
|
|
|
1316
1459
|
const used = new Set();
|
|
1317
1460
|
const byLabel = new Map();
|
|
1318
1461
|
for (const button of buttons) {
|
|
1319
|
-
|
|
1462
|
+
// A renamed row keeps its original label in `data-dpc-label`: the visible
|
|
1463
|
+
// text and aria-label change, but the entry it belongs to does not.
|
|
1464
|
+
const label = button.getAttribute('data-dpc-label') || button.getAttribute('aria-label') || '';
|
|
1320
1465
|
if (!byLabel.has(label)) byLabel.set(label, []);
|
|
1321
1466
|
byLabel.get(label).push(button);
|
|
1322
1467
|
}
|
|
@@ -1335,7 +1480,7 @@ function build(require) {
|
|
|
1335
1480
|
// Labels that agree prove the two lists are the same list; a button with no
|
|
1336
1481
|
// label at all is no evidence either way, so it may be filled positionally.
|
|
1337
1482
|
const sameOrder = loose.every((entry, index) => {
|
|
1338
|
-
const label = rest[index].getAttribute('aria-label') || '';
|
|
1483
|
+
const label = rest[index].getAttribute('data-dpc-label') || rest[index].getAttribute('aria-label') || '';
|
|
1339
1484
|
return label === '' || label === entry.label;
|
|
1340
1485
|
});
|
|
1341
1486
|
// A build that labels rows in wording this plugin never sees ("IM 机器人"
|
|
@@ -1447,7 +1592,10 @@ function build(require) {
|
|
|
1447
1592
|
if (!entry.label) continue;
|
|
1448
1593
|
let button = null;
|
|
1449
1594
|
try {
|
|
1450
|
-
|
|
1595
|
+
const label = cssEscape(entry.label);
|
|
1596
|
+
button = document.querySelector(
|
|
1597
|
+
`button[data-dpc-label="${label}"], button[aria-label="${label}"]`,
|
|
1598
|
+
);
|
|
1451
1599
|
} catch {
|
|
1452
1600
|
button = null;
|
|
1453
1601
|
}
|
|
@@ -1595,6 +1743,74 @@ function build(require) {
|
|
|
1595
1743
|
}
|
|
1596
1744
|
};
|
|
1597
1745
|
|
|
1746
|
+
/** The title node of a host panel row: the only direct span that is not the
|
|
1747
|
+
* aria-hidden glyph and not the original-name tag this plugin adds. */
|
|
1748
|
+
const titleSpanOf = (button) => {
|
|
1749
|
+
for (const child of Array.from(button.children || [])) {
|
|
1750
|
+
if (child.tagName !== 'SPAN') continue;
|
|
1751
|
+
const className = typeof child.className === 'string' ? child.className : '';
|
|
1752
|
+
if (className.split(/\s+/).includes('dpc-nameTag')) continue;
|
|
1753
|
+
if (child.getAttribute('aria-hidden') === 'true') continue;
|
|
1754
|
+
return child;
|
|
1755
|
+
}
|
|
1756
|
+
return null;
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Rename rows in the host sidebar, and keep the original label visible in a
|
|
1761
|
+
* small tag. The host owns this DOM, so this is a pure display overlay: the
|
|
1762
|
+
* entry id, its slot registration and every other preference are untouched.
|
|
1763
|
+
*/
|
|
1764
|
+
const applyNames = (map, plan, wide) => {
|
|
1765
|
+
const tagsOn = plan.prefs.tags !== false;
|
|
1766
|
+
const byId = new Map(plan.entries.map((entry) => [entry.id, entry]));
|
|
1767
|
+
for (const [id, button] of map) {
|
|
1768
|
+
try {
|
|
1769
|
+
const entry = byId.get(id);
|
|
1770
|
+
if (!entry) continue;
|
|
1771
|
+
const custom = customName(plan.prefs, id);
|
|
1772
|
+
const shown = custom || entry.label;
|
|
1773
|
+
// Keep the original label addressable: mapButtons() must keep matching
|
|
1774
|
+
// the same row after the user renames it.
|
|
1775
|
+
if (button.getAttribute('data-dpc-label') !== entry.label) {
|
|
1776
|
+
button.setAttribute('data-dpc-label', entry.label);
|
|
1777
|
+
}
|
|
1778
|
+
if (button.getAttribute('aria-label') !== shown) button.setAttribute('aria-label', shown);
|
|
1779
|
+
const title = titleSpanOf(button);
|
|
1780
|
+
if (title && title.textContent !== shown) title.textContent = shown;
|
|
1781
|
+
let tag = null;
|
|
1782
|
+
for (const child of Array.from(button.children || [])) {
|
|
1783
|
+
const className = typeof child.className === 'string' ? child.className : '';
|
|
1784
|
+
if (className.split(/\s+/).includes('dpc-nameTag')) tag = child;
|
|
1785
|
+
}
|
|
1786
|
+
// Only a renamed row needs the tag: it identifies the plugin by its
|
|
1787
|
+
// package (`dsh-` trimmed), which a custom display name can hide.
|
|
1788
|
+
const tagText = packageTagFor(id);
|
|
1789
|
+
const wantTag = Boolean(
|
|
1790
|
+
wide &&
|
|
1791
|
+
tagsOn &&
|
|
1792
|
+
custom &&
|
|
1793
|
+
custom !== entry.label &&
|
|
1794
|
+
tagText &&
|
|
1795
|
+
tagText.toLowerCase() !== shown.toLowerCase(),
|
|
1796
|
+
);
|
|
1797
|
+
if (wantTag) {
|
|
1798
|
+
if (!tag) {
|
|
1799
|
+
tag = document.createElement('span');
|
|
1800
|
+
tag.className = 'dpc-nameTag';
|
|
1801
|
+
tag.setAttribute('aria-hidden', 'true');
|
|
1802
|
+
button.appendChild(tag);
|
|
1803
|
+
}
|
|
1804
|
+
if (tag.textContent !== tagText) tag.textContent = tagText;
|
|
1805
|
+
} else if (tag) {
|
|
1806
|
+
tag.remove();
|
|
1807
|
+
}
|
|
1808
|
+
} catch {
|
|
1809
|
+
/* a host build with another row shape: leave that row alone */
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1598
1814
|
const apply = () => {
|
|
1599
1815
|
if (destroyed) return;
|
|
1600
1816
|
const plan = readPlan();
|
|
@@ -1623,10 +1839,21 @@ function build(require) {
|
|
|
1623
1839
|
const folded = folding ? new Set(plan.prefs.collapsed || []) : new Set();
|
|
1624
1840
|
diag.folded = Array.from(folded).join(',');
|
|
1625
1841
|
|
|
1842
|
+
// Renames are display state, like the height cap: apply them before the
|
|
1843
|
+
// signature shortcut so a pass that only renames still reaches the DOM.
|
|
1844
|
+
const buttons = Array.from(list.querySelectorAll(':scope > button'));
|
|
1845
|
+
const map = mapButtons(plan.entries, buttons);
|
|
1846
|
+
applyNames(map, plan, wide);
|
|
1847
|
+
// Package names label renamed rows; the first pass that knows the entries
|
|
1848
|
+
// asks the Host once, and later passes are deduplicated by the id set.
|
|
1849
|
+
void ensurePackages();
|
|
1850
|
+
|
|
1626
1851
|
const nextSignature = JSON.stringify({
|
|
1627
1852
|
blocks: plan.blocks,
|
|
1628
1853
|
hidden: plan.hidden ? Array.from(plan.hidden).sort() : [],
|
|
1629
1854
|
entries: plan.entries.map((entry) => entry.id + ':' + entry.label),
|
|
1855
|
+
names: plan.prefs.names || {},
|
|
1856
|
+
tags: plan.prefs.tags !== false,
|
|
1630
1857
|
wide,
|
|
1631
1858
|
folded: Array.from(folded),
|
|
1632
1859
|
});
|
|
@@ -1640,10 +1867,8 @@ function build(require) {
|
|
|
1640
1867
|
|
|
1641
1868
|
pause();
|
|
1642
1869
|
try {
|
|
1643
|
-
const buttons = Array.from(list.querySelectorAll(':scope > button'));
|
|
1644
1870
|
for (const node of Array.from(list.querySelectorAll('[data-dpc-sep]'))) node.remove();
|
|
1645
1871
|
|
|
1646
|
-
const map = mapButtons(plan.entries, buttons);
|
|
1647
1872
|
const managed = new Set(map.values());
|
|
1648
1873
|
|
|
1649
1874
|
// Anything we do not manage (the "More" launcher and its wrapper) keeps
|
|
@@ -1781,6 +2006,18 @@ function build(require) {
|
|
|
1781
2006
|
node.style.removeProperty('display');
|
|
1782
2007
|
}
|
|
1783
2008
|
}
|
|
2009
|
+
// Hand the host rows back exactly as they were: the original label, the
|
|
2010
|
+
// original title text, and no tag of ours.
|
|
2011
|
+
for (const button of Array.from(document.querySelectorAll('button[data-dpc-label]'))) {
|
|
2012
|
+
const label = button.getAttribute('data-dpc-label');
|
|
2013
|
+
if (label) {
|
|
2014
|
+
button.setAttribute('aria-label', label);
|
|
2015
|
+
const title = titleSpanOf(button);
|
|
2016
|
+
if (title) title.textContent = label;
|
|
2017
|
+
button.removeAttribute('data-dpc-label');
|
|
2018
|
+
}
|
|
2019
|
+
for (const tag of Array.from(button.querySelectorAll('.dpc-nameTag'))) tag.remove();
|
|
2020
|
+
}
|
|
1784
2021
|
for (const node of Array.from(document.querySelectorAll('[data-dpc-list]'))) {
|
|
1785
2022
|
node.removeAttribute('data-dpc-list');
|
|
1786
2023
|
node.removeAttribute('data-dpc-unbounded');
|
|
@@ -1868,7 +2105,9 @@ function build(require) {
|
|
|
1868
2105
|
}
|
|
1869
2106
|
|
|
1870
2107
|
function sortByName() {
|
|
1871
|
-
const
|
|
2108
|
+
const prefs = prefsNow();
|
|
2109
|
+
const entries = store.state.entries || [];
|
|
2110
|
+
const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
|
|
1872
2111
|
const locale = activeLang() === 'zh' ? 'zh-Hans-CN' : 'en';
|
|
1873
2112
|
const order = orderNow()
|
|
1874
2113
|
.slice()
|
|
@@ -2113,6 +2352,7 @@ function build(require) {
|
|
|
2113
2352
|
plus: 'M12 6v12M6 12h12',
|
|
2114
2353
|
eye: 'M2.5 12S6 6.5 12 6.5 21.5 12 21.5 12 18 17.5 12 17.5 2.5 12 2.5 12Z',
|
|
2115
2354
|
eyeOff: 'M2.5 12S6 6.5 12 6.5c1.6 0 3 .45 4.2 1.1M21.5 12S18 17.5 12 17.5c-1.6 0-3-.45-4.2-1.1M4 4l16 16',
|
|
2355
|
+
edit: 'M4 20h4l10.5-10.5a2.12 2.12 0 0 0-3-3L5 17v3Z',
|
|
2116
2356
|
back: 'M14.5 6.5 9 12l5.5 5.5',
|
|
2117
2357
|
};
|
|
2118
2358
|
|
|
@@ -2258,7 +2498,15 @@ function build(require) {
|
|
|
2258
2498
|
'span',
|
|
2259
2499
|
{ className: 'dpc-bridgeTags' },
|
|
2260
2500
|
h('span', { className: 'dpc-bridgeTag' }, t('bridgeTag')),
|
|
2261
|
-
h(
|
|
2501
|
+
h(
|
|
2502
|
+
'span',
|
|
2503
|
+
{ className: 'dpc-bridgeFrom' },
|
|
2504
|
+
fill(t('bridgeFrom'), {
|
|
2505
|
+
// The same package short name the row tag shows; the section
|
|
2506
|
+
// id is only a fallback while the Host lookup is in flight.
|
|
2507
|
+
p: packageTagFor(candidate.bridgeId) || candidate.sectionId,
|
|
2508
|
+
}),
|
|
2509
|
+
),
|
|
2262
2510
|
),
|
|
2263
2511
|
),
|
|
2264
2512
|
node
|
|
@@ -2373,6 +2621,9 @@ function build(require) {
|
|
|
2373
2621
|
// check runs again on focus, and the answer must not pop back up.
|
|
2374
2622
|
const [, forceRender] = React.useReducer((n) => n + 1, 0);
|
|
2375
2623
|
const [groupDrag, setGroupDrag] = React.useState({ from: null, over: null });
|
|
2624
|
+
const [editing, setEditing] = React.useState(null);
|
|
2625
|
+
const [draft, setDraft] = React.useState('');
|
|
2626
|
+
const cancelRenameRef = React.useRef(false);
|
|
2376
2627
|
/**
|
|
2377
2628
|
* Dragging works the same for a row and for a section: the handle starts it,
|
|
2378
2629
|
* the target highlights, dropping commits "move before this one". Only the
|
|
@@ -2436,12 +2687,44 @@ function build(require) {
|
|
|
2436
2687
|
}, [prefs.auto, prefs.autoFiled, entries.length, state.autoState && state.autoState.status]);
|
|
2437
2688
|
|
|
2438
2689
|
const order = normalize(prefs, entries);
|
|
2439
|
-
const
|
|
2690
|
+
const rawOf = new Map(entries.map((entry) => [entry.id, entry.label]));
|
|
2691
|
+
const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
|
|
2440
2692
|
const hidden = new Set(prefs.hidden || []);
|
|
2441
2693
|
const groupsOf = prefs.groups || [];
|
|
2442
2694
|
const autoOn = prefs.auto === true;
|
|
2443
2695
|
const autoState = state.autoState || { status: 'idle', message: '', groups: 0 };
|
|
2444
2696
|
|
|
2697
|
+
const startRename = (id) => {
|
|
2698
|
+
cancelRenameRef.current = false;
|
|
2699
|
+
setEditing(id);
|
|
2700
|
+
setDraft(customName(prefsNow(), id));
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2703
|
+
const finishRename = (id) => {
|
|
2704
|
+
// Escape unmounts the input; a trailing blur must not save the draft.
|
|
2705
|
+
if (cancelRenameRef.current) {
|
|
2706
|
+
cancelRenameRef.current = false;
|
|
2707
|
+
setEditing(null);
|
|
2708
|
+
setDraft('');
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
const original = rawOf.get(id) || id;
|
|
2712
|
+
const name = cleanName(draft);
|
|
2713
|
+
const names = { ...(prefsNow().names || {}) };
|
|
2714
|
+
if (!name || name === original) delete names[id];
|
|
2715
|
+
else names[id] = name;
|
|
2716
|
+
setEditing(null);
|
|
2717
|
+
setDraft('');
|
|
2718
|
+
commit({ ...prefsNow(), names });
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2721
|
+
const cancelRename = () => {
|
|
2722
|
+
cancelRenameRef.current = true;
|
|
2723
|
+
setEditing(null);
|
|
2724
|
+
setDraft('');
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2727
|
+
|
|
2445
2728
|
const head = h(
|
|
2446
2729
|
'header',
|
|
2447
2730
|
{ className: inSection ? 'dpc-head dpc-head--section' : 'dpc-head' },
|
|
@@ -2599,6 +2882,7 @@ function build(require) {
|
|
|
2599
2882
|
|
|
2600
2883
|
const rows = order.map((id, position) => {
|
|
2601
2884
|
const label = labelOf.get(id) || id;
|
|
2885
|
+
const rawLabel = rawOf.get(id) || id;
|
|
2602
2886
|
const isHidden = hidden.has(id);
|
|
2603
2887
|
const locked = id === PANEL_KEY;
|
|
2604
2888
|
return h(
|
|
@@ -2616,8 +2900,32 @@ function build(require) {
|
|
|
2616
2900
|
h(
|
|
2617
2901
|
'span',
|
|
2618
2902
|
{ className: 'dpc-rowText' },
|
|
2619
|
-
|
|
2620
|
-
|
|
2903
|
+
editing === id
|
|
2904
|
+
? h('input', {
|
|
2905
|
+
className: 'dpc-input dpc-nameInput',
|
|
2906
|
+
// autoFocus is enough: opening the editor is an explicit click.
|
|
2907
|
+
autoFocus: true,
|
|
2908
|
+
value: draft,
|
|
2909
|
+
placeholder: rawLabel,
|
|
2910
|
+
'aria-label': t('renameIt'),
|
|
2911
|
+
onChange: (event) => setDraft(event.target.value),
|
|
2912
|
+
onKeyDown: (event) => {
|
|
2913
|
+
if (event.key === 'Enter') {
|
|
2914
|
+
event.preventDefault();
|
|
2915
|
+
finishRename(id);
|
|
2916
|
+
} else if (event.key === 'Escape') {
|
|
2917
|
+
event.preventDefault();
|
|
2918
|
+
cancelRename();
|
|
2919
|
+
}
|
|
2920
|
+
},
|
|
2921
|
+
onBlur: () => finishRename(id),
|
|
2922
|
+
})
|
|
2923
|
+
: h(
|
|
2924
|
+
'span',
|
|
2925
|
+
{ className: 'dpc-name', title: label === rawLabel ? rawLabel : label + ' · ' + rawLabel },
|
|
2926
|
+
label,
|
|
2927
|
+
),
|
|
2928
|
+
editing === id ? null : h('code', { className: 'dpc-key', title: id }, id),
|
|
2621
2929
|
),
|
|
2622
2930
|
h(
|
|
2623
2931
|
'span',
|
|
@@ -2635,6 +2943,9 @@ function build(require) {
|
|
|
2635
2943
|
ungrouped: t('ungrouped'),
|
|
2636
2944
|
onPick: (next) => setGroup(id, next),
|
|
2637
2945
|
}),
|
|
2946
|
+
iconButton('edit', t('renameIt'), () => startRename(id), false, {
|
|
2947
|
+
'aria-pressed': editing === id ? 'true' : 'false',
|
|
2948
|
+
}),
|
|
2638
2949
|
iconButton(
|
|
2639
2950
|
isHidden ? 'eyeOff' : 'eye',
|
|
2640
2951
|
locked ? t('lockedHint') : isHidden ? t('showIt') : t('hideIt'),
|
|
@@ -2676,6 +2987,7 @@ function build(require) {
|
|
|
2676
2987
|
const layoutOn = prefs.guard !== false;
|
|
2677
2988
|
const foldingOn = prefs.collapse !== false;
|
|
2678
2989
|
const bridgeOn = prefs.bridge !== false;
|
|
2990
|
+
const tagsOn = prefs.tags !== false;
|
|
2679
2991
|
const capNow = typeof prefs.panelMax === 'number' ? prefs.panelMax : 45;
|
|
2680
2992
|
const setLayout = (patch) => commit({ ...prefsNow(), ...patch });
|
|
2681
2993
|
const layoutSwitch = (on, label, onClick) =>
|
|
@@ -2726,6 +3038,11 @@ function build(require) {
|
|
|
2726
3038
|
t('layoutBridgeHint'),
|
|
2727
3039
|
layoutSwitch(bridgeOn, t('layoutBridge'), () => setLayout({ bridge: !bridgeOn })),
|
|
2728
3040
|
),
|
|
3041
|
+
layoutRow(
|
|
3042
|
+
t('layoutTags'),
|
|
3043
|
+
t('layoutTagsHint'),
|
|
3044
|
+
layoutSwitch(tagsOn, t('layoutTags'), () => setLayout({ tags: !tagsOn })),
|
|
3045
|
+
),
|
|
2729
3046
|
layoutRow(
|
|
2730
3047
|
t('layoutMax'),
|
|
2731
3048
|
null,
|
|
@@ -2928,6 +3245,7 @@ function build(require) {
|
|
|
2928
3245
|
.dpc-glyph:empty::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.4}
|
|
2929
3246
|
.dpc-rowText{display:flex;flex:1 1 auto;flex-direction:row;align-items:baseline;gap:8px;min-width:0}
|
|
2930
3247
|
.dpc-name{font-size:13px;font-weight:500;line-height:18px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3248
|
+
.dpc-nameInput{flex:1 1 120px;max-width:320px;font-size:13px;line-height:18px}
|
|
2931
3249
|
.dpc-key{font-family:var(--dsw-font-family-code,ui-monospace,monospace);font-size:11px;line-height:16px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.7));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2932
3250
|
.dpc-tags{display:flex;flex:none;align-items:center;gap:8px}
|
|
2933
3251
|
.dpc-tag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));white-space:nowrap}
|
|
@@ -2942,7 +3260,9 @@ function build(require) {
|
|
|
2942
3260
|
.dpc-pickerText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2943
3261
|
.dpc-caret{flex:none;width:6px;height:6px;margin-top:-2px;border-right:1.2px solid currentColor;border-bottom:1.2px solid currentColor;transform:rotate(45deg);opacity:.7;transition:transform 120ms cubic-bezier(.16,1,.3,1)}
|
|
2944
3262
|
.dpc-picker[data-open="true"] .dpc-caret{margin-top:2px;transform:rotate(225deg)}
|
|
2945
|
-
|
|
3263
|
+
/* Opaque popover surface: --dsw-alias-bg-layer-2 is the host's own elevated
|
|
3264
|
+
layer, so the menu never lets the card behind it show through. */
|
|
3265
|
+
.dpc-menu{position:absolute;z-index:50;top:calc(100% + 4px);right:0;display:flex;flex-direction:column;gap:2px;min-width:144px;max-width:240px;padding:4px;background-color:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#1f1f1f));border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.24));border-radius:var(--dsw-radius-md,8px);box-shadow:0 8px 24px rgba(0,0,0,.28)}
|
|
2946
3266
|
.dpc-option{appearance:none;border:0;padding:4px 8px;border-radius:var(--dsw-radius-xs,4px);background:transparent;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95));font:inherit;font-size:12px;line-height:16px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2947
3267
|
.dpc-option:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18));color:var(--dsw-alias-label-primary,#8a8a8a)}
|
|
2948
3268
|
.dpc-option[aria-selected="true"]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#5865f2) 16%,transparent);color:var(--dsw-alias-label-primary-bluish,#7f8fd8)}
|
|
@@ -3045,6 +3365,10 @@ function build(require) {
|
|
|
3045
3365
|
[data-dpc-list]>button{flex:none}
|
|
3046
3366
|
[data-dpc-region]{flex:1 1 auto;min-height:min(120px,18vh)}
|
|
3047
3367
|
|
|
3368
|
+
/* The original name of a renamed row. It sits after the host title, pushed
|
|
3369
|
+
to the far right; the host row stays a single click target. */
|
|
3370
|
+
.dpc-nameTag{display:inline-block;flex:none;margin-left:auto;max-width:50%;height:16px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-l2,rgba(127,127,127,.14));color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));font-size:10px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
|
|
3371
|
+
|
|
3048
3372
|
.dpc-sep{display:flex;align-items:center;gap:6px;flex:none;padding:12px 8px 4px;font-size:11px;font-weight:500;line-height:14px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.78));white-space:nowrap;overflow:hidden;cursor:pointer;border-radius:var(--dsw-radius-sm,6px);user-select:none}
|
|
3049
3373
|
.dpc-sep:hover{color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
|
|
3050
3374
|
.dpc-sep:focus-visible{outline:2px solid var(--dsw-focus-ring-color,rgba(127,127,127,.65));outline-offset:1px}
|
|
@@ -3244,6 +3568,9 @@ function build(require) {
|
|
|
3244
3568
|
}
|
|
3245
3569
|
ctx.effect(() => () => disposeBridges(), `${PACKAGE}: bridge cleanup`);
|
|
3246
3570
|
kickBridge();
|
|
3571
|
+
// Renames show the owning package; the first scan answers the current rows,
|
|
3572
|
+
// later bridge scans refresh the set when more settings pages register.
|
|
3573
|
+
void ensurePackages();
|
|
3247
3574
|
try { window.__dpcApplied = true; } catch { /* ignore */ }
|
|
3248
3575
|
}
|
|
3249
3576
|
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
`props.renderSlot(slotName, { close: back }, {})` 渲染它。
|
|
45
45
|
|
|
46
46
|
这样 section 的 `inject`、`store`、locale `t`、`useStore` 和 error boundary 仍由宿主照常提供;直接调用
|
|
47
|
-
section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+
|
|
48
|
-
|
|
47
|
+
section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源包名短名,
|
|
48
|
+
查不到包名时退回 section id),下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
|
|
49
49
|
(`layout.selectPanel(null)`)。
|
|
50
50
|
|
|
51
51
|
桥接面板不常驻:开关关闭、section 消失、插件卸载时,`dispose()` 成对撤掉 `main` 与
|