@phoenix-wing/code-core 0.6.4 → 0.6.6

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.
@@ -0,0 +1,635 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ export const PNW_CLEANUP_DIALOG_TAG = "pnw-cleanup-dialog";
3
+ export const PNW_CLEANUP_DIALOG_ACTION = "pnw-cleanup-dialog-action";
4
+ const PNW_EMPTY_CLEANUP_DIALOG_MODEL = Object.freeze({
5
+ title: "清理",
6
+ modes: Object.freeze([]),
7
+ modePresentation: "select",
8
+ targets: Object.freeze([]),
9
+ rulesVisible: false,
10
+ rulesLabel: "清理规则",
11
+ rulesYaml: "",
12
+ preview: Object.freeze({ state: "idle", items: Object.freeze([]) }),
13
+ previewEnabled: false,
14
+ executeEnabled: false,
15
+ previewLabel: "预览",
16
+ executeLabel: "清理",
17
+ cancelLabel: "取消",
18
+ highRiskConfirmationLabel: "我已核对预览,确认执行不可撤销的清理。",
19
+ requireHighRiskConfirmation: true,
20
+ });
21
+ const PNW_CLEANUP_DIALOG_STYLE = `
22
+ :host { color: var(--vscode-foreground, #1f1f1f); font: var(--vscode-font-size, 13px)/1.4 var(--vscode-font-family, system-ui, sans-serif); }
23
+ * { box-sizing: border-box; }
24
+ button, select, textarea, input { font: inherit; }
25
+ .pnw-cleanup-dialog {
26
+ /* Anchor the header to the viewport; mode/section changes grow downward only. */
27
+ position: fixed; top: 16px; bottom: auto; margin: 0 auto;
28
+ width: min(680px, calc(100vw - 32px)); max-width: 100%; max-height: min(760px, calc(100vh - 32px)); padding: 0;
29
+ overflow: hidden; border: 1px solid var(--vscode-widget-border, #8e8e8e); color: inherit;
30
+ background: var(--vscode-editorWidget-background, var(--vscode-editor-background, #fff)); box-shadow: 0 8px 30px var(--vscode-widget-shadow, rgba(0,0,0,.35));
31
+ }
32
+ .pnw-cleanup-dialog::backdrop { background: rgba(0,0,0,.38); }
33
+ .pnw-cleanup-shell { display: grid; max-height: inherit; grid-template-rows: auto minmax(0, 1fr) auto; }
34
+ .pnw-cleanup-header, .pnw-cleanup-footer { display: flex; align-items: center; gap: 8px; padding: 9px 12px; }
35
+ .pnw-cleanup-header { border-bottom: 1px solid var(--vscode-panel-border, #d4d4d4); }
36
+ .pnw-cleanup-title { flex: 1; min-width: 0; margin: 0; font-size: 14px; font-weight: 600; }
37
+ .pnw-cleanup-close { width: 26px; height: 26px; padding: 0; border: 0; color: inherit; background: transparent; cursor: pointer; }
38
+ .pnw-cleanup-close:hover { background: var(--vscode-toolbar-hoverBackground, #e8e8e8); }
39
+ .pnw-cleanup-content { min-height: 0; padding: 10px 12px; overflow: auto; }
40
+ .pnw-cleanup-description, .pnw-cleanup-mode-description, .pnw-cleanup-target-description, .pnw-cleanup-message { color: var(--vscode-descriptionForeground, #616161); }
41
+ .pnw-cleanup-description { margin: 0 0 9px; }
42
+ .pnw-cleanup-field { display: grid; gap: 5px; margin-top: 10px; }
43
+ .pnw-cleanup-field:first-child { margin-top: 0; }
44
+ .pnw-cleanup-label { font-weight: 600; }
45
+ .pnw-cleanup-block { margin-top: 10px; border: 1px solid var(--vscode-panel-border, #d4d4d4); }
46
+ .pnw-cleanup-block > summary { cursor: pointer; padding: 6px 8px; font-weight: 600; background: var(--vscode-sideBarSectionHeader-background, rgba(128,128,128,.12)); }
47
+ .pnw-cleanup-block > summary:hover { background: var(--vscode-list-hoverBackground, rgba(128,128,128,.16)); }
48
+ .pnw-cleanup-block > :not(summary) { margin: 6px; }
49
+ .pnw-cleanup-header-actions { display: flex; align-items: center; gap: 6px; min-width: 0; overflow-x: auto; }
50
+ .pnw-cleanup-header-actions:empty { display: none; }
51
+ .pnw-cleanup-header-actions button, ::slotted(button[slot="header-actions"]) { flex: 0 0 auto; white-space: nowrap; }
52
+ ::slotted(button[slot="header-actions"]) { font: inherit; min-height: 28px; padding: 3px 8px; cursor: pointer; border: 1px solid var(--vscode-button-border, transparent); color: var(--vscode-button-secondaryForeground, inherit); background: var(--vscode-button-secondaryBackground, #e5e5e5); }
53
+ ::slotted(button[slot="header-actions"]:hover) { background: var(--vscode-button-secondaryHoverBackground, #d5d5d5); }
54
+ ::slotted(button[slot="header-actions"]:disabled) { opacity: .55; cursor: default; }
55
+ ::slotted(button[slot="header-actions"]:focus-visible) { outline: 1px solid var(--vscode-focusBorder, #0078d4); }
56
+ .pnw-cleanup-shell[data-header-actions="true"] { grid-template-rows: auto minmax(0, 1fr); }
57
+ .pnw-cleanup-footer[hidden] { display: none; }
58
+ .pnw-cleanup-mode { width: 100%; min-height: 28px; padding: 3px 6px; border: 1px solid var(--vscode-dropdown-border, #8e8e8e); color: var(--vscode-dropdown-foreground, inherit); background: var(--vscode-dropdown-background, #fff); }
59
+ .pnw-cleanup-mode option { color: var(--vscode-dropdown-foreground, var(--vscode-foreground, CanvasText)); background: var(--vscode-dropdown-background, var(--vscode-editor-background, Canvas)); }
60
+ .pnw-cleanup-modes { display: flex; flex-wrap: nowrap; align-items: center; gap: 12px; min-width: 0; overflow-x: auto; }
61
+ .pnw-cleanup-mode-choice { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 5px; min-height: 28px; white-space: nowrap; cursor: pointer; }
62
+ .pnw-cleanup-mode-radio { margin: 0; accent-color: var(--vscode-button-background, #0078d4); }
63
+ .pnw-cleanup-mode-description { margin: 0; font-size: 12px; }
64
+ .pnw-cleanup-targets { display: grid; border: 1px solid var(--vscode-panel-border, #d4d4d4); }
65
+ .pnw-cleanup-target { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px; padding: 6px 7px; align-items: start; }
66
+ .pnw-cleanup-target + .pnw-cleanup-target { border-top: 1px solid var(--vscode-panel-border, #d4d4d4); }
67
+ .pnw-cleanup-target-main { min-width: 0; }
68
+ .pnw-cleanup-target-label { font-weight: 600; }
69
+ .pnw-cleanup-target-path { overflow: hidden; color: var(--vscode-descriptionForeground, #616161); font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
70
+ .pnw-cleanup-target-description { font-size: 11px; }
71
+ .pnw-cleanup-rules { width: 100%; min-height: 132px; resize: vertical; padding: 6px 7px; border: 1px solid var(--vscode-input-border, #8e8e8e); color: var(--vscode-input-foreground, inherit); background: var(--vscode-input-background, #fff); font-family: var(--vscode-editor-font-family, monospace); }
72
+ .pnw-cleanup-preview { min-height: 74px; border: 1px solid var(--vscode-panel-border, #d4d4d4); }
73
+ .pnw-cleanup-preview-summary, .pnw-cleanup-message { margin: 0; padding: 6px 7px; }
74
+ .pnw-cleanup-preview-summary { font-weight: 600; background: var(--vscode-sideBarSectionHeader-background, rgba(128,128,128,.12)); }
75
+ .pnw-cleanup-message[data-error="true"] { color: var(--vscode-errorForeground, #a1260d); }
76
+ .pnw-cleanup-items { max-height: 164px; margin: 0; padding: 3px 7px 6px 25px; overflow: auto; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
77
+ .pnw-cleanup-confirm { display: flex; gap: 7px; margin-top: 10px; padding: 7px; border: 1px solid var(--vscode-inputValidation-warningBorder, #b89500); background: var(--vscode-inputValidation-warningBackground, rgba(184,149,0,.09)); }
78
+ .pnw-cleanup-footer { justify-content: flex-end; border-top: 1px solid var(--vscode-panel-border, #d4d4d4); }
79
+ .pnw-cleanup-button { min-height: 28px; padding: 3px 10px; border: 1px solid var(--vscode-button-border, transparent); color: var(--vscode-button-secondaryForeground, inherit); background: var(--vscode-button-secondaryBackground, #e5e5e5); cursor: pointer; }
80
+ .pnw-cleanup-button:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground, #d5d5d5); }
81
+ .pnw-cleanup-button-primary { color: var(--vscode-button-foreground, #fff); background: var(--vscode-button-background, #0078d4); }
82
+ .pnw-cleanup-button-primary:hover:not(:disabled) { background: var(--vscode-button-hoverBackground, #026ec1); }
83
+ .pnw-cleanup-button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: default; opacity: .55; }
84
+ button:focus-visible, select:focus-visible, textarea:focus-visible, input:focus-visible { outline: 1px solid var(--vscode-focusBorder, #0078d4); outline-offset: 1px; }
85
+ @media (forced-colors: active) { .pnw-cleanup-dialog, .pnw-cleanup-targets, .pnw-cleanup-preview, .pnw-cleanup-rules, .pnw-cleanup-mode { border-color: CanvasText; } }
86
+ `;
87
+ export class PnwCleanupDialog extends HTMLElement {
88
+ pnwRoot = this.attachShadow({ mode: "open" });
89
+ pnwActiveModel = PNW_EMPTY_CLEANUP_DIALOG_MODEL;
90
+ pnwDialog;
91
+ pnwBody;
92
+ pnwTitle;
93
+ pnwDescription;
94
+ pnwWorkspaceSlot;
95
+ pnwCancelButton;
96
+ pnwPreviewButton;
97
+ pnwExecuteButton;
98
+ pnwHighRiskConfirmed = false;
99
+ pnwHeaderActions;
100
+ pnwFooter;
101
+ pnwSectionOpen = new Map();
102
+ connectedCallback() {
103
+ this.pnwUpgradePreDefinitionModel();
104
+ this.pnwEnsureDom();
105
+ this.pnwRender();
106
+ }
107
+ set model(value) {
108
+ const previousToken = this.pnwActiveModel.preview.token;
109
+ this.pnwActiveModel = pnwNormalizeCleanupDialogModel(value);
110
+ if (previousToken !== this.pnwActiveModel.preview.token)
111
+ this.pnwHighRiskConfirmed = false;
112
+ this.pnwEnsureDom();
113
+ this.pnwRender();
114
+ }
115
+ get model() { return this.pnwActiveModel; }
116
+ showModal(modeId) {
117
+ this.pnwEnsureDom();
118
+ if (modeId && this.pnwActiveModel.modes.some(({ id }) => id === modeId)) {
119
+ this.pnwActivateMode(modeId, false);
120
+ }
121
+ this.pnwRender();
122
+ if (!this.pnwDialog?.open) {
123
+ if (typeof this.pnwDialog?.showModal === "function")
124
+ this.pnwDialog.showModal();
125
+ else
126
+ this.pnwDialog?.setAttribute("open", "");
127
+ }
128
+ }
129
+ close() {
130
+ if (!this.pnwDialog?.open && !this.pnwDialog?.hasAttribute("open"))
131
+ return;
132
+ if (typeof this.pnwDialog.close === "function")
133
+ this.pnwDialog.close();
134
+ else
135
+ this.pnwDialog.removeAttribute("open");
136
+ }
137
+ pnwUpgradePreDefinitionModel() {
138
+ if (!Object.prototype.hasOwnProperty.call(this, "model"))
139
+ return;
140
+ const holder = this;
141
+ const model = holder.model;
142
+ delete holder.model;
143
+ this.model = model;
144
+ }
145
+ pnwEnsureDom() {
146
+ if (this.pnwDialog)
147
+ return;
148
+ const style = document.createElement("style");
149
+ style.textContent = PNW_CLEANUP_DIALOG_STYLE;
150
+ const dialog = document.createElement("dialog");
151
+ dialog.className = "pnw-cleanup-dialog";
152
+ dialog.setAttribute("part", "dialog");
153
+ const shell = document.createElement("section");
154
+ shell.className = "pnw-cleanup-shell";
155
+ const header = document.createElement("header");
156
+ header.className = "pnw-cleanup-header";
157
+ const title = document.createElement("h2");
158
+ title.id = "pnw-cleanup-title";
159
+ title.className = "pnw-cleanup-title";
160
+ dialog.setAttribute("aria-labelledby", title.id);
161
+ const close = document.createElement("button");
162
+ close.type = "button";
163
+ close.className = "pnw-cleanup-close";
164
+ close.setAttribute("aria-label", "关闭清理对话框");
165
+ close.textContent = "×";
166
+ close.onclick = () => this.pnwCancel();
167
+ const headerActions = document.createElement("div");
168
+ headerActions.className = "pnw-cleanup-header-actions";
169
+ const hostActions = document.createElement("slot");
170
+ hostActions.name = "header-actions";
171
+ headerActions.append(hostActions);
172
+ const body = document.createElement("div");
173
+ body.className = "pnw-cleanup-content";
174
+ const description = document.createElement("p");
175
+ description.className = "pnw-cleanup-description";
176
+ const workspace = document.createElement("slot");
177
+ workspace.name = "workspace";
178
+ body.append(workspace, description);
179
+ const footer = document.createElement("footer");
180
+ footer.className = "pnw-cleanup-footer";
181
+ const cancel = this.pnwButton("pnw-cleanup-cancel", () => this.pnwCancel());
182
+ const preview = this.pnwButton("pnw-cleanup-preview-action", () => this.pnwEmitPreview());
183
+ const execute = this.pnwButton("pnw-cleanup-execute pnw-cleanup-button-primary", () => this.pnwEmitExecute());
184
+ header.append(title, headerActions, close);
185
+ footer.append(cancel, preview, execute);
186
+ shell.append(header, body, footer);
187
+ dialog.append(shell);
188
+ dialog.addEventListener("cancel", (event) => {
189
+ event.preventDefault();
190
+ this.pnwCancel();
191
+ });
192
+ this.pnwRoot.append(style, dialog);
193
+ this.pnwDialog = dialog;
194
+ this.pnwBody = body;
195
+ this.pnwTitle = title;
196
+ this.pnwDescription = description;
197
+ this.pnwWorkspaceSlot = workspace;
198
+ this.pnwCancelButton = cancel;
199
+ this.pnwPreviewButton = preview;
200
+ this.pnwExecuteButton = execute;
201
+ this.pnwHeaderActions = headerActions;
202
+ this.pnwFooter = footer;
203
+ }
204
+ pnwRender() {
205
+ if (!this.pnwBody || !this.pnwTitle || !this.pnwDescription)
206
+ return;
207
+ const model = this.pnwActiveModel;
208
+ const selectedMode = model.modes.find(({ id }) => id === model.selectedModeId);
209
+ const busy = model.preview.state === "loading" || model.preview.state === "executing";
210
+ const active = this.pnwRoot.activeElement;
211
+ const focusedMode = active?.classList.contains("pnw-cleanup-mode-radio")
212
+ ? active.value : undefined;
213
+ const focusedSelect = active?.classList.contains("pnw-cleanup-mode") === true;
214
+ this.pnwTitle.textContent = model.title;
215
+ this.pnwDescription.textContent = model.description ?? "";
216
+ this.pnwDescription.hidden = !model.description;
217
+ // Keep the Host-owned workspace slot mounted while refreshing mode/preview state.
218
+ for (const child of Array.from(this.pnwBody.children)) {
219
+ if (child !== this.pnwWorkspaceSlot && child !== this.pnwDescription)
220
+ child.remove();
221
+ }
222
+ this.pnwBody.append(this.pnwRenderMode(model, selectedMode, busy), this.pnwRenderTargets(model, busy));
223
+ if (selectedMode?.rulesVisible ?? model.rulesVisible) {
224
+ this.pnwBody.append(this.pnwRenderRules(model, busy));
225
+ }
226
+ this.pnwBody.append(this.pnwRenderPreview(model));
227
+ if (selectedMode?.risk === "high" && model.requireHighRiskConfirmation !== false) {
228
+ this.pnwBody.append(this.pnwRenderHighRisk(model, busy));
229
+ }
230
+ this.pnwRenderActions();
231
+ if (focusedMode !== undefined && !busy)
232
+ this.pnwFocusMode(focusedMode);
233
+ else if (focusedSelect && !busy)
234
+ this.pnwRoot.querySelector(".pnw-cleanup-mode")?.focus();
235
+ }
236
+ pnwRenderActions() {
237
+ const model = this.pnwActiveModel;
238
+ const selectedMode = model.modes.find(({ id }) => id === model.selectedModeId);
239
+ const busy = this.pnwBusy();
240
+ const inHeader = model.actionsPlacement === "header";
241
+ if (this.pnwFooter && this.pnwHeaderActions && this.pnwCancelButton && this.pnwPreviewButton && this.pnwExecuteButton) {
242
+ const actions = inHeader ? this.pnwHeaderActions : this.pnwFooter;
243
+ // Move existing controls, never duplicate actions or their event handlers.
244
+ if (this.pnwCancelButton.parentElement !== actions)
245
+ actions.append(this.pnwCancelButton, this.pnwPreviewButton, this.pnwExecuteButton);
246
+ this.pnwFooter.hidden = inHeader;
247
+ this.pnwFooter.parentElement?.setAttribute("data-header-actions", String(inHeader));
248
+ }
249
+ if (this.pnwCancelButton)
250
+ this.pnwCancelButton.textContent = model.cancelLabel;
251
+ if (this.pnwPreviewButton) {
252
+ this.pnwPreviewButton.textContent = model.preview.state === "loading" ? "预览中…" : model.previewLabel;
253
+ this.pnwPreviewButton.disabled = busy || !model.previewEnabled || !this.pnwRequestReady();
254
+ this.pnwPreviewButton.title = this.pnwPreviewButton.disabled
255
+ ? model.previewDisabledReason ?? "请选择清理方式和目标"
256
+ : "";
257
+ }
258
+ if (this.pnwExecuteButton) {
259
+ this.pnwExecuteButton.textContent = model.preview.state === "executing" ? "清理中…" : model.executeLabel;
260
+ this.pnwExecuteButton.disabled = busy
261
+ || !model.executeEnabled
262
+ || model.preview.state !== "ready"
263
+ || !model.preview.token
264
+ || !this.pnwRequestReady()
265
+ || (selectedMode?.risk === "high" && model.requireHighRiskConfirmation !== false && !this.pnwHighRiskConfirmed);
266
+ this.pnwExecuteButton.title = this.pnwExecuteButton.disabled
267
+ ? model.executeDisabledReason ?? "请先预览并确认"
268
+ : "";
269
+ }
270
+ }
271
+ pnwRenderMode(model, selectedMode, busy) {
272
+ const field = this.pnwField("清理方式");
273
+ if (model.modePresentation === "radio") {
274
+ const group = document.createElement("div");
275
+ group.className = "pnw-cleanup-modes";
276
+ group.setAttribute("role", "radiogroup");
277
+ group.setAttribute("aria-label", "清理方式");
278
+ for (const mode of model.modes) {
279
+ const label = document.createElement("label");
280
+ label.className = "pnw-cleanup-mode-choice";
281
+ const radio = document.createElement("input");
282
+ radio.type = "radio";
283
+ radio.name = "pnw-cleanup-mode";
284
+ radio.className = "pnw-cleanup-mode-radio";
285
+ radio.value = mode.id;
286
+ radio.checked = mode.id === model.selectedModeId;
287
+ radio.disabled = busy;
288
+ radio.onchange = () => {
289
+ if (radio.checked && !radio.disabled)
290
+ this.pnwActivateMode(mode.id, true);
291
+ };
292
+ radio.onkeydown = (event) => {
293
+ if (radio.disabled || this.pnwBusy())
294
+ return;
295
+ const keys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"];
296
+ if (!keys.includes(event.key))
297
+ return;
298
+ event.preventDefault();
299
+ const current = model.modes.findIndex(({ id }) => id === mode.id);
300
+ const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? model.modes.length - 1
301
+ : (current + (["ArrowLeft", "ArrowUp"].includes(event.key) ? -1 : 1) + model.modes.length) % model.modes.length;
302
+ const next = model.modes[nextIndex];
303
+ if (next) {
304
+ this.pnwActivateMode(next.id, true);
305
+ this.pnwFocusMode(next.id);
306
+ }
307
+ };
308
+ const text = document.createElement("span");
309
+ text.textContent = `${mode.label}${mode.risk === "high" ? " · 高风险" : ""}`;
310
+ label.append(radio, text);
311
+ group.append(label);
312
+ }
313
+ field.append(group);
314
+ }
315
+ else {
316
+ const select = document.createElement("select");
317
+ select.className = "pnw-cleanup-mode";
318
+ select.disabled = busy;
319
+ select.setAttribute("aria-label", "清理方式");
320
+ for (const mode of model.modes) {
321
+ const option = document.createElement("option");
322
+ option.value = mode.id;
323
+ option.textContent = `${mode.label}${mode.risk === "high" ? " · 高风险" : ""}`;
324
+ option.selected = mode.id === model.selectedModeId;
325
+ select.append(option);
326
+ }
327
+ select.onchange = () => {
328
+ if (!select.disabled)
329
+ this.pnwActivateMode(select.value, true);
330
+ };
331
+ field.append(select);
332
+ }
333
+ if (selectedMode?.description) {
334
+ const description = document.createElement("p");
335
+ description.className = "pnw-cleanup-mode-description";
336
+ description.textContent = selectedMode.description;
337
+ field.append(description);
338
+ }
339
+ return field;
340
+ }
341
+ pnwRenderTargets(model, busy) {
342
+ const field = this.pnwField("清理目标");
343
+ const targets = document.createElement("div");
344
+ targets.className = "pnw-cleanup-targets";
345
+ const visibleTargets = model.targets.filter((target) => (!target.supportedModeIds?.length || target.supportedModeIds.includes(model.selectedModeId ?? "")));
346
+ for (const target of visibleTargets) {
347
+ const label = document.createElement("label");
348
+ label.className = "pnw-cleanup-target";
349
+ const checkbox = document.createElement("input");
350
+ checkbox.type = "checkbox";
351
+ checkbox.checked = target.selected;
352
+ checkbox.disabled = busy || Boolean(target.disabled);
353
+ checkbox.title = target.disabledReason ?? "";
354
+ checkbox.onchange = () => {
355
+ const nextTargets = this.pnwActiveModel.targets.map((candidate) => (candidate.id === target.id ? { ...candidate, selected: checkbox.checked } : candidate));
356
+ this.pnwHighRiskConfirmed = false;
357
+ this.pnwActiveModel = pnwNormalizeCleanupDialogModel({
358
+ ...this.pnwActiveModel,
359
+ targets: nextTargets,
360
+ preview: { state: "idle", items: [] },
361
+ });
362
+ this.pnwEmit({ kind: "toggle-target", targetId: target.id, selected: checkbox.checked });
363
+ this.pnwRender();
364
+ };
365
+ const main = document.createElement("span");
366
+ main.className = "pnw-cleanup-target-main";
367
+ const name = document.createElement("span");
368
+ name.className = "pnw-cleanup-target-label";
369
+ name.textContent = target.label;
370
+ const path = document.createElement("span");
371
+ path.className = "pnw-cleanup-target-path";
372
+ path.textContent = target.path;
373
+ path.title = target.path;
374
+ main.append(name, document.createElement("br"), path);
375
+ if (target.description) {
376
+ const description = document.createElement("span");
377
+ description.className = "pnw-cleanup-target-description";
378
+ description.textContent = target.description;
379
+ main.append(document.createElement("br"), description);
380
+ }
381
+ label.append(checkbox, main);
382
+ targets.append(label);
383
+ }
384
+ if (!visibleTargets.length) {
385
+ const empty = document.createElement("p");
386
+ empty.className = "pnw-cleanup-message";
387
+ empty.textContent = "当前方式没有可用目标。";
388
+ targets.append(empty);
389
+ }
390
+ field.append(targets);
391
+ return field;
392
+ }
393
+ pnwActivateMode(modeId, emit) {
394
+ if (this.pnwBusy() || !this.pnwActiveModel.modes.some(({ id }) => id === modeId))
395
+ return;
396
+ const nextTargets = this.pnwActiveModel.targets.map((target) => ({
397
+ ...target,
398
+ selected: !target.disabled
399
+ && (!target.supportedModeIds?.length || target.supportedModeIds.includes(modeId)),
400
+ }));
401
+ this.pnwHighRiskConfirmed = false;
402
+ this.pnwActiveModel = pnwNormalizeCleanupDialogModel({
403
+ ...this.pnwActiveModel,
404
+ selectedModeId: modeId,
405
+ targets: nextTargets,
406
+ preview: { state: "idle", items: [] },
407
+ });
408
+ if (emit)
409
+ this.pnwEmit({ kind: "change-mode", modeId });
410
+ this.pnwRender();
411
+ }
412
+ pnwBusy() {
413
+ return this.pnwActiveModel.preview.state === "loading" || this.pnwActiveModel.preview.state === "executing";
414
+ }
415
+ pnwFocusMode(modeId) {
416
+ const radios = Array.from(this.pnwRoot.querySelectorAll(".pnw-cleanup-mode-radio"));
417
+ (radios.find(({ value }) => value === modeId) ?? radios.find(({ checked }) => checked))?.focus();
418
+ }
419
+ pnwRenderRules(model, busy) {
420
+ const field = this.pnwField(model.rulesLabel);
421
+ const rules = document.createElement("textarea");
422
+ rules.className = "pnw-cleanup-rules";
423
+ rules.value = model.rulesYaml;
424
+ rules.disabled = busy;
425
+ rules.spellcheck = false;
426
+ rules.setAttribute("aria-label", model.rulesLabel);
427
+ rules.oninput = () => {
428
+ if (rules.disabled || this.pnwBusy())
429
+ return;
430
+ this.pnwHighRiskConfirmed = false;
431
+ this.pnwActiveModel = pnwNormalizeCleanupDialogModel({
432
+ ...this.pnwActiveModel,
433
+ rulesYaml: rules.value,
434
+ executeEnabled: false,
435
+ preview: { state: "idle", items: [] },
436
+ });
437
+ // Refresh feedback only: keep the active textarea node, caret, IME and scroll intact.
438
+ const previewField = this.pnwBody?.querySelector(".pnw-cleanup-preview")?.parentElement;
439
+ previewField?.replaceWith(this.pnwRenderPreview(this.pnwActiveModel));
440
+ this.pnwBody?.querySelector(".pnw-cleanup-confirm")
441
+ ?.replaceWith(this.pnwRenderHighRisk(this.pnwActiveModel, false));
442
+ this.pnwRenderActions();
443
+ this.pnwEmit({ kind: "change-rules", rulesYaml: rules.value });
444
+ };
445
+ field.append(rules);
446
+ return field;
447
+ }
448
+ pnwRenderPreview(model) {
449
+ const field = this.pnwField("预览结果");
450
+ const preview = document.createElement("section");
451
+ preview.className = "pnw-cleanup-preview";
452
+ preview.setAttribute("aria-live", "polite");
453
+ if (model.preview.summary) {
454
+ const summary = document.createElement("p");
455
+ summary.className = "pnw-cleanup-preview-summary";
456
+ summary.textContent = model.preview.summary;
457
+ preview.append(summary);
458
+ }
459
+ if (model.preview.message || !model.preview.items.length) {
460
+ const message = document.createElement("p");
461
+ message.className = "pnw-cleanup-message";
462
+ message.dataset.error = String(model.preview.state === "error");
463
+ message.textContent = model.preview.message ?? pnwCleanupPreviewFallback(model.preview.state);
464
+ preview.append(message);
465
+ }
466
+ if (model.preview.items.length) {
467
+ const items = document.createElement("ol");
468
+ items.className = "pnw-cleanup-items";
469
+ for (const value of model.preview.items) {
470
+ const item = document.createElement("li");
471
+ item.textContent = value;
472
+ item.title = value;
473
+ items.append(item);
474
+ }
475
+ preview.append(items);
476
+ }
477
+ field.append(preview);
478
+ return field;
479
+ }
480
+ pnwRenderHighRisk(model, busy) {
481
+ const label = document.createElement("label");
482
+ label.className = "pnw-cleanup-confirm";
483
+ const checkbox = document.createElement("input");
484
+ checkbox.type = "checkbox";
485
+ checkbox.checked = this.pnwHighRiskConfirmed;
486
+ checkbox.disabled = busy || model.preview.state !== "ready" || !model.preview.token;
487
+ checkbox.onchange = () => {
488
+ this.pnwHighRiskConfirmed = checkbox.checked;
489
+ this.pnwRender();
490
+ };
491
+ const text = document.createElement("span");
492
+ text.textContent = model.highRiskConfirmationLabel;
493
+ label.append(checkbox, text);
494
+ return label;
495
+ }
496
+ pnwField(labelText) {
497
+ if (this.pnwActiveModel.collapsibleSections && labelText !== "清理方式") {
498
+ const field = document.createElement("details");
499
+ field.className = "pnw-cleanup-block";
500
+ field.dataset.section = labelText;
501
+ field.open = this.pnwSectionOpen.get(labelText) ?? true;
502
+ const summary = document.createElement("summary");
503
+ summary.textContent = labelText;
504
+ // Capture synchronously before mode/model refresh can replace this node.
505
+ summary.onclick = (event) => {
506
+ event.preventDefault();
507
+ field.open = !field.open;
508
+ this.pnwSectionOpen.set(labelText, field.open);
509
+ };
510
+ field.ontoggle = () => { if (field.isConnected)
511
+ this.pnwSectionOpen.set(labelText, field.open); };
512
+ field.append(summary);
513
+ return field;
514
+ }
515
+ const field = document.createElement("div");
516
+ field.className = "pnw-cleanup-field";
517
+ const label = document.createElement("div");
518
+ label.className = "pnw-cleanup-label";
519
+ label.textContent = labelText;
520
+ field.append(label);
521
+ return field;
522
+ }
523
+ pnwButton(className, action) {
524
+ const button = document.createElement("button");
525
+ button.type = "button";
526
+ button.className = `pnw-cleanup-button ${className}`;
527
+ button.onclick = action;
528
+ return button;
529
+ }
530
+ pnwRequest() {
531
+ const modeId = this.pnwActiveModel.selectedModeId;
532
+ if (!modeId)
533
+ return undefined;
534
+ const targetIds = this.pnwActiveModel.targets
535
+ .filter((target) => target.selected
536
+ && !target.disabled
537
+ && (!target.supportedModeIds?.length || target.supportedModeIds.includes(modeId)))
538
+ .map(({ id }) => id);
539
+ if (!targetIds.length)
540
+ return undefined;
541
+ return Object.freeze({ modeId, targetIds: Object.freeze(targetIds), rulesYaml: this.pnwActiveModel.rulesYaml });
542
+ }
543
+ pnwRequestReady() { return Boolean(this.pnwRequest()); }
544
+ pnwEmitPreview() {
545
+ const request = this.pnwRequest();
546
+ if (!request || !this.pnwActiveModel.previewEnabled)
547
+ return;
548
+ this.pnwHighRiskConfirmed = false;
549
+ this.pnwEmit({ kind: "preview", request });
550
+ }
551
+ pnwEmitExecute() {
552
+ const request = this.pnwRequest();
553
+ const token = this.pnwActiveModel.preview.token;
554
+ const selectedMode = this.pnwActiveModel.modes.find(({ id }) => id === request?.modeId);
555
+ if (!request || !token || !this.pnwActiveModel.executeEnabled
556
+ || this.pnwActiveModel.preview.state !== "ready"
557
+ || (selectedMode?.risk === "high" && this.pnwActiveModel.requireHighRiskConfirmation !== false && !this.pnwHighRiskConfirmed))
558
+ return;
559
+ this.pnwEmit({ kind: "execute", request, previewToken: token });
560
+ }
561
+ pnwCancel() {
562
+ this.close();
563
+ this.pnwEmit({ kind: "cancel" });
564
+ }
565
+ pnwEmit(detail) {
566
+ this.dispatchEvent(new CustomEvent(PNW_CLEANUP_DIALOG_ACTION, {
567
+ bubbles: true,
568
+ composed: true,
569
+ detail,
570
+ }));
571
+ }
572
+ }
573
+ export function pnwNormalizeCleanupDialogModel(value) {
574
+ if (!value)
575
+ return PNW_EMPTY_CLEANUP_DIALOG_MODEL;
576
+ const modeIds = new Set();
577
+ const modes = Object.freeze(value.modes.flatMap((mode) => {
578
+ const id = mode.id.trim();
579
+ if (!id || modeIds.has(id))
580
+ return [];
581
+ modeIds.add(id);
582
+ return [Object.freeze({ ...mode, id, label: mode.label.trim() || id })];
583
+ }));
584
+ const selectedModeId = value.selectedModeId && modeIds.has(value.selectedModeId)
585
+ ? value.selectedModeId
586
+ : modes[0]?.id;
587
+ const targetIds = new Set();
588
+ const targets = Object.freeze(value.targets.flatMap((target) => {
589
+ const id = target.id.trim();
590
+ if (!id || targetIds.has(id))
591
+ return [];
592
+ targetIds.add(id);
593
+ return [Object.freeze({
594
+ ...target,
595
+ id,
596
+ label: target.label.trim() || id,
597
+ path: target.path.trim(),
598
+ supportedModeIds: target.supportedModeIds
599
+ ? Object.freeze([...new Set(target.supportedModeIds.filter((modeId) => modeIds.has(modeId)))])
600
+ : undefined,
601
+ })];
602
+ }));
603
+ const preview = Object.freeze({
604
+ ...value.preview,
605
+ items: Object.freeze([...value.preview.items]),
606
+ });
607
+ return Object.freeze({
608
+ ...value,
609
+ title: value.title.trim() || "清理",
610
+ modes,
611
+ modePresentation: value.modePresentation === "radio" ? "radio" : "select",
612
+ selectedModeId,
613
+ targets,
614
+ preview,
615
+ requireHighRiskConfirmation: value.requireHighRiskConfirmation !== false,
616
+ });
617
+ }
618
+ export function pnwCodeDefineCleanupDialog(tagName = PNW_CLEANUP_DIALOG_TAG) {
619
+ if (!customElements.get(tagName))
620
+ customElements.define(tagName, PnwCleanupDialog);
621
+ }
622
+ function pnwCleanupPreviewFallback(state) {
623
+ if (state === "loading")
624
+ return "正在生成预览…";
625
+ if (state === "executing")
626
+ return "正在执行清理…";
627
+ if (state === "complete")
628
+ return "清理已完成。";
629
+ if (state === "error")
630
+ return "清理预览失败。";
631
+ if (state === "ready")
632
+ return "未匹配到需要清理的项目。";
633
+ return "先选择清理方式和目标,再预览实际改动。";
634
+ }
635
+ //# sourceMappingURL=PnwCleanupDialog.js.map