dsh-panel-control 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/client.js +332 -15
- package/docs/renaming-panels.md +74 -0
- package/index.js +123 -2
- package/package.json +5 -2
- package/scripts/host-smoke.mjs +39 -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 的模块图解析,`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.1
|
|
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`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包回退到入口 id,以及 `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 模块图解析,接口不可用时标签回退为入口 id。
|
|
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.1';
|
|
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,84 @@ 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
|
+
const pkg = packageState.map[id];
|
|
1042
|
+
return packageLabel(pkg) || packageLabel(id);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Ask the Host which package each visible row belongs to.
|
|
1047
|
+
*
|
|
1048
|
+
* Scans are de-duplicated by the exact id set, so the engine's many passes and
|
|
1049
|
+
* every bridge re-sync cost one call per set rather than one per pass.
|
|
1050
|
+
*/
|
|
1051
|
+
async function ensurePackages(force) {
|
|
1052
|
+
const records = [];
|
|
1053
|
+
const seen = new Set();
|
|
1054
|
+
const add = (id, slot) => {
|
|
1055
|
+
if (typeof id !== 'string' || id === '' || seen.has(slot + ':' + id)) return;
|
|
1056
|
+
seen.add(slot + ':' + id);
|
|
1057
|
+
records.push({ id, slot });
|
|
1058
|
+
};
|
|
1059
|
+
for (const entry of store.state.entries) {
|
|
1060
|
+
if (entry.id.startsWith(BRIDGE_PREFIX)) continue;
|
|
1061
|
+
add(entry.id, SLOT_PANELS);
|
|
1062
|
+
}
|
|
1063
|
+
for (const record of bridgeState.entries.values()) {
|
|
1064
|
+
if (record && typeof record.sectionId === 'string') add(record.sectionId, SLOT_SETTINGS);
|
|
1065
|
+
}
|
|
1066
|
+
if (records.length === 0) return;
|
|
1067
|
+
records.sort((left, right) => (left.slot + ':' + left.id).localeCompare(right.slot + ':' + right.id));
|
|
1068
|
+
const key = records.map((row) => row.slot + ':' + row.id).join('|');
|
|
1069
|
+
if (!force && key === packageState.key) return;
|
|
1070
|
+
if (packageState.loading) {
|
|
1071
|
+
packageState.pending = key;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
packageState.key = key;
|
|
1075
|
+
packageState.loading = true;
|
|
1076
|
+
try {
|
|
1077
|
+
const value = await hostCall('packages', { ids: records });
|
|
1078
|
+
const packages = value && value.packages;
|
|
1079
|
+
if (!packages || typeof packages !== 'object') return;
|
|
1080
|
+
let changed = false;
|
|
1081
|
+
for (const [id, pkg] of Object.entries(packages)) {
|
|
1082
|
+
if (typeof pkg !== 'string' || pkg === '') continue;
|
|
1083
|
+
if (packageState.map[id] === pkg) continue;
|
|
1084
|
+
packageState.map[id] = pkg;
|
|
1085
|
+
changed = true;
|
|
1086
|
+
}
|
|
1087
|
+
// A bridge row's own id is synthetic; its package is the section's.
|
|
1088
|
+
for (const [bridgeId, record] of bridgeState.entries) {
|
|
1089
|
+
const pkg = record && packageState.map[record.sectionId];
|
|
1090
|
+
if (pkg && packageState.map[bridgeId] !== pkg) {
|
|
1091
|
+
packageState.map[bridgeId] = pkg;
|
|
1092
|
+
changed = true;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (changed && engineRef) engineRef.schedule();
|
|
1096
|
+
} finally {
|
|
1097
|
+
packageState.loading = false;
|
|
1098
|
+
const next = packageState.pending;
|
|
1099
|
+
packageState.pending = '';
|
|
1100
|
+
if (next && next !== packageState.key) void ensurePackages();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
966
1104
|
function registrantOf(entry) {
|
|
967
1105
|
const options = (entry && entry.options) || {};
|
|
968
1106
|
const value = (entry && entry.registrant) || options.registrant;
|
|
@@ -1111,7 +1249,7 @@ function build(require) {
|
|
|
1111
1249
|
for (const candidate of candidates) {
|
|
1112
1250
|
if (bridgeState.entries.has(candidate.bridgeId)) continue;
|
|
1113
1251
|
const slotName = BRIDGE_SLOT_PREFIX + candidate.sectionId;
|
|
1114
|
-
const record = { disposers: [] };
|
|
1252
|
+
const record = { sectionId: candidate.sectionId, disposers: [] };
|
|
1115
1253
|
try {
|
|
1116
1254
|
record.disposers.push(
|
|
1117
1255
|
slots.register(
|
|
@@ -1189,6 +1327,9 @@ function build(require) {
|
|
|
1189
1327
|
// re-render it would keep showing the value from before the sync.
|
|
1190
1328
|
emit();
|
|
1191
1329
|
}
|
|
1330
|
+
// New bridge rows (or a first scan that finally sees the real rows) need
|
|
1331
|
+
// their package names; the call is keyed, so repeated scans are free.
|
|
1332
|
+
void ensurePackages();
|
|
1192
1333
|
} catch (error) {
|
|
1193
1334
|
console.warn(`[${PACKAGE}] bridge sync failed`, error);
|
|
1194
1335
|
} finally {
|
|
@@ -1316,7 +1457,9 @@ function build(require) {
|
|
|
1316
1457
|
const used = new Set();
|
|
1317
1458
|
const byLabel = new Map();
|
|
1318
1459
|
for (const button of buttons) {
|
|
1319
|
-
|
|
1460
|
+
// A renamed row keeps its original label in `data-dpc-label`: the visible
|
|
1461
|
+
// text and aria-label change, but the entry it belongs to does not.
|
|
1462
|
+
const label = button.getAttribute('data-dpc-label') || button.getAttribute('aria-label') || '';
|
|
1320
1463
|
if (!byLabel.has(label)) byLabel.set(label, []);
|
|
1321
1464
|
byLabel.get(label).push(button);
|
|
1322
1465
|
}
|
|
@@ -1335,7 +1478,7 @@ function build(require) {
|
|
|
1335
1478
|
// Labels that agree prove the two lists are the same list; a button with no
|
|
1336
1479
|
// label at all is no evidence either way, so it may be filled positionally.
|
|
1337
1480
|
const sameOrder = loose.every((entry, index) => {
|
|
1338
|
-
const label = rest[index].getAttribute('aria-label') || '';
|
|
1481
|
+
const label = rest[index].getAttribute('data-dpc-label') || rest[index].getAttribute('aria-label') || '';
|
|
1339
1482
|
return label === '' || label === entry.label;
|
|
1340
1483
|
});
|
|
1341
1484
|
// A build that labels rows in wording this plugin never sees ("IM 机器人"
|
|
@@ -1447,7 +1590,10 @@ function build(require) {
|
|
|
1447
1590
|
if (!entry.label) continue;
|
|
1448
1591
|
let button = null;
|
|
1449
1592
|
try {
|
|
1450
|
-
|
|
1593
|
+
const label = cssEscape(entry.label);
|
|
1594
|
+
button = document.querySelector(
|
|
1595
|
+
`button[data-dpc-label="${label}"], button[aria-label="${label}"]`,
|
|
1596
|
+
);
|
|
1451
1597
|
} catch {
|
|
1452
1598
|
button = null;
|
|
1453
1599
|
}
|
|
@@ -1595,6 +1741,74 @@ function build(require) {
|
|
|
1595
1741
|
}
|
|
1596
1742
|
};
|
|
1597
1743
|
|
|
1744
|
+
/** The title node of a host panel row: the only direct span that is not the
|
|
1745
|
+
* aria-hidden glyph and not the original-name tag this plugin adds. */
|
|
1746
|
+
const titleSpanOf = (button) => {
|
|
1747
|
+
for (const child of Array.from(button.children || [])) {
|
|
1748
|
+
if (child.tagName !== 'SPAN') continue;
|
|
1749
|
+
const className = typeof child.className === 'string' ? child.className : '';
|
|
1750
|
+
if (className.split(/\s+/).includes('dpc-nameTag')) continue;
|
|
1751
|
+
if (child.getAttribute('aria-hidden') === 'true') continue;
|
|
1752
|
+
return child;
|
|
1753
|
+
}
|
|
1754
|
+
return null;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Rename rows in the host sidebar, and keep the original label visible in a
|
|
1759
|
+
* small tag. The host owns this DOM, so this is a pure display overlay: the
|
|
1760
|
+
* entry id, its slot registration and every other preference are untouched.
|
|
1761
|
+
*/
|
|
1762
|
+
const applyNames = (map, plan, wide) => {
|
|
1763
|
+
const tagsOn = plan.prefs.tags !== false;
|
|
1764
|
+
const byId = new Map(plan.entries.map((entry) => [entry.id, entry]));
|
|
1765
|
+
for (const [id, button] of map) {
|
|
1766
|
+
try {
|
|
1767
|
+
const entry = byId.get(id);
|
|
1768
|
+
if (!entry) continue;
|
|
1769
|
+
const custom = customName(plan.prefs, id);
|
|
1770
|
+
const shown = custom || entry.label;
|
|
1771
|
+
// Keep the original label addressable: mapButtons() must keep matching
|
|
1772
|
+
// the same row after the user renames it.
|
|
1773
|
+
if (button.getAttribute('data-dpc-label') !== entry.label) {
|
|
1774
|
+
button.setAttribute('data-dpc-label', entry.label);
|
|
1775
|
+
}
|
|
1776
|
+
if (button.getAttribute('aria-label') !== shown) button.setAttribute('aria-label', shown);
|
|
1777
|
+
const title = titleSpanOf(button);
|
|
1778
|
+
if (title && title.textContent !== shown) title.textContent = shown;
|
|
1779
|
+
let tag = null;
|
|
1780
|
+
for (const child of Array.from(button.children || [])) {
|
|
1781
|
+
const className = typeof child.className === 'string' ? child.className : '';
|
|
1782
|
+
if (className.split(/\s+/).includes('dpc-nameTag')) tag = child;
|
|
1783
|
+
}
|
|
1784
|
+
// Only a renamed row needs the tag: it identifies the plugin by its
|
|
1785
|
+
// package (`dsh-` trimmed), which a custom display name can hide.
|
|
1786
|
+
const tagText = packageTagFor(id);
|
|
1787
|
+
const wantTag = Boolean(
|
|
1788
|
+
wide &&
|
|
1789
|
+
tagsOn &&
|
|
1790
|
+
custom &&
|
|
1791
|
+
custom !== entry.label &&
|
|
1792
|
+
tagText &&
|
|
1793
|
+
tagText.toLowerCase() !== shown.toLowerCase(),
|
|
1794
|
+
);
|
|
1795
|
+
if (wantTag) {
|
|
1796
|
+
if (!tag) {
|
|
1797
|
+
tag = document.createElement('span');
|
|
1798
|
+
tag.className = 'dpc-nameTag';
|
|
1799
|
+
tag.setAttribute('aria-hidden', 'true');
|
|
1800
|
+
button.appendChild(tag);
|
|
1801
|
+
}
|
|
1802
|
+
if (tag.textContent !== tagText) tag.textContent = tagText;
|
|
1803
|
+
} else if (tag) {
|
|
1804
|
+
tag.remove();
|
|
1805
|
+
}
|
|
1806
|
+
} catch {
|
|
1807
|
+
/* a host build with another row shape: leave that row alone */
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1598
1812
|
const apply = () => {
|
|
1599
1813
|
if (destroyed) return;
|
|
1600
1814
|
const plan = readPlan();
|
|
@@ -1623,10 +1837,21 @@ function build(require) {
|
|
|
1623
1837
|
const folded = folding ? new Set(plan.prefs.collapsed || []) : new Set();
|
|
1624
1838
|
diag.folded = Array.from(folded).join(',');
|
|
1625
1839
|
|
|
1840
|
+
// Renames are display state, like the height cap: apply them before the
|
|
1841
|
+
// signature shortcut so a pass that only renames still reaches the DOM.
|
|
1842
|
+
const buttons = Array.from(list.querySelectorAll(':scope > button'));
|
|
1843
|
+
const map = mapButtons(plan.entries, buttons);
|
|
1844
|
+
applyNames(map, plan, wide);
|
|
1845
|
+
// Package names label renamed rows; the first pass that knows the entries
|
|
1846
|
+
// asks the Host once, and later passes are deduplicated by the id set.
|
|
1847
|
+
void ensurePackages();
|
|
1848
|
+
|
|
1626
1849
|
const nextSignature = JSON.stringify({
|
|
1627
1850
|
blocks: plan.blocks,
|
|
1628
1851
|
hidden: plan.hidden ? Array.from(plan.hidden).sort() : [],
|
|
1629
1852
|
entries: plan.entries.map((entry) => entry.id + ':' + entry.label),
|
|
1853
|
+
names: plan.prefs.names || {},
|
|
1854
|
+
tags: plan.prefs.tags !== false,
|
|
1630
1855
|
wide,
|
|
1631
1856
|
folded: Array.from(folded),
|
|
1632
1857
|
});
|
|
@@ -1640,10 +1865,8 @@ function build(require) {
|
|
|
1640
1865
|
|
|
1641
1866
|
pause();
|
|
1642
1867
|
try {
|
|
1643
|
-
const buttons = Array.from(list.querySelectorAll(':scope > button'));
|
|
1644
1868
|
for (const node of Array.from(list.querySelectorAll('[data-dpc-sep]'))) node.remove();
|
|
1645
1869
|
|
|
1646
|
-
const map = mapButtons(plan.entries, buttons);
|
|
1647
1870
|
const managed = new Set(map.values());
|
|
1648
1871
|
|
|
1649
1872
|
// Anything we do not manage (the "More" launcher and its wrapper) keeps
|
|
@@ -1781,6 +2004,18 @@ function build(require) {
|
|
|
1781
2004
|
node.style.removeProperty('display');
|
|
1782
2005
|
}
|
|
1783
2006
|
}
|
|
2007
|
+
// Hand the host rows back exactly as they were: the original label, the
|
|
2008
|
+
// original title text, and no tag of ours.
|
|
2009
|
+
for (const button of Array.from(document.querySelectorAll('button[data-dpc-label]'))) {
|
|
2010
|
+
const label = button.getAttribute('data-dpc-label');
|
|
2011
|
+
if (label) {
|
|
2012
|
+
button.setAttribute('aria-label', label);
|
|
2013
|
+
const title = titleSpanOf(button);
|
|
2014
|
+
if (title) title.textContent = label;
|
|
2015
|
+
button.removeAttribute('data-dpc-label');
|
|
2016
|
+
}
|
|
2017
|
+
for (const tag of Array.from(button.querySelectorAll('.dpc-nameTag'))) tag.remove();
|
|
2018
|
+
}
|
|
1784
2019
|
for (const node of Array.from(document.querySelectorAll('[data-dpc-list]'))) {
|
|
1785
2020
|
node.removeAttribute('data-dpc-list');
|
|
1786
2021
|
node.removeAttribute('data-dpc-unbounded');
|
|
@@ -1868,7 +2103,9 @@ function build(require) {
|
|
|
1868
2103
|
}
|
|
1869
2104
|
|
|
1870
2105
|
function sortByName() {
|
|
1871
|
-
const
|
|
2106
|
+
const prefs = prefsNow();
|
|
2107
|
+
const entries = store.state.entries || [];
|
|
2108
|
+
const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
|
|
1872
2109
|
const locale = activeLang() === 'zh' ? 'zh-Hans-CN' : 'en';
|
|
1873
2110
|
const order = orderNow()
|
|
1874
2111
|
.slice()
|
|
@@ -2113,6 +2350,7 @@ function build(require) {
|
|
|
2113
2350
|
plus: 'M12 6v12M6 12h12',
|
|
2114
2351
|
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
2352
|
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',
|
|
2353
|
+
edit: 'M4 20h4l10.5-10.5a2.12 2.12 0 0 0-3-3L5 17v3Z',
|
|
2116
2354
|
back: 'M14.5 6.5 9 12l5.5 5.5',
|
|
2117
2355
|
};
|
|
2118
2356
|
|
|
@@ -2373,6 +2611,9 @@ function build(require) {
|
|
|
2373
2611
|
// check runs again on focus, and the answer must not pop back up.
|
|
2374
2612
|
const [, forceRender] = React.useReducer((n) => n + 1, 0);
|
|
2375
2613
|
const [groupDrag, setGroupDrag] = React.useState({ from: null, over: null });
|
|
2614
|
+
const [editing, setEditing] = React.useState(null);
|
|
2615
|
+
const [draft, setDraft] = React.useState('');
|
|
2616
|
+
const cancelRenameRef = React.useRef(false);
|
|
2376
2617
|
/**
|
|
2377
2618
|
* Dragging works the same for a row and for a section: the handle starts it,
|
|
2378
2619
|
* the target highlights, dropping commits "move before this one". Only the
|
|
@@ -2436,12 +2677,44 @@ function build(require) {
|
|
|
2436
2677
|
}, [prefs.auto, prefs.autoFiled, entries.length, state.autoState && state.autoState.status]);
|
|
2437
2678
|
|
|
2438
2679
|
const order = normalize(prefs, entries);
|
|
2439
|
-
const
|
|
2680
|
+
const rawOf = new Map(entries.map((entry) => [entry.id, entry.label]));
|
|
2681
|
+
const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
|
|
2440
2682
|
const hidden = new Set(prefs.hidden || []);
|
|
2441
2683
|
const groupsOf = prefs.groups || [];
|
|
2442
2684
|
const autoOn = prefs.auto === true;
|
|
2443
2685
|
const autoState = state.autoState || { status: 'idle', message: '', groups: 0 };
|
|
2444
2686
|
|
|
2687
|
+
const startRename = (id) => {
|
|
2688
|
+
cancelRenameRef.current = false;
|
|
2689
|
+
setEditing(id);
|
|
2690
|
+
setDraft(customName(prefsNow(), id));
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
const finishRename = (id) => {
|
|
2694
|
+
// Escape unmounts the input; a trailing blur must not save the draft.
|
|
2695
|
+
if (cancelRenameRef.current) {
|
|
2696
|
+
cancelRenameRef.current = false;
|
|
2697
|
+
setEditing(null);
|
|
2698
|
+
setDraft('');
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
const original = rawOf.get(id) || id;
|
|
2702
|
+
const name = cleanName(draft);
|
|
2703
|
+
const names = { ...(prefsNow().names || {}) };
|
|
2704
|
+
if (!name || name === original) delete names[id];
|
|
2705
|
+
else names[id] = name;
|
|
2706
|
+
setEditing(null);
|
|
2707
|
+
setDraft('');
|
|
2708
|
+
commit({ ...prefsNow(), names });
|
|
2709
|
+
};
|
|
2710
|
+
|
|
2711
|
+
const cancelRename = () => {
|
|
2712
|
+
cancelRenameRef.current = true;
|
|
2713
|
+
setEditing(null);
|
|
2714
|
+
setDraft('');
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
|
|
2445
2718
|
const head = h(
|
|
2446
2719
|
'header',
|
|
2447
2720
|
{ className: inSection ? 'dpc-head dpc-head--section' : 'dpc-head' },
|
|
@@ -2599,6 +2872,7 @@ function build(require) {
|
|
|
2599
2872
|
|
|
2600
2873
|
const rows = order.map((id, position) => {
|
|
2601
2874
|
const label = labelOf.get(id) || id;
|
|
2875
|
+
const rawLabel = rawOf.get(id) || id;
|
|
2602
2876
|
const isHidden = hidden.has(id);
|
|
2603
2877
|
const locked = id === PANEL_KEY;
|
|
2604
2878
|
return h(
|
|
@@ -2616,8 +2890,32 @@ function build(require) {
|
|
|
2616
2890
|
h(
|
|
2617
2891
|
'span',
|
|
2618
2892
|
{ className: 'dpc-rowText' },
|
|
2619
|
-
|
|
2620
|
-
|
|
2893
|
+
editing === id
|
|
2894
|
+
? h('input', {
|
|
2895
|
+
className: 'dpc-input dpc-nameInput',
|
|
2896
|
+
// autoFocus is enough: opening the editor is an explicit click.
|
|
2897
|
+
autoFocus: true,
|
|
2898
|
+
value: draft,
|
|
2899
|
+
placeholder: rawLabel,
|
|
2900
|
+
'aria-label': t('renameIt'),
|
|
2901
|
+
onChange: (event) => setDraft(event.target.value),
|
|
2902
|
+
onKeyDown: (event) => {
|
|
2903
|
+
if (event.key === 'Enter') {
|
|
2904
|
+
event.preventDefault();
|
|
2905
|
+
finishRename(id);
|
|
2906
|
+
} else if (event.key === 'Escape') {
|
|
2907
|
+
event.preventDefault();
|
|
2908
|
+
cancelRename();
|
|
2909
|
+
}
|
|
2910
|
+
},
|
|
2911
|
+
onBlur: () => finishRename(id),
|
|
2912
|
+
})
|
|
2913
|
+
: h(
|
|
2914
|
+
'span',
|
|
2915
|
+
{ className: 'dpc-name', title: label === rawLabel ? rawLabel : label + ' · ' + rawLabel },
|
|
2916
|
+
label,
|
|
2917
|
+
),
|
|
2918
|
+
editing === id ? null : h('code', { className: 'dpc-key', title: id }, id),
|
|
2621
2919
|
),
|
|
2622
2920
|
h(
|
|
2623
2921
|
'span',
|
|
@@ -2635,6 +2933,9 @@ function build(require) {
|
|
|
2635
2933
|
ungrouped: t('ungrouped'),
|
|
2636
2934
|
onPick: (next) => setGroup(id, next),
|
|
2637
2935
|
}),
|
|
2936
|
+
iconButton('edit', t('renameIt'), () => startRename(id), false, {
|
|
2937
|
+
'aria-pressed': editing === id ? 'true' : 'false',
|
|
2938
|
+
}),
|
|
2638
2939
|
iconButton(
|
|
2639
2940
|
isHidden ? 'eyeOff' : 'eye',
|
|
2640
2941
|
locked ? t('lockedHint') : isHidden ? t('showIt') : t('hideIt'),
|
|
@@ -2676,6 +2977,7 @@ function build(require) {
|
|
|
2676
2977
|
const layoutOn = prefs.guard !== false;
|
|
2677
2978
|
const foldingOn = prefs.collapse !== false;
|
|
2678
2979
|
const bridgeOn = prefs.bridge !== false;
|
|
2980
|
+
const tagsOn = prefs.tags !== false;
|
|
2679
2981
|
const capNow = typeof prefs.panelMax === 'number' ? prefs.panelMax : 45;
|
|
2680
2982
|
const setLayout = (patch) => commit({ ...prefsNow(), ...patch });
|
|
2681
2983
|
const layoutSwitch = (on, label, onClick) =>
|
|
@@ -2726,6 +3028,11 @@ function build(require) {
|
|
|
2726
3028
|
t('layoutBridgeHint'),
|
|
2727
3029
|
layoutSwitch(bridgeOn, t('layoutBridge'), () => setLayout({ bridge: !bridgeOn })),
|
|
2728
3030
|
),
|
|
3031
|
+
layoutRow(
|
|
3032
|
+
t('layoutTags'),
|
|
3033
|
+
t('layoutTagsHint'),
|
|
3034
|
+
layoutSwitch(tagsOn, t('layoutTags'), () => setLayout({ tags: !tagsOn })),
|
|
3035
|
+
),
|
|
2729
3036
|
layoutRow(
|
|
2730
3037
|
t('layoutMax'),
|
|
2731
3038
|
null,
|
|
@@ -2928,6 +3235,7 @@ function build(require) {
|
|
|
2928
3235
|
.dpc-glyph:empty::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.4}
|
|
2929
3236
|
.dpc-rowText{display:flex;flex:1 1 auto;flex-direction:row;align-items:baseline;gap:8px;min-width:0}
|
|
2930
3237
|
.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}
|
|
3238
|
+
.dpc-nameInput{flex:1 1 120px;max-width:320px;font-size:13px;line-height:18px}
|
|
2931
3239
|
.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
3240
|
.dpc-tags{display:flex;flex:none;align-items:center;gap:8px}
|
|
2933
3241
|
.dpc-tag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));white-space:nowrap}
|
|
@@ -2942,7 +3250,9 @@ function build(require) {
|
|
|
2942
3250
|
.dpc-pickerText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2943
3251
|
.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
3252
|
.dpc-picker[data-open="true"] .dpc-caret{margin-top:2px;transform:rotate(225deg)}
|
|
2945
|
-
|
|
3253
|
+
/* Opaque popover surface: --dsw-alias-bg-layer-2 is the host's own elevated
|
|
3254
|
+
layer, so the menu never lets the card behind it show through. */
|
|
3255
|
+
.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
3256
|
.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
3257
|
.dpc-option:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18));color:var(--dsw-alias-label-primary,#8a8a8a)}
|
|
2948
3258
|
.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 +3355,10 @@ function build(require) {
|
|
|
3045
3355
|
[data-dpc-list]>button{flex:none}
|
|
3046
3356
|
[data-dpc-region]{flex:1 1 auto;min-height:min(120px,18vh)}
|
|
3047
3357
|
|
|
3358
|
+
/* The original name of a renamed row. It sits after the host title, pushed
|
|
3359
|
+
to the far right; the host row stays a single click target. */
|
|
3360
|
+
.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}
|
|
3361
|
+
|
|
3048
3362
|
.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
3363
|
.dpc-sep:hover{color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
|
|
3050
3364
|
.dpc-sep:focus-visible{outline:2px solid var(--dsw-focus-ring-color,rgba(127,127,127,.65));outline-offset:1px}
|
|
@@ -3244,6 +3558,9 @@ function build(require) {
|
|
|
3244
3558
|
}
|
|
3245
3559
|
ctx.effect(() => () => disposeBridges(), `${PACKAGE}: bridge cleanup`);
|
|
3246
3560
|
kickBridge();
|
|
3561
|
+
// Renames show the owning package; the first scan answers the current rows,
|
|
3562
|
+
// later bridge scans refresh the set when more settings pages register.
|
|
3563
|
+
void ensurePackages();
|
|
3247
3564
|
try { window.__dpcApplied = true; } catch { /* ignore */ }
|
|
3248
3565
|
}
|
|
3249
3566
|
|