dsh-panel-control 0.7.2 → 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 +19 -4
- package/client.js +867 -18
- package/docs/bridged-settings-pages.md +85 -0
- package/docs/renaming-panels.md +74 -0
- package/index.js +123 -2
- package/package.json +5 -2
- package/scripts/bridge-smoke.mjs +324 -0
- package/scripts/host-smoke.mjs +39 -3
- package/scripts/layout-smoke.mjs +27 -0
- package/scripts/names-smoke.mjs +425 -0
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.
|
|
@@ -265,6 +265,14 @@ function build(require) {
|
|
|
265
265
|
const SLOT_MAIN = 'main';
|
|
266
266
|
const SLOT_SETTINGS = 'settings.section';
|
|
267
267
|
const PANEL_KEY = 'panel-control';
|
|
268
|
+
/** Bridged pages get their own ids; the prefix also keeps them out of the host's reserved keys. */
|
|
269
|
+
const BRIDGE_PREFIX = 'dpc-bridge-';
|
|
270
|
+
const BRIDGE_SLOT_PREFIX = 'dpc-bridge-slot-';
|
|
271
|
+
/** Between the ordinary entries and this manager's own row (order 90). */
|
|
272
|
+
const BRIDGE_ORDER = 85;
|
|
273
|
+
const BRIDGE_DEBOUNCE_MS = 120;
|
|
274
|
+
/** Boot-time rescans: plugins register their settings page while the page starts. */
|
|
275
|
+
const BRIDGE_RETRY_MS = [350, 900, 2000, 4200, 8000, 15000];
|
|
268
276
|
const STORAGE_KEY = 'dsh.panel-control.v1';
|
|
269
277
|
/** Host route that answers "file these entries into groups" (see index.js). */
|
|
270
278
|
const RPC_CHANNEL = '/api';
|
|
@@ -340,9 +348,10 @@ function build(require) {
|
|
|
340
348
|
diagYes: { zh: '是', en: 'yes' },
|
|
341
349
|
diagNo: { zh: '否', en: 'no' },
|
|
342
350
|
hint: {
|
|
343
|
-
zh: '
|
|
344
|
-
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.',
|
|
345
353
|
},
|
|
354
|
+
renameIt: { zh: '修改显示名', en: 'Rename entry' },
|
|
346
355
|
dragHint: { zh: '拖动排序', en: 'Drag to reorder' },
|
|
347
356
|
locked: { zh: '固定显示', en: 'always shown' },
|
|
348
357
|
lockedHint: {
|
|
@@ -377,6 +386,21 @@ function build(require) {
|
|
|
377
386
|
layoutCollapseHint: { zh: '点分组标题收起该组,标题与数量仍保留。', en: 'Click a group title to fold it; the title and count stay.' },
|
|
378
387
|
layoutMax: { zh: '高度上限', en: 'Height cap' },
|
|
379
388
|
layoutMaxNone: { zh: '不限', en: 'No cap' },
|
|
389
|
+
layoutBridge: { zh: '补全设置页入口', en: 'Entries for settings pages' },
|
|
390
|
+
layoutBridgeHint: {
|
|
391
|
+
zh: '第三方插件只做了设置页、没做侧栏入口时,替它在侧栏补一个入口。关掉后这些入口全部移除,插件自己的设置页不受影响。',
|
|
392
|
+
en: 'When a third-party plugin ships a settings page but no sidebar entry, add one for it. Turning this off removes only those entries; the plugin keeps its settings page.',
|
|
393
|
+
},
|
|
394
|
+
bridgeBack: { zh: '返回对话', en: 'Back to conversation' },
|
|
395
|
+
bridgeTag: { zh: '设置页', en: 'Settings page' },
|
|
396
|
+
bridgeFrom: { zh: '来自 {p}', en: 'From {p}' },
|
|
397
|
+
bridgeMissing: { zh: '这个设置页已经不可用。', en: 'This settings page is no longer available.' },
|
|
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
|
+
},
|
|
380
404
|
layoutCollapseAll: { zh: '全部收起', en: 'Fold all' },
|
|
381
405
|
layoutExpandAll: { zh: '全部展开', en: 'Unfold all' },
|
|
382
406
|
layoutCollapseOne: { zh: '点此收起', en: 'click to fold' },
|
|
@@ -468,6 +492,9 @@ function build(require) {
|
|
|
468
492
|
collapsed: [],
|
|
469
493
|
guard: true,
|
|
470
494
|
collapse: true,
|
|
495
|
+
bridge: true,
|
|
496
|
+
names: {},
|
|
497
|
+
tags: true,
|
|
471
498
|
panelMax: 45,
|
|
472
499
|
});
|
|
473
500
|
|
|
@@ -479,7 +506,17 @@ function build(require) {
|
|
|
479
506
|
* would then build that many separators. The numbers here are far above any
|
|
480
507
|
* real sidebar and turn a hostile or corrupted store into a cheap no-op.
|
|
481
508
|
*/
|
|
482
|
-
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
|
+
}
|
|
483
520
|
|
|
484
521
|
/** Dedupe, drop anything that is not a usable id, and stop at `cap`. */
|
|
485
522
|
function uniqueIds(raw, cap) {
|
|
@@ -508,6 +545,9 @@ function build(require) {
|
|
|
508
545
|
// explicit false is the only way to turn the CSS off.
|
|
509
546
|
out.guard = raw.guard !== false;
|
|
510
547
|
out.collapse = raw.collapse !== false;
|
|
548
|
+
// Missing in files written before the bridge existed: those installs get the
|
|
549
|
+
// feature by default, exactly like a fresh one.
|
|
550
|
+
out.bridge = raw.bridge !== false;
|
|
511
551
|
const cap = Math.round(Number(raw.panelMax));
|
|
512
552
|
out.panelMax = Number.isFinite(cap) ? Math.min(70, Math.max(0, cap)) : 45;
|
|
513
553
|
if (Array.isArray(raw.groups)) {
|
|
@@ -531,6 +571,20 @@ function build(require) {
|
|
|
531
571
|
kept += 1;
|
|
532
572
|
}
|
|
533
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;
|
|
534
588
|
out.auto = raw.auto === true;
|
|
535
589
|
return out;
|
|
536
590
|
}
|
|
@@ -559,7 +613,15 @@ function build(require) {
|
|
|
559
613
|
(prefs.groups || []).length > 0 ||
|
|
560
614
|
(prefs.collapsed || []).length > 0 ||
|
|
561
615
|
(prefs.autoFiled || []).length > 0 ||
|
|
562
|
-
prefs.auto === true
|
|
616
|
+
prefs.auto === true ||
|
|
617
|
+
// A file that only turned a switch off is still a file the reader edited;
|
|
618
|
+
// without this, the next load treats it as empty and drops the edit.
|
|
619
|
+
prefs.guard === false ||
|
|
620
|
+
prefs.collapse === false ||
|
|
621
|
+
prefs.bridge === false ||
|
|
622
|
+
prefs.tags === false ||
|
|
623
|
+
Object.keys(prefs.names || {}).length > 0 ||
|
|
624
|
+
(typeof prefs.panelMax === 'number' && prefs.panelMax !== 45));
|
|
563
625
|
|
|
564
626
|
/** One call to the Host half, or null when this Client cannot reach it. */
|
|
565
627
|
async function hostCall(method, payload) {
|
|
@@ -870,6 +932,32 @@ function build(require) {
|
|
|
870
932
|
return fallback;
|
|
871
933
|
}
|
|
872
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
|
+
|
|
873
961
|
/**
|
|
874
962
|
* The sidebar's own list: every registered entry, sorted exactly the way the
|
|
875
963
|
* sidebar sorts it (`order`, then registration order).
|
|
@@ -900,6 +988,420 @@ function build(require) {
|
|
|
900
988
|
left.length === right.length &&
|
|
901
989
|
left.every((row, index) => row.id === right[index].id && row.label === right[index].label);
|
|
902
990
|
|
|
991
|
+
/* -------------------------------------------------------------- bridges */
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Settings pages with no panel of their own.
|
|
995
|
+
*
|
|
996
|
+
* A Client plugin addresses its settings page with `settings.section`; the
|
|
997
|
+
* sidebar only lists `sidebar.panellist` entries, and the two registries are
|
|
998
|
+
* unrelated. A plugin that never registers a panel therefore has a reachable
|
|
999
|
+
* settings page and no way to open it from the navigation rail. This block
|
|
1000
|
+
* gives each such page a panel of its own: a panellist row plus the `main`
|
|
1001
|
+
* entry it addresses, whose body is the host's own renderer for that section.
|
|
1002
|
+
*
|
|
1003
|
+
* A slot entry's `registrant` names the Cordis fiber that registered it, but
|
|
1004
|
+
* the client runner applies many plugins inside one fiber: in practice every
|
|
1005
|
+
* plugin except the market reports the same name, so a registrant match cannot
|
|
1006
|
+
* tell one plugin from another. Attribution is therefore by ownership of the
|
|
1007
|
+
* navigation row: a settings id whose base name (with a trailing `-settings`
|
|
1008
|
+
* removed) is an existing panel id, or whose label is an existing panel label,
|
|
1009
|
+
* belongs to a plugin that already has a way in. Core settings ids are listed
|
|
1010
|
+
* explicitly; they ship without a panel because that is the product decision,
|
|
1011
|
+
* not a gap.
|
|
1012
|
+
*/
|
|
1013
|
+
const CORE_SECTION_IDS = new Set(['account', 'general', 'models', 'agent-presets', 'plugins']);
|
|
1014
|
+
|
|
1015
|
+
const bridgeState = {
|
|
1016
|
+
entries: new Map(),
|
|
1017
|
+
labels: new Map(),
|
|
1018
|
+
timer: null,
|
|
1019
|
+
retries: new Set(),
|
|
1020
|
+
retriesStarted: false,
|
|
1021
|
+
syncing: false,
|
|
1022
|
+
scans: 0,
|
|
1023
|
+
unsub: null,
|
|
1024
|
+
};
|
|
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
|
+
|
|
1104
|
+
function registrantOf(entry) {
|
|
1105
|
+
const options = (entry && entry.options) || {};
|
|
1106
|
+
const value = (entry && entry.registrant) || options.registrant;
|
|
1107
|
+
return typeof value === 'string' ? value : '';
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function readSections(slots) {
|
|
1111
|
+
try {
|
|
1112
|
+
const raw = slots && typeof slots.entriesOfSlot === 'function' ? slots.entriesOfSlot(SLOT_SETTINGS) || [] : [];
|
|
1113
|
+
return raw
|
|
1114
|
+
.map((entry) => {
|
|
1115
|
+
const options = (entry && entry.options) || {};
|
|
1116
|
+
return {
|
|
1117
|
+
id: typeof options.id === 'string' ? options.id : '',
|
|
1118
|
+
label: options.label,
|
|
1119
|
+
registrant: registrantOf(entry),
|
|
1120
|
+
component: entry && entry.component,
|
|
1121
|
+
inject: options.inject,
|
|
1122
|
+
store: options.store,
|
|
1123
|
+
locale: options.locale,
|
|
1124
|
+
};
|
|
1125
|
+
})
|
|
1126
|
+
.filter((section) => section.id !== '');
|
|
1127
|
+
} catch {
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function panelEntries(slots) {
|
|
1133
|
+
try {
|
|
1134
|
+
return (slots && typeof slots.entriesOfSlot === 'function' ? slots.entriesOfSlot(SLOT_PANELS) || [] : [])
|
|
1135
|
+
.map((entry) => entry && entry.options)
|
|
1136
|
+
.filter((options) => options && typeof options.id === 'string' && options.id !== '')
|
|
1137
|
+
.map((options) => ({ id: options.id, label: resolveLabel(options.label, options.id) }));
|
|
1138
|
+
} catch {
|
|
1139
|
+
return [];
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function sectionBase(id) {
|
|
1144
|
+
return id.endsWith('-settings') ? id.slice(0, -'-settings'.length) : id;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Every settings page whose plugin owns no sidebar entry.
|
|
1149
|
+
*
|
|
1150
|
+
* A plugin may expose more than one settings page; each becomes its own row,
|
|
1151
|
+
* labelled with the plugin's own section label. A label already used by a real
|
|
1152
|
+
* panel is suffixed with the settings id so two rows never read the same.
|
|
1153
|
+
*/
|
|
1154
|
+
function bridgeCandidates(slots) {
|
|
1155
|
+
const rows = panelEntries(slots).filter((row) => !row.id.startsWith(BRIDGE_PREFIX));
|
|
1156
|
+
const panelIds = new Set(rows.map((row) => row.id));
|
|
1157
|
+
const panelLabels = new Set(rows.map((row) => row.label));
|
|
1158
|
+
// Real panel labels own their names; a bridged row must not steal one, and
|
|
1159
|
+
// must not collide with another bridged row either.
|
|
1160
|
+
const used = new Set(panelLabels);
|
|
1161
|
+
const out = [];
|
|
1162
|
+
const taken = new Set();
|
|
1163
|
+
for (const section of readSections(slots)) {
|
|
1164
|
+
if (CORE_SECTION_IDS.has(section.id)) continue;
|
|
1165
|
+
const base = sectionBase(section.id);
|
|
1166
|
+
let owned = panelIds.has(section.id) || panelIds.has(base);
|
|
1167
|
+
if (!owned) {
|
|
1168
|
+
for (const panelId of panelIds) {
|
|
1169
|
+
if (panelId.endsWith('-' + base)) {
|
|
1170
|
+
owned = true;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (owned) continue;
|
|
1176
|
+
const label = resolveLabel(section.label, section.id);
|
|
1177
|
+
if (panelLabels.has(label)) continue;
|
|
1178
|
+
if (typeof section.component !== 'function') continue;
|
|
1179
|
+
const bridgeId = BRIDGE_PREFIX + section.id;
|
|
1180
|
+
if (bridgeId.length > LIMITS.key || taken.has(bridgeId)) continue;
|
|
1181
|
+
taken.add(bridgeId);
|
|
1182
|
+
const chosen = used.has(label) ? `${label} · ${section.id}` : label;
|
|
1183
|
+
used.add(chosen);
|
|
1184
|
+
out.push({
|
|
1185
|
+
bridgeId,
|
|
1186
|
+
sectionId: section.id,
|
|
1187
|
+
label: chosen,
|
|
1188
|
+
component: section.component,
|
|
1189
|
+
inject: section.inject,
|
|
1190
|
+
store: section.store,
|
|
1191
|
+
locale: section.locale,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
return out;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** The layout's live selection, read through the public store when it exposes one. */
|
|
1198
|
+
function activePanelId() {
|
|
1199
|
+
try {
|
|
1200
|
+
const layout = optionalService(ctxRef, 'layout');
|
|
1201
|
+
const info = layout && layout.panelInfo;
|
|
1202
|
+
const snapshot = info && typeof info.getSnapshot === 'function' ? info.getSnapshot() : info;
|
|
1203
|
+
return snapshot && typeof snapshot.activePanelId === 'string' ? snapshot.activePanelId : null;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function disposeBridge(id, record) {
|
|
1210
|
+
bridgeState.entries.delete(id);
|
|
1211
|
+
for (const dispose of record.disposers.slice().reverse()) {
|
|
1212
|
+
try {
|
|
1213
|
+
dispose();
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
console.warn(`[${PACKAGE}] bridge cleanup failed for "${id}"`, error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
// The main key this row addressed is gone, so a reader sitting on it would
|
|
1219
|
+
// stare at an empty centre column. The Conversation is the only safe landing.
|
|
1220
|
+
if (activePanelId() === id) {
|
|
1221
|
+
try {
|
|
1222
|
+
const layout = optionalService(ctxRef, 'layout');
|
|
1223
|
+
if (layout && typeof layout.selectPanel === 'function') layout.selectPanel(null);
|
|
1224
|
+
} catch {
|
|
1225
|
+
/* nothing was selected after all */
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function syncBridges() {
|
|
1231
|
+
const slots = ctxRef && ctxRef.slots;
|
|
1232
|
+
if (!slots || typeof slots.register !== 'function' || bridgeState.syncing) return;
|
|
1233
|
+
bridgeState.syncing = true;
|
|
1234
|
+
bridgeState.scans += 1;
|
|
1235
|
+
let changed = false;
|
|
1236
|
+
try {
|
|
1237
|
+
const enabled = prefsNow().bridge !== false;
|
|
1238
|
+
watchSettings();
|
|
1239
|
+
const sections = readSections(slots);
|
|
1240
|
+
const panelIds = panelEntries(slots).map((row) => row.id);
|
|
1241
|
+
const candidates = enabled ? bridgeCandidates(slots) : [];
|
|
1242
|
+
const desired = new Set(candidates.map((candidate) => candidate.bridgeId));
|
|
1243
|
+
bridgeState.labels = new Map(candidates.map((candidate) => [candidate.bridgeId, candidate.label]));
|
|
1244
|
+
for (const [id, record] of Array.from(bridgeState.entries)) {
|
|
1245
|
+
if (desired.has(id)) continue;
|
|
1246
|
+
disposeBridge(id, record);
|
|
1247
|
+
changed = true;
|
|
1248
|
+
}
|
|
1249
|
+
for (const candidate of candidates) {
|
|
1250
|
+
if (bridgeState.entries.has(candidate.bridgeId)) continue;
|
|
1251
|
+
const slotName = BRIDGE_SLOT_PREFIX + candidate.sectionId;
|
|
1252
|
+
const record = { sectionId: candidate.sectionId, disposers: [] };
|
|
1253
|
+
try {
|
|
1254
|
+
record.disposers.push(
|
|
1255
|
+
slots.register(
|
|
1256
|
+
{
|
|
1257
|
+
name: SLOT_MAIN,
|
|
1258
|
+
key: candidate.bridgeId,
|
|
1259
|
+
locale: NS,
|
|
1260
|
+
children: { [slotName]: { kind: 'single', scope: 'root' } },
|
|
1261
|
+
},
|
|
1262
|
+
makeBridgePanel(candidate, slotName),
|
|
1263
|
+
),
|
|
1264
|
+
);
|
|
1265
|
+
// The settings shell owns the `settings.section` declaration, so its
|
|
1266
|
+
// props-face renderer is not ours to call. Declaring one child slot of
|
|
1267
|
+
// our own and re-registering the same component into it gives the
|
|
1268
|
+
// section the host kit it expects (inject, locale, store, boundary)
|
|
1269
|
+
// without touching the shell or the foreign registration.
|
|
1270
|
+
record.disposers.push(
|
|
1271
|
+
slots.register(
|
|
1272
|
+
{
|
|
1273
|
+
name: slotName,
|
|
1274
|
+
inject: candidate.inject,
|
|
1275
|
+
store: candidate.store,
|
|
1276
|
+
locale: candidate.locale,
|
|
1277
|
+
},
|
|
1278
|
+
candidate.component,
|
|
1279
|
+
),
|
|
1280
|
+
);
|
|
1281
|
+
record.disposers.push(
|
|
1282
|
+
slots.register(
|
|
1283
|
+
{
|
|
1284
|
+
name: SLOT_PANELS,
|
|
1285
|
+
id: candidate.bridgeId,
|
|
1286
|
+
order: BRIDGE_ORDER,
|
|
1287
|
+
locale: NS,
|
|
1288
|
+
label: () => bridgeState.labels.get(candidate.bridgeId) || candidate.label,
|
|
1289
|
+
},
|
|
1290
|
+
BridgeIcon,
|
|
1291
|
+
),
|
|
1292
|
+
);
|
|
1293
|
+
bridgeState.entries.set(candidate.bridgeId, record);
|
|
1294
|
+
changed = true;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
for (const dispose of record.disposers.slice().reverse()) {
|
|
1297
|
+
try {
|
|
1298
|
+
dispose();
|
|
1299
|
+
} catch {
|
|
1300
|
+
/* the pair is already half gone */
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
console.warn(`[${PACKAGE}] could not add a panel for "${candidate.sectionId}"`, error);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Early scans run while plugins are still registering. Keeping a trace of
|
|
1307
|
+
// exactly what the slot returned tells a later reader whether a missing
|
|
1308
|
+
// row is a timing problem or an ownership guess that did not hold.
|
|
1309
|
+
if (bridgeState.scans <= 8 || changed) {
|
|
1310
|
+
reportHost({
|
|
1311
|
+
kind: 'bridge-scan',
|
|
1312
|
+
enabled,
|
|
1313
|
+
scans: bridgeState.scans,
|
|
1314
|
+
sections: sections.map((section) => `${section.id}:${section.registrant || '?'}`).join('|'),
|
|
1315
|
+
panels: panelIds.join('|'),
|
|
1316
|
+
ids: Array.from(bridgeState.entries.keys()).join(','),
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (changed) {
|
|
1320
|
+
reportHost({
|
|
1321
|
+
kind: 'bridge',
|
|
1322
|
+
enabled,
|
|
1323
|
+
ids: Array.from(bridgeState.entries.keys()).join(','),
|
|
1324
|
+
sections: candidates.length,
|
|
1325
|
+
});
|
|
1326
|
+
// The diagnostics line in the settings card reads this count; without a
|
|
1327
|
+
// re-render it would keep showing the value from before the sync.
|
|
1328
|
+
emit();
|
|
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();
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
console.warn(`[${PACKAGE}] bridge sync failed`, error);
|
|
1335
|
+
} finally {
|
|
1336
|
+
bridgeState.syncing = false;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function scheduleBridgeSync() {
|
|
1341
|
+
if (bridgeState.timer) clearTimeout(bridgeState.timer);
|
|
1342
|
+
bridgeState.timer = setTimeout(() => {
|
|
1343
|
+
bridgeState.timer = null;
|
|
1344
|
+
syncBridges();
|
|
1345
|
+
}, BRIDGE_DEBOUNCE_MS);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Watch the section registry.
|
|
1350
|
+
*
|
|
1351
|
+
* `subscribe` is trustworthy only once the slot is declared. The plugin runs
|
|
1352
|
+
* before some of the plugins it bridges, so an early subscription can resolve
|
|
1353
|
+
* to nothing and never be retried; the declaration callback calls this again.
|
|
1354
|
+
*/
|
|
1355
|
+
function watchSettings() {
|
|
1356
|
+
if (bridgeState.unsub) return;
|
|
1357
|
+
const slots = ctxRef && ctxRef.slots;
|
|
1358
|
+
if (!slots || typeof slots.subscribe !== 'function') return;
|
|
1359
|
+
try {
|
|
1360
|
+
const dispose = slots.subscribe(SLOT_SETTINGS, () => scheduleBridgeSync());
|
|
1361
|
+
bridgeState.unsub = typeof dispose === 'function' ? dispose : () => {};
|
|
1362
|
+
} catch {
|
|
1363
|
+
/* not declared yet; the next scan or declaration retries */
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function unwatchSettings() {
|
|
1368
|
+
if (!bridgeState.unsub) return;
|
|
1369
|
+
try {
|
|
1370
|
+
bridgeState.unsub();
|
|
1371
|
+
} catch {
|
|
1372
|
+
/* the slot is already gone */
|
|
1373
|
+
}
|
|
1374
|
+
bridgeState.unsub = null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function scheduleBridgeRetry(delay) {
|
|
1378
|
+
const id = setTimeout(() => {
|
|
1379
|
+
bridgeState.retries.delete(id);
|
|
1380
|
+
if (ctxRef) syncBridges();
|
|
1381
|
+
}, delay);
|
|
1382
|
+
// Browsers return a number; Node returns an object. Letting the harness
|
|
1383
|
+
// exit without waiting out the last boot scan keeps `npm run check` quick.
|
|
1384
|
+
if (id && typeof id.unref === 'function') id.unref();
|
|
1385
|
+
bridgeState.retries.add(id);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function scheduleBridgeRetries() {
|
|
1389
|
+
if (bridgeState.retriesStarted) return;
|
|
1390
|
+
bridgeState.retriesStarted = true;
|
|
1391
|
+
for (const delay of BRIDGE_RETRY_MS) scheduleBridgeRetry(delay);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function disposeBridges() {
|
|
1395
|
+
if (bridgeState.timer) {
|
|
1396
|
+
clearTimeout(bridgeState.timer);
|
|
1397
|
+
bridgeState.timer = null;
|
|
1398
|
+
}
|
|
1399
|
+
for (const id of Array.from(bridgeState.retries)) clearTimeout(id);
|
|
1400
|
+
bridgeState.retries.clear();
|
|
1401
|
+
unwatchSettings();
|
|
1402
|
+
for (const [id, record] of Array.from(bridgeState.entries)) disposeBridge(id, record);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
903
1405
|
/** The stored order, extended with every entry that is new since it was saved. */
|
|
904
1406
|
function normalize(prefs, entries) {
|
|
905
1407
|
const known = new Set(entries.map((entry) => entry.id));
|
|
@@ -955,7 +1457,9 @@ function build(require) {
|
|
|
955
1457
|
const used = new Set();
|
|
956
1458
|
const byLabel = new Map();
|
|
957
1459
|
for (const button of buttons) {
|
|
958
|
-
|
|
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') || '';
|
|
959
1463
|
if (!byLabel.has(label)) byLabel.set(label, []);
|
|
960
1464
|
byLabel.get(label).push(button);
|
|
961
1465
|
}
|
|
@@ -974,7 +1478,7 @@ function build(require) {
|
|
|
974
1478
|
// Labels that agree prove the two lists are the same list; a button with no
|
|
975
1479
|
// label at all is no evidence either way, so it may be filled positionally.
|
|
976
1480
|
const sameOrder = loose.every((entry, index) => {
|
|
977
|
-
const label = rest[index].getAttribute('aria-label') || '';
|
|
1481
|
+
const label = rest[index].getAttribute('data-dpc-label') || rest[index].getAttribute('aria-label') || '';
|
|
978
1482
|
return label === '' || label === entry.label;
|
|
979
1483
|
});
|
|
980
1484
|
// A build that labels rows in wording this plugin never sees ("IM 机器人"
|
|
@@ -1086,7 +1590,10 @@ function build(require) {
|
|
|
1086
1590
|
if (!entry.label) continue;
|
|
1087
1591
|
let button = null;
|
|
1088
1592
|
try {
|
|
1089
|
-
|
|
1593
|
+
const label = cssEscape(entry.label);
|
|
1594
|
+
button = document.querySelector(
|
|
1595
|
+
`button[data-dpc-label="${label}"], button[aria-label="${label}"]`,
|
|
1596
|
+
);
|
|
1090
1597
|
} catch {
|
|
1091
1598
|
button = null;
|
|
1092
1599
|
}
|
|
@@ -1234,6 +1741,74 @@ function build(require) {
|
|
|
1234
1741
|
}
|
|
1235
1742
|
};
|
|
1236
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
|
+
|
|
1237
1812
|
const apply = () => {
|
|
1238
1813
|
if (destroyed) return;
|
|
1239
1814
|
const plan = readPlan();
|
|
@@ -1262,10 +1837,21 @@ function build(require) {
|
|
|
1262
1837
|
const folded = folding ? new Set(plan.prefs.collapsed || []) : new Set();
|
|
1263
1838
|
diag.folded = Array.from(folded).join(',');
|
|
1264
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
|
+
|
|
1265
1849
|
const nextSignature = JSON.stringify({
|
|
1266
1850
|
blocks: plan.blocks,
|
|
1267
1851
|
hidden: plan.hidden ? Array.from(plan.hidden).sort() : [],
|
|
1268
1852
|
entries: plan.entries.map((entry) => entry.id + ':' + entry.label),
|
|
1853
|
+
names: plan.prefs.names || {},
|
|
1854
|
+
tags: plan.prefs.tags !== false,
|
|
1269
1855
|
wide,
|
|
1270
1856
|
folded: Array.from(folded),
|
|
1271
1857
|
});
|
|
@@ -1279,10 +1865,8 @@ function build(require) {
|
|
|
1279
1865
|
|
|
1280
1866
|
pause();
|
|
1281
1867
|
try {
|
|
1282
|
-
const buttons = Array.from(list.querySelectorAll(':scope > button'));
|
|
1283
1868
|
for (const node of Array.from(list.querySelectorAll('[data-dpc-sep]'))) node.remove();
|
|
1284
1869
|
|
|
1285
|
-
const map = mapButtons(plan.entries, buttons);
|
|
1286
1870
|
const managed = new Set(map.values());
|
|
1287
1871
|
|
|
1288
1872
|
// Anything we do not manage (the "More" launcher and its wrapper) keeps
|
|
@@ -1420,6 +2004,18 @@ function build(require) {
|
|
|
1420
2004
|
node.style.removeProperty('display');
|
|
1421
2005
|
}
|
|
1422
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
|
+
}
|
|
1423
2019
|
for (const node of Array.from(document.querySelectorAll('[data-dpc-list]'))) {
|
|
1424
2020
|
node.removeAttribute('data-dpc-list');
|
|
1425
2021
|
node.removeAttribute('data-dpc-unbounded');
|
|
@@ -1447,6 +2043,9 @@ function build(require) {
|
|
|
1447
2043
|
scheduleSaveToHost();
|
|
1448
2044
|
emit();
|
|
1449
2045
|
if (engineRef) engineRef.schedule();
|
|
2046
|
+
// The bridge switch lives here too; toggling it must add or drop the rows
|
|
2047
|
+
// right away rather than on the next unrelated registration.
|
|
2048
|
+
scheduleBridgeSync();
|
|
1450
2049
|
}
|
|
1451
2050
|
|
|
1452
2051
|
function setDiag(patch) {
|
|
@@ -1477,6 +2076,8 @@ function build(require) {
|
|
|
1477
2076
|
if (!stored || !hasContent(stored)) await hostCall('save', { prefs: chosen });
|
|
1478
2077
|
}
|
|
1479
2078
|
setDiag({ prefsSource: prefsSource.value, hostFile });
|
|
2079
|
+
// The Host copy may carry the bridge switch with it.
|
|
2080
|
+
scheduleBridgeSync();
|
|
1480
2081
|
}
|
|
1481
2082
|
|
|
1482
2083
|
const prefsNow = () => store.state.prefs;
|
|
@@ -1502,7 +2103,9 @@ function build(require) {
|
|
|
1502
2103
|
}
|
|
1503
2104
|
|
|
1504
2105
|
function sortByName() {
|
|
1505
|
-
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)]));
|
|
1506
2109
|
const locale = activeLang() === 'zh' ? 'zh-Hans-CN' : 'en';
|
|
1507
2110
|
const order = orderNow()
|
|
1508
2111
|
.slice()
|
|
@@ -1747,6 +2350,8 @@ function build(require) {
|
|
|
1747
2350
|
plus: 'M12 6v12M6 12h12',
|
|
1748
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',
|
|
1749
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',
|
|
2354
|
+
back: 'M14.5 6.5 9 12l5.5 5.5',
|
|
1750
2355
|
};
|
|
1751
2356
|
|
|
1752
2357
|
/** One 16px line glyph, one 1.5 stroke, colours inherited from the text role. */
|
|
@@ -1807,6 +2412,105 @@ function build(require) {
|
|
|
1807
2412
|
|
|
1808
2413
|
const PanelIcon = SlidersIcon;
|
|
1809
2414
|
|
|
2415
|
+
/**
|
|
2416
|
+
* A page glyph with text lines: the settings page, not the plugin's own icon
|
|
2417
|
+
* (that one belongs to the plugin and is not available to the bridge).
|
|
2418
|
+
*/
|
|
2419
|
+
function BridgeIcon(props) {
|
|
2420
|
+
const size = props && typeof props.size === 'number' ? props.size : 18;
|
|
2421
|
+
const active = Boolean(props && props.active);
|
|
2422
|
+
return h(
|
|
2423
|
+
'svg',
|
|
2424
|
+
{
|
|
2425
|
+
width: size,
|
|
2426
|
+
height: size,
|
|
2427
|
+
viewBox: '0 0 24 24',
|
|
2428
|
+
fill: 'none',
|
|
2429
|
+
'aria-hidden': true,
|
|
2430
|
+
style: { display: 'block', opacity: active ? 1 : 0.78 },
|
|
2431
|
+
},
|
|
2432
|
+
h('rect', {
|
|
2433
|
+
x: 3.5,
|
|
2434
|
+
y: 4.5,
|
|
2435
|
+
width: 17,
|
|
2436
|
+
height: 15,
|
|
2437
|
+
rx: 2.5,
|
|
2438
|
+
stroke: 'currentColor',
|
|
2439
|
+
strokeWidth: 1.6,
|
|
2440
|
+
}),
|
|
2441
|
+
h('path', {
|
|
2442
|
+
d: 'M8 9.5h8M8 12h5M8 14.5h8',
|
|
2443
|
+
stroke: 'currentColor',
|
|
2444
|
+
strokeWidth: 1.5,
|
|
2445
|
+
strokeLinecap: 'round',
|
|
2446
|
+
}),
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* One bridged page: a back bar, then the host's renderer for the section.
|
|
2452
|
+
*
|
|
2453
|
+
* The settings shell owns the `settings.section` declaration, so the ctx-level
|
|
2454
|
+
* `slots.renderSlot` cannot reach it: that entry point only renders `root`, and
|
|
2455
|
+
* a child slot is renderable only through the props face of the entry that
|
|
2456
|
+
* declared it. Each bridge therefore declares its own single-child slot and
|
|
2457
|
+
* re-registers the section component into it (see `syncBridges`), and renders
|
|
2458
|
+
* that slot through the `renderSlot` prop the host hands this component. The
|
|
2459
|
+
* host still builds the section's kit — inject, locale, store, boundary and
|
|
2460
|
+
* hooks all stay its own. The owner prop is the same `close` the settings shell
|
|
2461
|
+
* passes, wired to the Conversation because this page has no dialog to close.
|
|
2462
|
+
*/
|
|
2463
|
+
function makeBridgePanel(candidate, slotName) {
|
|
2464
|
+
const back = () => {
|
|
2465
|
+
try {
|
|
2466
|
+
const layout = optionalService(ctxRef, 'layout');
|
|
2467
|
+
if (layout && typeof layout.selectPanel === 'function') layout.selectPanel(null);
|
|
2468
|
+
} catch {
|
|
2469
|
+
/* the Conversation is already showing */
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
return function BridgePanel(props) {
|
|
2473
|
+
const renderSlot = props && typeof props.renderSlot === 'function' ? props.renderSlot : null;
|
|
2474
|
+
let node = null;
|
|
2475
|
+
try {
|
|
2476
|
+
if (renderSlot) node = renderSlot(slotName, { close: back }, {});
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
console.warn(`[${PACKAGE}] could not render settings page "${candidate.sectionId}"`, error);
|
|
2479
|
+
}
|
|
2480
|
+
return h(
|
|
2481
|
+
'div',
|
|
2482
|
+
{ className: 'dpc-root dpc-bridge' },
|
|
2483
|
+
h(
|
|
2484
|
+
'div',
|
|
2485
|
+
{ className: 'dpc-page dpc-bridgePage' },
|
|
2486
|
+
h(
|
|
2487
|
+
'div',
|
|
2488
|
+
{ className: 'dpc-bridgeBar' },
|
|
2489
|
+
h(
|
|
2490
|
+
'button',
|
|
2491
|
+
{ type: 'button', className: 'dpc-link dpc-bridgeBack', onClick: back },
|
|
2492
|
+
h(Icon, { name: 'back' }),
|
|
2493
|
+
h('span', null, t('bridgeBack')),
|
|
2494
|
+
),
|
|
2495
|
+
h(
|
|
2496
|
+
'span',
|
|
2497
|
+
{ className: 'dpc-bridgeTags' },
|
|
2498
|
+
h('span', { className: 'dpc-bridgeTag' }, t('bridgeTag')),
|
|
2499
|
+
h('span', { className: 'dpc-bridgeFrom' }, fill(t('bridgeFrom'), { p: candidate.sectionId })),
|
|
2500
|
+
),
|
|
2501
|
+
),
|
|
2502
|
+
node
|
|
2503
|
+
? h('div', { className: 'dpc-bridgeBody' }, node)
|
|
2504
|
+
: h(
|
|
2505
|
+
'section',
|
|
2506
|
+
{ className: 'dpc-card dpc-empty' },
|
|
2507
|
+
h('p', { className: 'dpc-emptyTitle' }, t('bridgeMissing')),
|
|
2508
|
+
),
|
|
2509
|
+
),
|
|
2510
|
+
);
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
|
|
1810
2514
|
/** The entry's own glyph, borrowed from the slot it registered. */
|
|
1811
2515
|
function Glyph(props) {
|
|
1812
2516
|
const [node, setNode] = React.useState(null);
|
|
@@ -1907,6 +2611,9 @@ function build(require) {
|
|
|
1907
2611
|
// check runs again on focus, and the answer must not pop back up.
|
|
1908
2612
|
const [, forceRender] = React.useReducer((n) => n + 1, 0);
|
|
1909
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);
|
|
1910
2617
|
/**
|
|
1911
2618
|
* Dragging works the same for a row and for a section: the handle starts it,
|
|
1912
2619
|
* the target highlights, dropping commits "move before this one". Only the
|
|
@@ -1970,12 +2677,44 @@ function build(require) {
|
|
|
1970
2677
|
}, [prefs.auto, prefs.autoFiled, entries.length, state.autoState && state.autoState.status]);
|
|
1971
2678
|
|
|
1972
2679
|
const order = normalize(prefs, entries);
|
|
1973
|
-
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)]));
|
|
1974
2682
|
const hidden = new Set(prefs.hidden || []);
|
|
1975
2683
|
const groupsOf = prefs.groups || [];
|
|
1976
2684
|
const autoOn = prefs.auto === true;
|
|
1977
2685
|
const autoState = state.autoState || { status: 'idle', message: '', groups: 0 };
|
|
1978
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
|
+
|
|
1979
2718
|
const head = h(
|
|
1980
2719
|
'header',
|
|
1981
2720
|
{ className: inSection ? 'dpc-head dpc-head--section' : 'dpc-head' },
|
|
@@ -2133,6 +2872,7 @@ function build(require) {
|
|
|
2133
2872
|
|
|
2134
2873
|
const rows = order.map((id, position) => {
|
|
2135
2874
|
const label = labelOf.get(id) || id;
|
|
2875
|
+
const rawLabel = rawOf.get(id) || id;
|
|
2136
2876
|
const isHidden = hidden.has(id);
|
|
2137
2877
|
const locked = id === PANEL_KEY;
|
|
2138
2878
|
return h(
|
|
@@ -2150,8 +2890,32 @@ function build(require) {
|
|
|
2150
2890
|
h(
|
|
2151
2891
|
'span',
|
|
2152
2892
|
{ className: 'dpc-rowText' },
|
|
2153
|
-
|
|
2154
|
-
|
|
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),
|
|
2155
2919
|
),
|
|
2156
2920
|
h(
|
|
2157
2921
|
'span',
|
|
@@ -2169,6 +2933,9 @@ function build(require) {
|
|
|
2169
2933
|
ungrouped: t('ungrouped'),
|
|
2170
2934
|
onPick: (next) => setGroup(id, next),
|
|
2171
2935
|
}),
|
|
2936
|
+
iconButton('edit', t('renameIt'), () => startRename(id), false, {
|
|
2937
|
+
'aria-pressed': editing === id ? 'true' : 'false',
|
|
2938
|
+
}),
|
|
2172
2939
|
iconButton(
|
|
2173
2940
|
isHidden ? 'eyeOff' : 'eye',
|
|
2174
2941
|
locked ? t('lockedHint') : isHidden ? t('showIt') : t('hideIt'),
|
|
@@ -2209,6 +2976,8 @@ function build(require) {
|
|
|
2209
2976
|
// is the only place the guard, the cap and the fold switch are explained.
|
|
2210
2977
|
const layoutOn = prefs.guard !== false;
|
|
2211
2978
|
const foldingOn = prefs.collapse !== false;
|
|
2979
|
+
const bridgeOn = prefs.bridge !== false;
|
|
2980
|
+
const tagsOn = prefs.tags !== false;
|
|
2212
2981
|
const capNow = typeof prefs.panelMax === 'number' ? prefs.panelMax : 45;
|
|
2213
2982
|
const setLayout = (patch) => commit({ ...prefsNow(), ...patch });
|
|
2214
2983
|
const layoutSwitch = (on, label, onClick) =>
|
|
@@ -2251,6 +3020,19 @@ function build(require) {
|
|
|
2251
3020
|
t('layoutCollapseHint'),
|
|
2252
3021
|
layoutSwitch(foldingOn, t('layoutCollapse'), () => setLayout({ collapse: !foldingOn })),
|
|
2253
3022
|
),
|
|
3023
|
+
// The switch is the only way back for a reader who does not want the
|
|
3024
|
+
// extra rows: turning it off also disposes the registrations, not just
|
|
3025
|
+
// the display, so the host list and this page agree.
|
|
3026
|
+
layoutRow(
|
|
3027
|
+
t('layoutBridge'),
|
|
3028
|
+
t('layoutBridgeHint'),
|
|
3029
|
+
layoutSwitch(bridgeOn, t('layoutBridge'), () => setLayout({ bridge: !bridgeOn })),
|
|
3030
|
+
),
|
|
3031
|
+
layoutRow(
|
|
3032
|
+
t('layoutTags'),
|
|
3033
|
+
t('layoutTagsHint'),
|
|
3034
|
+
layoutSwitch(tagsOn, t('layoutTags'), () => setLayout({ tags: !tagsOn })),
|
|
3035
|
+
),
|
|
2254
3036
|
layoutRow(
|
|
2255
3037
|
t('layoutMax'),
|
|
2256
3038
|
null,
|
|
@@ -2350,6 +3132,7 @@ function build(require) {
|
|
|
2350
3132
|
state.inventory && state.inventory.repeated ? t('diagYes') : t('diagNo'),
|
|
2351
3133
|
true,
|
|
2352
3134
|
),
|
|
3135
|
+
diagRow(t('diagBridge'), String(bridgeState.entries.size), true),
|
|
2353
3136
|
diagnostics && diagnostics.error ? h('dt', { key: 'et' }, 'error') : null,
|
|
2354
3137
|
diagnostics && diagnostics.error
|
|
2355
3138
|
? h('dd', { key: 'ed', className: 'dpc-error' }, diagnostics.error)
|
|
@@ -2452,6 +3235,7 @@ function build(require) {
|
|
|
2452
3235
|
.dpc-glyph:empty::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.4}
|
|
2453
3236
|
.dpc-rowText{display:flex;flex:1 1 auto;flex-direction:row;align-items:baseline;gap:8px;min-width:0}
|
|
2454
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}
|
|
2455
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}
|
|
2456
3240
|
.dpc-tags{display:flex;flex:none;align-items:center;gap:8px}
|
|
2457
3241
|
.dpc-tag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));white-space:nowrap}
|
|
@@ -2466,7 +3250,9 @@ function build(require) {
|
|
|
2466
3250
|
.dpc-pickerText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2467
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)}
|
|
2468
3252
|
.dpc-picker[data-open="true"] .dpc-caret{margin-top:2px;transform:rotate(225deg)}
|
|
2469
|
-
|
|
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)}
|
|
2470
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}
|
|
2471
3257
|
.dpc-option:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18));color:var(--dsw-alias-label-primary,#8a8a8a)}
|
|
2472
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)}
|
|
@@ -2525,6 +3311,16 @@ function build(require) {
|
|
|
2525
3311
|
.dpc-segmentBtn--on{background:var(--dsw-alias-settings-card-fill,rgba(127,127,127,.22));color:var(--dsw-alias-label-primary,#8a8a8a)}
|
|
2526
3312
|
.dpc-segmentBtn:disabled{opacity:.4;cursor:default}
|
|
2527
3313
|
|
|
3314
|
+
/* A bridged settings page: its own bar, then the host-rendered section. */
|
|
3315
|
+
.dpc-bridgePage{gap:16px}
|
|
3316
|
+
.dpc-bridgeBar{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:40px;padding-bottom:8px;border-bottom:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.14))}
|
|
3317
|
+
.dpc-bridgeBack{display:inline-flex;align-items:center;gap:6px;margin-left:-8px}
|
|
3318
|
+
.dpc-bridgeBack .dpc-ico{width:14px;height:14px}
|
|
3319
|
+
.dpc-bridgeTags{display:flex;align-items:center;gap:8px;min-width:0}
|
|
3320
|
+
.dpc-bridgeTag{flex:none;padding:2px 8px;border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.24));border-radius:999px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
|
|
3321
|
+
.dpc-bridgeFrom{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--dsw-font-family-code,ui-monospace,monospace);font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8))}
|
|
3322
|
+
.dpc-bridgeBody{min-width:0}
|
|
3323
|
+
|
|
2528
3324
|
@media (max-width:1000px){
|
|
2529
3325
|
/* Below the column width the frame becomes the gutter instead of pushing
|
|
2530
3326
|
content past the window edge. */
|
|
@@ -2534,6 +3330,7 @@ function build(require) {
|
|
|
2534
3330
|
.dpc-row{flex-wrap:wrap;gap:8px}
|
|
2535
3331
|
.dpc-rowText{flex:1 1 120px}
|
|
2536
3332
|
.dpc-tags{order:8}
|
|
3333
|
+
.dpc-bridgeFrom{display:none}
|
|
2537
3334
|
}
|
|
2538
3335
|
/* The settings column is narrower than the page column: drop what is only
|
|
2539
3336
|
nice-to-have there instead of letting the row overflow. */
|
|
@@ -2558,6 +3355,10 @@ function build(require) {
|
|
|
2558
3355
|
[data-dpc-list]>button{flex:none}
|
|
2559
3356
|
[data-dpc-region]{flex:1 1 auto;min-height:min(120px,18vh)}
|
|
2560
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
|
+
|
|
2561
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}
|
|
2562
3363
|
.dpc-sep:hover{color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
|
|
2563
3364
|
.dpc-sep:focus-visible{outline:2px solid var(--dsw-focus-ring-color,rgba(127,127,127,.65));outline-offset:1px}
|
|
@@ -2672,10 +3473,24 @@ function build(require) {
|
|
|
2672
3473
|
// A registration, a rename or a locale switch changes the list this plugin
|
|
2673
3474
|
// lays out, so every one of them re-runs the pass.
|
|
2674
3475
|
if (typeof ctx.slots.subscribe === 'function') {
|
|
2675
|
-
ctx.effect(
|
|
3476
|
+
ctx.effect(
|
|
3477
|
+
() =>
|
|
3478
|
+
ctx.slots.subscribe(SLOT_PANELS, () => {
|
|
3479
|
+
if (engineRef) engineRef.schedule();
|
|
3480
|
+
scheduleBridgeSync();
|
|
3481
|
+
}),
|
|
3482
|
+
`${PACKAGE}: panel entries`,
|
|
3483
|
+
);
|
|
2676
3484
|
}
|
|
2677
3485
|
if (locale && typeof locale.subscribe === 'function') {
|
|
2678
|
-
ctx.effect(
|
|
3486
|
+
ctx.effect(
|
|
3487
|
+
() =>
|
|
3488
|
+
locale.subscribe(() => {
|
|
3489
|
+
if (engineRef) engineRef.schedule();
|
|
3490
|
+
scheduleBridgeSync();
|
|
3491
|
+
}),
|
|
3492
|
+
`${PACKAGE}: labels`,
|
|
3493
|
+
);
|
|
2679
3494
|
}
|
|
2680
3495
|
|
|
2681
3496
|
ctx.slots.inject(SLOT_MAIN, () => {
|
|
@@ -2714,6 +3529,38 @@ function build(require) {
|
|
|
2714
3529
|
return undefined;
|
|
2715
3530
|
}
|
|
2716
3531
|
});
|
|
3532
|
+
|
|
3533
|
+
// A bridged page follows three registries: the settings sections it
|
|
3534
|
+
// mirrors, the panels it must not duplicate, and the locale that names
|
|
3535
|
+
// both. Each declaration is its own readiness signal, so the sync runs
|
|
3536
|
+
// once the slots exist and again on every later change.
|
|
3537
|
+
const kickBridge = () => {
|
|
3538
|
+
try {
|
|
3539
|
+
watchSettings();
|
|
3540
|
+
scheduleBridgeSync();
|
|
3541
|
+
scheduleBridgeRetries();
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
console.warn(`[${PACKAGE}] bridge scheduling failed`, error);
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
try {
|
|
3547
|
+
// Re-install the watch on every declaration: before the slot exists
|
|
3548
|
+
// `subscribe` cannot observe the section registry, and a stale
|
|
3549
|
+
// subscription would miss the pages that register after us.
|
|
3550
|
+
ctx.slots.inject(SLOT_SETTINGS, () => {
|
|
3551
|
+
unwatchSettings();
|
|
3552
|
+
watchSettings();
|
|
3553
|
+
kickBridge();
|
|
3554
|
+
});
|
|
3555
|
+
ctx.slots.inject(SLOT_PANELS, kickBridge);
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
console.warn(`[${PACKAGE}] bridge slots unavailable`, error);
|
|
3558
|
+
}
|
|
3559
|
+
ctx.effect(() => () => disposeBridges(), `${PACKAGE}: bridge cleanup`);
|
|
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();
|
|
2717
3564
|
try { window.__dpcApplied = true; } catch { /* ignore */ }
|
|
2718
3565
|
}
|
|
2719
3566
|
|
|
@@ -2736,7 +3583,9 @@ function build(require) {
|
|
|
2736
3583
|
}
|
|
2737
3584
|
|
|
2738
3585
|
return {
|
|
2739
|
-
|
|
3586
|
+
// `layout` is only read (never patched): the bridge's back button and its
|
|
3587
|
+
// selection fallback call the public selectPanel action.
|
|
3588
|
+
inject: ['slots', 'layout'],
|
|
2740
3589
|
// Pure helpers the browser harness drives directly, without a Cordis context.
|
|
2741
3590
|
testables: { createEngine, mapButtons, readEntries, normalize, sanitize, store, reorderList, applyInner, setCtx: (value) => { ctxRef = value; } },
|
|
2742
3591
|
apply(ctx) {
|