@rogatio/cli 6.9.0 → 6.10.0

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.
@@ -1742,6 +1742,31 @@ var HTTP_METHODS2 = [
1742
1742
  "CONNECT",
1743
1743
  "TRACE"
1744
1744
  ];
1745
+ var RESOURCE_TYPE_LABELS = {
1746
+ main_frame: "Page",
1747
+ sub_frame: "Frame",
1748
+ stylesheet: "Stylesheet",
1749
+ script: "Script",
1750
+ image: "Image",
1751
+ font: "Font",
1752
+ object: "Plugin",
1753
+ media: "Media",
1754
+ xmlhttprequest: "Fetch",
1755
+ ping: "Ping",
1756
+ csp_report: "CSP report",
1757
+ websocket: "WebSocket",
1758
+ webtransport: "WebTransport",
1759
+ webbundle: "Web Bundle",
1760
+ other: "Other"
1761
+ };
1762
+ var TEST_CASE_LIMIT = 256;
1763
+ var RULE_TYPE_LABELS = {
1764
+ redirect: "Redirect",
1765
+ query: "Query parameters",
1766
+ header: "Header",
1767
+ "response-body": "Response body",
1768
+ "request-body": "Request body"
1769
+ };
1745
1770
  var COMMON_RULE_FIELDS = /* @__PURE__ */ new Set([
1746
1771
  "id",
1747
1772
  "name",
@@ -1759,6 +1784,30 @@ var FORBIDDEN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
1759
1784
  var MAX_SNAPSHOT_ARRAY_LENGTH = 4096;
1760
1785
  var F2_MAX_URL_REGEX_LENGTH2 = 2048;
1761
1786
  var editorInstanceCount = 0;
1787
+ function normalizeProjectActions(value) {
1788
+ if (!Array.isArray(value)) return [];
1789
+ const actions = [];
1790
+ for (let index = 0; index < value.length; index += 1) {
1791
+ if (!Object.hasOwn(value, index)) continue;
1792
+ const item = value[index];
1793
+ if (!isRecord4(item)) continue;
1794
+ const command = item.command;
1795
+ const label = item.label;
1796
+ if (typeof command !== "string" || command.length === 0) continue;
1797
+ if (typeof label !== "string" || label.length === 0) continue;
1798
+ if (item.tone === "danger") {
1799
+ actions.push({ command, label, tone: "danger" });
1800
+ } else {
1801
+ actions.push({ command, label });
1802
+ }
1803
+ }
1804
+ return actions;
1805
+ }
1806
+ function ruleCountLabel(count) {
1807
+ if (count === 0) return "No rules";
1808
+ if (count === 1) return "1 rule";
1809
+ return `${count} rules`;
1810
+ }
1762
1811
  function isRecord4(value) {
1763
1812
  return value !== null && typeof value === "object" && !Array.isArray(value);
1764
1813
  }
@@ -2033,6 +2082,80 @@ function displayName(value, fallback) {
2033
2082
  const text = safeText(value, fallback);
2034
2083
  return text.length > 0 ? text : fallback;
2035
2084
  }
2085
+ function checkingCopy(method, resourceType) {
2086
+ if (method === "GET" && resourceType === "main_frame") {
2087
+ return "Checking these as page loads (GET).";
2088
+ }
2089
+ if (method === "" && resourceType === "") {
2090
+ return "The method and the resource type are not tested.";
2091
+ }
2092
+ const resourceLabel = resourceType === "" ? "" : RESOURCE_TYPE_LABELS[resourceType];
2093
+ if (method === "") {
2094
+ const subject = resourceType === "main_frame" ? "page loads" : `${resourceLabel} requests`;
2095
+ return `Checking these as ${subject}. The method is not tested.`;
2096
+ }
2097
+ if (resourceType === "") {
2098
+ return `Checking these as ${method} requests. The resource type is not tested.`;
2099
+ }
2100
+ if (resourceType === "main_frame") {
2101
+ return `Checking these as page loads (${method}).`;
2102
+ }
2103
+ return `Checking these as ${resourceLabel} requests (${method}).`;
2104
+ }
2105
+ function exactUrlFromSource(source) {
2106
+ if (source?.key !== "url" || source.operator !== "regex") {
2107
+ return null;
2108
+ }
2109
+ if (typeof source.value !== "string") return null;
2110
+ const value = source.value;
2111
+ if (!value.startsWith("^") || !value.endsWith("$") || value.length < 2) {
2112
+ return null;
2113
+ }
2114
+ let decoded = "";
2115
+ for (let index = 1; index < value.length - 1; index += 1) {
2116
+ const char = value[index];
2117
+ if (char === "\\") {
2118
+ index += 1;
2119
+ const escaped = value[index];
2120
+ if (escaped === void 0 || index >= value.length - 1) return null;
2121
+ decoded += escaped;
2122
+ continue;
2123
+ }
2124
+ if ("^$\\.*+?()[]{}|".includes(char)) return null;
2125
+ decoded += char;
2126
+ }
2127
+ const converted = urlToExactRegex(decoded);
2128
+ if (!converted.ok || converted.source !== value) return null;
2129
+ return decoded;
2130
+ }
2131
+ function isDryRunResult(value) {
2132
+ if (!isRecord4(value)) return false;
2133
+ const summary = value.summary;
2134
+ return Array.isArray(value.results) && Array.isArray(value.errors) && isRecord4(summary) && typeof summary.matchedUrlCount === "number" && typeof summary.urlCount === "number";
2135
+ }
2136
+ function previewSentence(preview) {
2137
+ if (!preview || preview.summary.length === 0) return null;
2138
+ switch (preview.kind) {
2139
+ case "redirect":
2140
+ return `The browser would go to ${preview.summary}.`;
2141
+ case "query":
2142
+ return `The query would be ${preview.summary}.`;
2143
+ case "header":
2144
+ return `The header would be ${preview.summary}.`;
2145
+ case "request-body":
2146
+ return `The request body would be ${preview.summary}.`;
2147
+ case "response-body":
2148
+ return `The response body would be ${preview.summary}.`;
2149
+ default:
2150
+ return null;
2151
+ }
2152
+ }
2153
+ function missReason(rule) {
2154
+ if (rule.source.state === "unmatched") return "URL pattern";
2155
+ if (rule.method.state === "unmatched") return "method";
2156
+ if (rule.resourceType.state === "unmatched") return "resource type";
2157
+ return "URL pattern";
2158
+ }
2036
2159
  function isHTMLElement(value) {
2037
2160
  return value !== null && typeof value === "object" && value.nodeType === 1 && typeof value.appendChild === "function";
2038
2161
  }
@@ -2163,6 +2286,8 @@ var EditorControllerImpl = class {
2163
2286
  extensionControls = /* @__PURE__ */ new Map();
2164
2287
  extensionCleanups = [];
2165
2288
  confirmation;
2289
+ groupPickerOpen = false;
2290
+ projectActions;
2166
2291
  focusRequest;
2167
2292
  saving = false;
2168
2293
  destroyed = false;
@@ -2171,9 +2296,8 @@ var EditorControllerImpl = class {
2171
2296
  controlNumber = 0;
2172
2297
  previousFocus;
2173
2298
  testUrls = "";
2174
- testMethod = "";
2175
- testResourceType = "";
2176
- testMaxCases = "256";
2299
+ testMethod = "GET";
2300
+ testResourceType = "main_frame";
2177
2301
  testResult = void 0;
2178
2302
  testRunning = false;
2179
2303
  testRequestId = 0;
@@ -2193,6 +2317,7 @@ var EditorControllerImpl = class {
2193
2317
  this.root = options.root;
2194
2318
  this.document = options.root.ownerDocument;
2195
2319
  this.options = options;
2320
+ this.projectActions = normalizeProjectActions(options.projectActions);
2196
2321
  this.extensions = extensions;
2197
2322
  this.instanceId = `rogatio-editor-${++editorInstanceCount}`;
2198
2323
  this.draft = initial;
@@ -2310,6 +2435,10 @@ var EditorControllerImpl = class {
2310
2435
  if (this.destroyed) return;
2311
2436
  const target = event.target;
2312
2437
  if (!(target instanceof Element)) return;
2438
+ if (this.groupPickerOpen && target instanceof HTMLElement && target.dataset.groupPicker !== void 0 && event.target === target) {
2439
+ this.closeGroupPicker();
2440
+ return;
2441
+ }
2313
2442
  const element = target.closest(
2314
2443
  "[data-command], [data-route], [data-search-result], [data-error-path]"
2315
2444
  );
@@ -2349,10 +2478,6 @@ var EditorControllerImpl = class {
2349
2478
  this.testUrls = target.value;
2350
2479
  return;
2351
2480
  }
2352
- if (target.dataset.testMaxCases !== void 0) {
2353
- this.testMaxCases = target.value;
2354
- return;
2355
- }
2356
2481
  const path = target.dataset.path;
2357
2482
  if (!path || target.type === "checkbox" || this.extensionControls.has(path))
2358
2483
  return;
@@ -2366,12 +2491,6 @@ var EditorControllerImpl = class {
2366
2491
  if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) {
2367
2492
  return;
2368
2493
  }
2369
- if (target.dataset.mobileRoute !== void 0 && target instanceof HTMLSelectElement) {
2370
- const selectedOption = target.options[target.selectedIndex];
2371
- const groupId = selectedOption?.dataset.groupId;
2372
- this.navigate(target.value, groupId);
2373
- return;
2374
- }
2375
2494
  if (target instanceof HTMLInputElement && target.dataset.resourcePath !== void 0) {
2376
2495
  this.updateResourceTypes(
2377
2496
  target.dataset.resourcePath,
@@ -2387,10 +2506,12 @@ var EditorControllerImpl = class {
2387
2506
  }
2388
2507
  if (target instanceof HTMLSelectElement && target.dataset.testMethod !== void 0) {
2389
2508
  this.testMethod = target.value || "";
2509
+ this.render();
2390
2510
  return;
2391
2511
  }
2392
2512
  if (target instanceof HTMLSelectElement && target.dataset.testResourceType !== void 0) {
2393
2513
  this.testResourceType = target.value || "";
2514
+ this.render();
2394
2515
  return;
2395
2516
  }
2396
2517
  const path = target.dataset.path;
@@ -2425,6 +2546,11 @@ var EditorControllerImpl = class {
2425
2546
  this.render();
2426
2547
  return;
2427
2548
  }
2549
+ if (event.key === "Escape" && this.groupPickerOpen) {
2550
+ event.preventDefault();
2551
+ this.closeGroupPicker();
2552
+ return;
2553
+ }
2428
2554
  const target = event.target;
2429
2555
  const inRenameInput = target instanceof HTMLElement && target.dataset.renameInput !== void 0;
2430
2556
  if (!inRenameInput) return;
@@ -2571,6 +2697,25 @@ var EditorControllerImpl = class {
2571
2697
  void this.runTest();
2572
2698
  return;
2573
2699
  }
2700
+ if (command === "test:open-rule") {
2701
+ this.navigateToRule(element.dataset.groupId, element.dataset.ruleId);
2702
+ return;
2703
+ }
2704
+ if (command === "test:try-url") {
2705
+ const url = element.dataset.url;
2706
+ if (!url) return;
2707
+ this.testUrls = url;
2708
+ this.statusMessage = "";
2709
+ this.render();
2710
+ return;
2711
+ }
2712
+ if (command === "open-group-picker") {
2713
+ this.openGroupPicker();
2714
+ return;
2715
+ }
2716
+ if (command === "close-group-picker") {
2717
+ this.closeGroupPicker();
2718
+ }
2574
2719
  }
2575
2720
  collectDiagnostics(value) {
2576
2721
  let hostDiagnostics;
@@ -3282,65 +3427,111 @@ var EditorControllerImpl = class {
3282
3427
  this.focusRequest = path;
3283
3428
  this.render();
3284
3429
  }
3430
+ ruleCount() {
3431
+ let count = 0;
3432
+ for (const group of this.draft.groups) count += group.rules.length;
3433
+ return count;
3434
+ }
3435
+ testLines() {
3436
+ const entries = [];
3437
+ const rawLines = this.testUrls.split(/\r?\n/);
3438
+ for (let index = 0; index < rawLines.length; index += 1) {
3439
+ const url = rawLines[index]?.trim() ?? "";
3440
+ if (url.length > 0) entries.push({ url, line: index + 1 });
3441
+ }
3442
+ return entries;
3443
+ }
3444
+ showDryRunDiagnostics(diagnostics) {
3445
+ this.testResult = void 0;
3446
+ this.testRunning = false;
3447
+ this.errors = [...diagnostics];
3448
+ const count = diagnostics.length;
3449
+ this.statusMessage = count === 0 ? "The project could not be tested." : `${count} validation error${count === 1 ? "" : "s"} found.`;
3450
+ const first = diagnostics[0];
3451
+ if (first) {
3452
+ this.navigateToPath(first.path);
3453
+ return;
3454
+ }
3455
+ this.render();
3456
+ }
3285
3457
  async runTest() {
3286
3458
  if (!this.options.dryRun) {
3287
3459
  this.statusMessage = "Dry-run is not configured for this editor instance.";
3288
3460
  this.render();
3289
3461
  return;
3290
3462
  }
3291
- const urlsText = this.testUrls.trim();
3292
- if (!urlsText) {
3463
+ if (this.ruleCount() === 0) {
3464
+ this.testResult = void 0;
3465
+ this.testRunning = false;
3466
+ this.statusMessage = "This project has no rules to test.";
3467
+ this.render();
3468
+ return;
3469
+ }
3470
+ const lines = this.testLines();
3471
+ if (lines.length === 0) {
3293
3472
  this.statusMessage = "Please enter at least one URL to test.";
3294
3473
  this.render();
3295
3474
  return;
3296
3475
  }
3297
- const lines = urlsText.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
3298
- const cases = lines.map((url) => ({
3299
- url,
3300
- method: this.testMethod === "" ? void 0 : this.testMethod,
3301
- resourceType: this.testResourceType === "" ? void 0 : this.testResourceType
3302
- }));
3303
- const maxCasesRaw = Number.parseInt(this.testMaxCases, 10);
3304
- const maxCases = Number.isSafeInteger(maxCasesRaw) && maxCasesRaw > 0 ? maxCasesRaw : void 0;
3476
+ if (lines.length > TEST_CASE_LIMIT) {
3477
+ this.testResult = void 0;
3478
+ this.testRunning = false;
3479
+ this.statusMessage = "Only 256 URLs can be checked at once.";
3480
+ this.render();
3481
+ return;
3482
+ }
3483
+ const cases = lines.map((entry) => {
3484
+ const testCase = { url: entry.url };
3485
+ if (this.testMethod !== "") testCase.method = this.testMethod;
3486
+ if (this.testResourceType !== "") {
3487
+ testCase.resourceType = this.testResourceType;
3488
+ }
3489
+ return testCase;
3490
+ });
3305
3491
  const requestId = ++this.testRequestId;
3306
3492
  this.testRunning = true;
3307
- this.statusMessage = "Running dry-run...";
3493
+ this.statusMessage = "Checking URLs...";
3308
3494
  this.render();
3309
3495
  try {
3310
- const result = await this.options.dryRun(
3311
- this.getDraft(),
3312
- cases,
3313
- maxCases === void 0 ? void 0 : { maxCases }
3314
- );
3496
+ const outcome = await this.options.dryRun(this.getDraft(), cases);
3315
3497
  if (requestId !== this.testRequestId) return;
3316
- this.testResult = result;
3317
3498
  this.testRunning = false;
3318
- const matched = result.summary.matchedUrlCount;
3319
- const total = result.summary.urlCount;
3320
- this.statusMessage = `Test complete: ${matched}/${total} URLs matched at least one rule.`;
3499
+ if (isDryRunResult(outcome)) {
3500
+ this.testResult = outcome;
3501
+ const checked = outcome.summary.urlCount;
3502
+ this.statusMessage = outcome.errors.length > 0 ? `Checked ${checked} URL${checked === 1 ? "" : "s"}. Some lines are not http(s) URLs.` : `Checked ${checked} URL${checked === 1 ? "" : "s"}.`;
3503
+ this.render();
3504
+ return;
3505
+ }
3506
+ const diagnostics = normalizeDiagnostics(
3507
+ isRecord4(outcome) ? outcome.diagnostics : void 0
3508
+ );
3509
+ this.showDryRunDiagnostics(diagnostics);
3321
3510
  } catch (e) {
3322
3511
  if (requestId !== this.testRequestId) return;
3512
+ this.testResult = void 0;
3323
3513
  this.testRunning = false;
3324
3514
  const message = e instanceof Error ? e.message : "Test failed";
3325
3515
  this.statusMessage = `Test error: ${message}`;
3516
+ this.render();
3326
3517
  }
3327
- this.render();
3328
3518
  }
3329
3519
  renderTestResults(result) {
3330
3520
  const resultsSection = this.host.querySelector(
3331
3521
  "[data-test-results]"
3332
3522
  );
3333
- if (!resultsSection) return;
3523
+ if (!resultsSection || !isDryRunResult(result)) return;
3334
3524
  resultsSection.replaceChildren();
3335
3525
  const heading = this.document.createElement("h3");
3336
3526
  heading.textContent = "Results";
3337
3527
  resultsSection.append(heading);
3528
+ const lines = this.testLines();
3338
3529
  if (result.errors.length > 0) {
3339
3530
  const errorsList = this.document.createElement("ul");
3340
3531
  errorsList.dataset.testErrors = "true";
3341
3532
  for (const err of result.errors) {
3342
3533
  const item = this.document.createElement("li");
3343
- item.textContent = `${err.code}: ${err.message}${err.index !== void 0 ? ` (case ${err.index})` : ""}`;
3534
+ item.textContent = this.testErrorText(err, lines);
3344
3535
  errorsList.append(item);
3345
3536
  }
3346
3537
  resultsSection.append(errorsList);
@@ -3355,75 +3546,138 @@ var EditorControllerImpl = class {
3355
3546
  for (const urlResult of result.results) {
3356
3547
  const card = this.document.createElement("article");
3357
3548
  card.dataset.testResultCard = "true";
3358
- const matched = urlResult.matchedRuleCount > 0;
3359
- card.dataset.matched = matched ? "true" : "false";
3360
- const urlHeader = this.document.createElement("div");
3361
- urlHeader.dataset.testResultHeader = "true";
3362
- const urlText = this.document.createElement("strong");
3363
- urlText.textContent = urlResult.url;
3364
- const urlBadge = this.document.createElement("span");
3365
- urlBadge.dataset.testBadge = "true";
3366
- urlBadge.dataset.variant = matched ? "matched" : "unmatched";
3367
- urlBadge.textContent = matched ? "MATCHED" : "NO MATCH";
3368
- urlHeader.append(urlText, urlBadge);
3369
- card.append(urlHeader);
3370
- for (const rule of urlResult.rules) {
3371
- const ruleDiv = this.document.createElement("div");
3372
- ruleDiv.dataset.testRule = "true";
3373
- const ruleHeader = this.document.createElement("div");
3374
- ruleHeader.dataset.testRuleHeader = "true";
3375
- const ruleName = this.document.createElement("strong");
3376
- ruleName.textContent = this.testRuleLabel(rule.groupId, rule.ruleId);
3377
- const ruleBadge = this.document.createElement("span");
3378
- ruleBadge.dataset.testBadge = "true";
3379
- ruleBadge.dataset.variant = rule.matched ? "matched" : "unmatched";
3380
- ruleBadge.textContent = rule.matched ? "\u2713 MATCHED" : "\u2717 NOT MATCHED";
3381
- ruleHeader.append(ruleName, ruleBadge);
3382
- ruleDiv.append(ruleHeader);
3383
- const dims = [
3384
- { label: "source", dim: rule.source },
3385
- { label: "method", dim: rule.method },
3386
- { label: "resourceType", dim: rule.resourceType }
3387
- ];
3388
- for (const { label, dim } of dims) {
3389
- const dimDiv = this.document.createElement("div");
3390
- dimDiv.dataset.testDimension = "true";
3391
- const badge = this.document.createElement("span");
3392
- badge.dataset.testBadge = "true";
3393
- badge.dataset.variant = dim.state === "matched" ? "matched" : dim.state === "unmatched" ? "unmatched" : "na";
3394
- badge.textContent = dim.state.toUpperCase();
3395
- dimDiv.append(
3396
- badge,
3397
- this.document.createTextNode(`${label}: ${dim.detail}`)
3398
- );
3399
- ruleDiv.append(dimDiv);
3400
- }
3401
- if (rule.actionPreview) {
3402
- const apDiv = this.document.createElement("div");
3403
- apDiv.dataset.testActionPreview = "true";
3404
- apDiv.textContent = `Action preview: ${rule.actionPreview.kind} - ${rule.actionPreview.summary}`;
3405
- ruleDiv.append(apDiv);
3406
- }
3407
- card.append(ruleDiv);
3549
+ const matches = urlResult.rules.filter((rule) => rule.matched);
3550
+ const misses = urlResult.rules.filter((rule) => !rule.matched);
3551
+ card.dataset.matched = matches.length > 0 ? "true" : "false";
3552
+ for (const rule of matches) {
3553
+ card.append(this.renderTestOutcome(urlResult.url, rule));
3554
+ }
3555
+ if (misses.length > 0) {
3556
+ card.append(this.renderTestMisses(misses));
3408
3557
  }
3409
3558
  resultsSection.append(card);
3410
3559
  }
3411
3560
  }
3561
+ testErrorText(err, lines) {
3562
+ if (err.code === "dryrun.batch-limit") {
3563
+ return "Only 256 URLs can be checked at once.";
3564
+ }
3565
+ const entry = err.index === void 0 ? void 0 : lines[err.index];
3566
+ if (err.code === "dryrun.invalid-url" && entry) {
3567
+ return `Line ${entry.line} is not an http(s) URL: ${entry.url}`;
3568
+ }
3569
+ if (entry) {
3570
+ return `Line ${entry.line} could not be checked: ${entry.url}`;
3571
+ }
3572
+ return err.message;
3573
+ }
3574
+ renderTestOutcome(url, rule) {
3575
+ const names = this.testRuleNames(rule.groupId, rule.ruleId);
3576
+ const typeLabel = this.testRuleTypeLabel(rule.groupId, rule.ruleId);
3577
+ const outcome = this.document.createElement("p");
3578
+ outcome.dataset.testOutcome = "true";
3579
+ const lead = this.document.createElement("span");
3580
+ lead.textContent = names.groupKnown ? `${url} matches ${typeLabel} in ${names.groupName} / ` : `${url} matches ${typeLabel} in `;
3581
+ const ruleButton = this.createCommandButton(
3582
+ names.ruleLabel,
3583
+ "test:open-rule",
3584
+ false,
3585
+ { groupId: rule.groupId, ruleId: rule.ruleId }
3586
+ );
3587
+ ruleButton.dataset.testRuleLink = "true";
3588
+ ruleButton.removeAttribute("data-btn");
3589
+ const tailParts = ["."];
3590
+ const preview = previewSentence(rule.actionPreview);
3591
+ if (preview) tailParts.push(` ${preview}`);
3592
+ if (rule.method.state === "not-applicable") {
3593
+ tailParts.push(" The method was not tested.");
3594
+ }
3595
+ if (rule.resourceType.state === "not-applicable") {
3596
+ tailParts.push(" The resource type was not tested.");
3597
+ }
3598
+ if (this.groupIsOffInBrowser(rule.groupId)) {
3599
+ tailParts.push(
3600
+ " This group is off in Chrome, so the browser will not apply this rule."
3601
+ );
3602
+ }
3603
+ const tail = this.document.createElement("span");
3604
+ tail.textContent = tailParts.join("");
3605
+ outcome.append(lead, ruleButton, tail);
3606
+ return outcome;
3607
+ }
3608
+ renderTestMisses(misses) {
3609
+ const details = this.document.createElement("details");
3610
+ details.dataset.testMisses = "true";
3611
+ const summary = this.document.createElement("summary");
3612
+ summary.textContent = misses.length === 1 ? "1 rule did not match" : `${misses.length} rules did not match`;
3613
+ const list = this.document.createElement("ul");
3614
+ for (const rule of misses) {
3615
+ const names = this.testRuleNames(rule.groupId, rule.ruleId);
3616
+ const item = this.document.createElement("li");
3617
+ const ruleButton = this.createCommandButton(
3618
+ names.ruleLabel,
3619
+ "test:open-rule",
3620
+ false,
3621
+ { groupId: rule.groupId, ruleId: rule.ruleId }
3622
+ );
3623
+ ruleButton.dataset.testRuleLink = "true";
3624
+ ruleButton.removeAttribute("data-btn");
3625
+ const reason = this.document.createElement("span");
3626
+ reason.dataset.testMissReason = "true";
3627
+ reason.textContent = missReason(rule);
3628
+ item.append(ruleButton, this.document.createTextNode(" \u2014 "), reason);
3629
+ list.append(item);
3630
+ }
3631
+ details.append(summary, list);
3632
+ return details;
3633
+ }
3412
3634
  /**
3413
3635
  * A test-result row is identified by the names the user actually sees. The dry
3414
3636
  * result carries ids only, and the draft the editor handed over is the source
3415
- * of truth for both, so no contract changes. A rule that is no longer in the
3416
- * draft falls back to its id rather than rendering a blank row.
3637
+ * of truth for both. A rule that is no longer in the draft falls back to its id.
3417
3638
  */
3418
- testRuleLabel(groupId, ruleId) {
3639
+ testRuleNames(groupId, ruleId) {
3419
3640
  const group = this.groupById(groupId);
3420
- if (!group) return `${groupId}/${ruleId}`;
3641
+ if (!group) {
3642
+ return {
3643
+ groupKnown: false,
3644
+ ruleKnown: false,
3645
+ groupName: groupId,
3646
+ ruleLabel: `${groupId}/${ruleId}`
3647
+ };
3648
+ }
3421
3649
  const groupName = displayName(group.name, "Unnamed group");
3422
3650
  const rule = this.ruleById(groupId, ruleId);
3423
- if (!rule) return `${groupName} / ${ruleId}`;
3424
- return `${groupName} / ${displayName(rule.name, "Unnamed rule")}`;
3651
+ if (!rule) {
3652
+ return {
3653
+ groupKnown: true,
3654
+ ruleKnown: false,
3655
+ groupName,
3656
+ ruleLabel: ruleId
3657
+ };
3658
+ }
3659
+ return {
3660
+ groupKnown: true,
3661
+ ruleKnown: true,
3662
+ groupName,
3663
+ ruleLabel: displayName(rule.name, "Unnamed rule")
3664
+ };
3665
+ }
3666
+ testRuleTypeLabel(groupId, ruleId) {
3667
+ const rule = this.ruleById(groupId, ruleId);
3668
+ const type = rule && typeof rule.type === "string" ? rule.type : "";
3669
+ return RULE_TYPE_LABELS[type] ?? "the rule";
3670
+ }
3671
+ groupIsOffInBrowser(groupId) {
3672
+ const enablement = this.options.groupEnablement;
3673
+ if (!enablement) return false;
3674
+ const group = this.groupById(groupId);
3675
+ const saved = group ? this.savedGroupId(group) : void 0;
3676
+ const id = saved && saved.length > 0 ? saved : groupId;
3677
+ return enablement.isEnabled(id) !== true;
3425
3678
  }
3426
3679
  navigate(route, groupId) {
3680
+ const returnFocusToGroups = this.groupPickerOpen;
3427
3681
  if (route === "project") {
3428
3682
  this.route = { kind: "project" };
3429
3683
  } else if (route === "test") {
@@ -3433,6 +3687,8 @@ var EditorControllerImpl = class {
3433
3687
  } else {
3434
3688
  return;
3435
3689
  }
3690
+ this.groupPickerOpen = false;
3691
+ if (returnFocusToGroups) this.focusRequest = "route:groups";
3436
3692
  this.closeRenameOnLeave();
3437
3693
  this.testRequestId += 1;
3438
3694
  this.statusMessage = "";
@@ -3458,6 +3714,7 @@ var EditorControllerImpl = class {
3458
3714
  }
3459
3715
  }
3460
3716
  navigateToGroup(groupId) {
3717
+ this.groupPickerOpen = false;
3461
3718
  const groupIds = new Set(
3462
3719
  this.draft.groups.map((group) => typeof group.id === "string" ? group.id : "").filter((id) => id.length > 0)
3463
3720
  );
@@ -3530,6 +3787,7 @@ var EditorControllerImpl = class {
3530
3787
  this.revealRuleCard(resolvedGroup, resolvedRule);
3531
3788
  }
3532
3789
  navigateToSearchResult(path) {
3790
+ this.groupPickerOpen = false;
3533
3791
  const segments = decodePointer(path);
3534
3792
  if (segments?.[0] !== "groups") {
3535
3793
  this.route = { kind: "project" };
@@ -3621,7 +3879,9 @@ var EditorControllerImpl = class {
3621
3879
  this.renderSummary();
3622
3880
  this.renderMigrationNotices();
3623
3881
  this.renderSearchResults();
3882
+ if (this.confirmation) this.groupPickerOpen = false;
3624
3883
  this.renderConfirmation();
3884
+ this.renderGroupPicker();
3625
3885
  this.restoreFocus();
3626
3886
  this.focusRequest = void 0;
3627
3887
  }
@@ -3634,39 +3894,7 @@ var EditorControllerImpl = class {
3634
3894
  dirty.dataset.dirtyState = "true";
3635
3895
  dirty.textContent = this.isDirty() ? "Unsaved changes" : "All changes saved";
3636
3896
  titleBlock.append(title, dirty);
3637
- const mobileNav = this.document.createElement("label");
3638
- mobileNav.dataset.mobileRouteNav = "true";
3639
- mobileNav.textContent = "Project section";
3640
- const select = this.document.createElement("select");
3641
- select.dataset.mobileRoute = "true";
3642
- select.dataset.editorKey = "mobile-route";
3643
- const projectOption = this.document.createElement("option");
3644
- projectOption.value = "project";
3645
- projectOption.textContent = "Project";
3646
- select.append(projectOption);
3647
- for (const group of this.draft.groups) {
3648
- const option = this.document.createElement("option");
3649
- option.value = "group";
3650
- option.dataset.groupId = safeText(group.id);
3651
- option.textContent = displayName(group.name, "Unnamed group");
3652
- select.append(option);
3653
- }
3654
- const testOption = this.document.createElement("option");
3655
- testOption.value = "test";
3656
- testOption.textContent = "Test console";
3657
- select.append(testOption);
3658
- if (this.route.kind === "project") {
3659
- select.value = "project";
3660
- } else if (this.route.kind === "test") {
3661
- select.value = "test";
3662
- } else {
3663
- const groupId = this.route.groupId;
3664
- const options = Array.from(select.options);
3665
- const option = options.find((value) => value.dataset.groupId === groupId);
3666
- if (option) select.value = "group";
3667
- }
3668
- mobileNav.append(select);
3669
- this.header.append(titleBlock, mobileNav);
3897
+ this.header.append(titleBlock);
3670
3898
  this.status.textContent = this.statusMessage;
3671
3899
  this.status.setAttribute("aria-busy", this.saving ? "true" : "false");
3672
3900
  }
@@ -3676,30 +3904,34 @@ var EditorControllerImpl = class {
3676
3904
  heading.dataset.editorVisuallyHidden = "true";
3677
3905
  heading.textContent = "Project sections";
3678
3906
  this.rail.append(heading);
3679
- const project = this.createButton("Project", "route:project");
3907
+ const list = this.document.createElement("ol");
3908
+ list.dataset.routeBreadcrumb = "true";
3909
+ const projectItem = this.document.createElement("li");
3910
+ const project = this.createButton(
3911
+ displayName(this.draft.name, "Project"),
3912
+ "route:project"
3913
+ );
3680
3914
  project.dataset.route = "project";
3681
3915
  project.dataset.editorKey = "route:project";
3682
- if (this.route.kind === "project")
3916
+ if (this.route.kind === "project") {
3683
3917
  project.setAttribute("aria-current", "page");
3684
- this.rail.append(project);
3685
- for (const group of this.draft.groups) {
3686
- const groupId = safeText(group.id);
3687
- const name = displayName(group.name, "Unnamed group");
3688
- const button = this.createButton(name, `route:group:${groupId}`);
3689
- button.dataset.route = "group";
3690
- button.dataset.groupId = groupId;
3691
- button.dataset.editorKey = `route:group:${groupId}`;
3692
- if (this.route.kind === "group" && this.route.groupId === groupId) {
3693
- button.setAttribute("aria-current", "page");
3694
- }
3695
- this.rail.append(button);
3696
3918
  }
3697
- const testBtn = this.createButton("Test console", "route:test");
3698
- testBtn.dataset.route = "test";
3699
- testBtn.dataset.editorKey = "route:test";
3700
- if (this.route.kind === "test")
3701
- testBtn.setAttribute("aria-current", "page");
3702
- this.rail.append(testBtn);
3919
+ projectItem.append(project);
3920
+ const groupItem = this.document.createElement("li");
3921
+ const openGroup = this.route.kind === "group" ? this.groupById(this.route.groupId) : void 0;
3922
+ const groups = this.createButton(
3923
+ openGroup ? displayName(openGroup.name, "Unnamed group") : "Groups",
3924
+ "route:groups"
3925
+ );
3926
+ groups.dataset.command = "open-group-picker";
3927
+ groups.dataset.editorKey = "route:groups";
3928
+ if (openGroup) {
3929
+ groups.dataset.groupId = safeText(openGroup.id);
3930
+ groups.setAttribute("aria-current", "page");
3931
+ }
3932
+ groupItem.append(groups);
3933
+ list.append(projectItem, groupItem);
3934
+ this.rail.append(list);
3703
3935
  const searchWrap = this.document.createElement("div");
3704
3936
  searchWrap.dataset.searchWrap = "true";
3705
3937
  const searchLabel = this.document.createElement("label");
@@ -3731,18 +3963,18 @@ var EditorControllerImpl = class {
3731
3963
  this.createCommandButton("AI Assist", "ai-assist", this.saving)
3732
3964
  );
3733
3965
  }
3734
- tools.append(this.createCommandButton("Validate", "validate", this.saving));
3966
+ const testBtn = this.createButton("Test console", "route:test");
3967
+ testBtn.dataset.route = "test";
3968
+ testBtn.dataset.editorKey = "route:test";
3969
+ testBtn.dataset.btn = "secondary";
3970
+ testBtn.disabled = this.saving;
3735
3971
  if (this.route.kind === "test") {
3736
- tools.append(
3737
- this.createCommandButton(
3738
- "Run test",
3739
- "test:run",
3740
- this.saving || this.testRunning,
3741
- {},
3742
- "primary"
3743
- )
3744
- );
3972
+ testBtn.setAttribute("aria-current", "page");
3745
3973
  }
3974
+ tools.append(
3975
+ testBtn,
3976
+ this.createCommandButton("Validate", "validate", this.saving)
3977
+ );
3746
3978
  const commit = this.document.createElement("div");
3747
3979
  commit.dataset.actionCluster = "commit";
3748
3980
  commit.append(
@@ -3890,19 +4122,6 @@ var EditorControllerImpl = class {
3890
4122
  this.createGroupActionClusters(groupId, groupName)
3891
4123
  );
3892
4124
  }
3893
- } else {
3894
- const run = this.document.createElement("div");
3895
- run.dataset.actionCluster = "safe";
3896
- run.append(
3897
- this.createCommandButton(
3898
- "Run test",
3899
- "test:run",
3900
- this.saving || this.testRunning,
3901
- {},
3902
- "primary"
3903
- )
3904
- );
3905
- this.appendDockRow(dock, "Test", run);
3906
4125
  }
3907
4126
  this.appendDockRow(dock, "Project", this.createProjectActionRow());
3908
4127
  this.form.append(dock);
@@ -3932,6 +4151,7 @@ var EditorControllerImpl = class {
3932
4151
  description
3933
4152
  );
3934
4153
  fields.append(fieldGrid);
4154
+ this.renderProjectActions(fields);
3935
4155
  this.form.append(fields);
3936
4156
  const groups = this.document.createElement("section");
3937
4157
  groups.dataset.groupListSection = "true";
@@ -4055,7 +4275,7 @@ var EditorControllerImpl = class {
4055
4275
  this.form.append(heading);
4056
4276
  const description = this.document.createElement("p");
4057
4277
  description.dataset.testDescription = "true";
4058
- description.textContent = "Run offline dry-run tests against the current project. Enter test cases (one URL per line) and optional method/resource type defaults. No network requests are made.";
4278
+ description.textContent = "Check whether these URLs match your rules. Nothing is contacted, and nothing is saved.";
4059
4279
  this.form.append(description);
4060
4280
  const panel = this.document.createElement("div");
4061
4281
  panel.dataset.testPanel = "true";
@@ -4072,91 +4292,128 @@ var EditorControllerImpl = class {
4072
4292
  urlsTextarea.placeholder = "https://example.com/page\nhttps://example.com/script.js\nhttps://other.com/";
4073
4293
  urlsTextarea.value = this.testUrls;
4074
4294
  urlsTextarea.dataset.testUrls = "true";
4295
+ urlsTextarea.dataset.editorKey = "test-urls";
4075
4296
  urlsLabel.append(urlsTextarea);
4076
4297
  urlsField.append(urlsLabel);
4077
4298
  urlsFieldset.append(urlsField);
4299
+ const samples = this.renderSampleUrlButtons();
4300
+ if (samples) urlsFieldset.append(samples);
4078
4301
  panel.append(urlsFieldset);
4079
- const defaultsFieldset = this.document.createElement("fieldset");
4080
- const defaultsLegend = this.document.createElement("legend");
4081
- defaultsLegend.textContent = "Defaults (applied to all URLs without explicit values)";
4082
- defaultsFieldset.append(defaultsLegend);
4302
+ const checking = this.document.createElement("p");
4303
+ checking.dataset.testChecking = "true";
4304
+ checking.textContent = checkingCopy(this.testMethod, this.testResourceType);
4305
+ panel.append(checking);
4083
4306
  const defaultsGrid = this.document.createElement("div");
4084
4307
  defaultsGrid.dataset.testDefaults = "true";
4085
4308
  const methodField = this.document.createElement("div");
4086
4309
  methodField.dataset.editorField = "true";
4087
4310
  const methodLabel = this.document.createElement("label");
4088
- methodLabel.textContent = "Default HTTP method";
4311
+ methodLabel.textContent = "Method";
4089
4312
  const methodSelect = this.document.createElement("select");
4090
4313
  methodSelect.dataset.testMethod = "true";
4314
+ methodSelect.dataset.editorKey = "test-method";
4315
+ for (const method of HTTP_METHODS2) {
4316
+ const option = this.document.createElement("option");
4317
+ option.value = method;
4318
+ option.textContent = method;
4319
+ methodSelect.append(option);
4320
+ }
4321
+ const methodAny = this.document.createElement("option");
4322
+ methodAny.value = "";
4323
+ methodAny.textContent = "Any method";
4324
+ methodSelect.append(methodAny);
4091
4325
  methodSelect.value = this.testMethod;
4092
- const methodEmpty = this.document.createElement("option");
4093
- methodEmpty.value = "";
4094
- methodEmpty.textContent = "(none \u2014 not-applicable)";
4095
- methodSelect.append(methodEmpty);
4096
- for (const m of HTTP_METHODS2) {
4097
- const opt = this.document.createElement("option");
4098
- opt.value = m;
4099
- opt.textContent = m;
4100
- methodSelect.append(opt);
4101
- }
4102
4326
  methodLabel.append(methodSelect);
4103
4327
  methodField.append(methodLabel);
4104
4328
  const rtField = this.document.createElement("div");
4105
4329
  rtField.dataset.editorField = "true";
4106
4330
  const rtLabel = this.document.createElement("label");
4107
- rtLabel.textContent = "Default resource type";
4331
+ rtLabel.textContent = "Resource type";
4108
4332
  const rtSelect = this.document.createElement("select");
4109
4333
  rtSelect.dataset.testResourceType = "true";
4334
+ rtSelect.dataset.editorKey = "test-resource-type";
4335
+ for (const resourceType of RESOURCE_TYPES2) {
4336
+ const option = this.document.createElement("option");
4337
+ option.value = resourceType;
4338
+ option.textContent = RESOURCE_TYPE_LABELS[resourceType];
4339
+ rtSelect.append(option);
4340
+ }
4341
+ const rtAny = this.document.createElement("option");
4342
+ rtAny.value = "";
4343
+ rtAny.textContent = "Any resource type";
4344
+ rtSelect.append(rtAny);
4110
4345
  rtSelect.value = this.testResourceType;
4111
- const rtEmpty = this.document.createElement("option");
4112
- rtEmpty.value = "";
4113
- rtEmpty.textContent = "(none \u2014 not-applicable)";
4114
- rtSelect.append(rtEmpty);
4115
- for (const rt of RESOURCE_TYPES2) {
4116
- const opt = this.document.createElement("option");
4117
- opt.value = rt;
4118
- opt.textContent = rt;
4119
- rtSelect.append(opt);
4120
- }
4121
4346
  rtLabel.append(rtSelect);
4122
4347
  rtField.append(rtLabel);
4123
4348
  defaultsGrid.append(methodField, rtField);
4124
- defaultsFieldset.append(defaultsGrid);
4125
- panel.append(defaultsFieldset);
4126
- const maxCasesField = this.document.createElement("div");
4127
- maxCasesField.dataset.editorField = "true";
4128
- const maxCasesLabel = this.document.createElement("label");
4129
- maxCasesLabel.textContent = "Max test cases";
4130
- const maxCasesInput = this.document.createElement("input");
4131
- maxCasesInput.type = "number";
4132
- maxCasesInput.min = "1";
4133
- maxCasesInput.max = "10000";
4134
- maxCasesInput.value = this.testMaxCases;
4135
- maxCasesInput.style.width = "8rem";
4136
- maxCasesInput.dataset.testMaxCases = "true";
4137
- maxCasesLabel.append(maxCasesInput);
4138
- maxCasesField.append(maxCasesLabel);
4139
- panel.append(maxCasesField);
4140
- const runBtn = this.createButton("Run test", "test:run");
4141
- runBtn.type = "button";
4349
+ panel.append(defaultsGrid);
4350
+ const runBtn = this.createCommandButton(
4351
+ "Run test",
4352
+ "test:run",
4353
+ this.saving || this.testRunning,
4354
+ {},
4355
+ "primary"
4356
+ );
4142
4357
  runBtn.dataset.testRun = "true";
4143
- runBtn.dataset.command = "test:run";
4144
- runBtn.dataset.btn = "primary";
4145
- runBtn.disabled = this.saving || this.testRunning;
4146
4358
  panel.append(runBtn);
4147
4359
  this.form.append(panel);
4148
4360
  const resultsSection = this.document.createElement("section");
4149
4361
  resultsSection.dataset.testResults = "true";
4150
4362
  this.form.append(resultsSection);
4151
- if (this.testResult) {
4363
+ if (this.ruleCount() === 0) {
4364
+ const empty = this.document.createElement("p");
4365
+ empty.dataset.testEmpty = "true";
4366
+ empty.textContent = "This project has no rules to test.";
4367
+ resultsSection.append(empty);
4368
+ } else if (this.testResult) {
4152
4369
  this.renderTestResults(this.testResult);
4153
4370
  } else if (this.testRunning) {
4154
4371
  const pending = this.document.createElement("p");
4155
4372
  pending.dataset.testPending = "true";
4156
- pending.textContent = "Running dry-run...";
4373
+ pending.textContent = "Checking URLs...";
4157
4374
  resultsSection.append(pending);
4375
+ } else if (this.statusMessage === "Only 256 URLs can be checked at once.") {
4376
+ const limit = this.document.createElement("p");
4377
+ limit.dataset.testLimit = "true";
4378
+ limit.textContent = this.statusMessage;
4379
+ resultsSection.append(limit);
4158
4380
  }
4159
4381
  }
4382
+ renderSampleUrlButtons() {
4383
+ const samples = [];
4384
+ for (const group of this.draft.groups) {
4385
+ const groupId = safeText(group.id);
4386
+ for (const rule of group.rules) {
4387
+ const url = exactUrlFromSource(rule.source);
4388
+ if (!url) continue;
4389
+ samples.push({
4390
+ groupId,
4391
+ ruleId: safeText(rule.id),
4392
+ url,
4393
+ name: displayName(rule.name, "Unnamed rule")
4394
+ });
4395
+ }
4396
+ }
4397
+ if (samples.length === 0) return void 0;
4398
+ const row = this.document.createElement("div");
4399
+ row.dataset.testSamples = "true";
4400
+ for (const sample of samples) {
4401
+ const button = this.createCommandButton(
4402
+ "Try a URL from this rule",
4403
+ "test:try-url",
4404
+ false,
4405
+ { groupId: sample.groupId, ruleId: sample.ruleId, url: sample.url }
4406
+ );
4407
+ if (samples.length > 1) {
4408
+ button.setAttribute(
4409
+ "aria-label",
4410
+ `Try a URL from this rule, ${sample.name}`
4411
+ );
4412
+ }
4413
+ row.append(button);
4414
+ }
4415
+ return row;
4416
+ }
4160
4417
  renderRule(group, rule, groupIndex, ruleIndex) {
4161
4418
  const groupId = safeText(group.id);
4162
4419
  const ruleId = safeText(rule.id);
@@ -5056,6 +5313,103 @@ var EditorControllerImpl = class {
5056
5313
  }
5057
5314
  return results;
5058
5315
  }
5316
+ renderProjectActions(fields) {
5317
+ if (this.projectActions.length === 0) return;
5318
+ const group = this.document.createElement("div");
5319
+ group.dataset.projectActions = "true";
5320
+ group.setAttribute("role", "group");
5321
+ group.setAttribute("aria-label", "Project settings");
5322
+ for (const action of this.projectActions) {
5323
+ group.append(
5324
+ this.createCommandButton(
5325
+ action.label,
5326
+ action.command,
5327
+ false,
5328
+ {},
5329
+ action.tone === "danger" ? "danger" : "secondary"
5330
+ )
5331
+ );
5332
+ }
5333
+ fields.append(group);
5334
+ }
5335
+ openGroupPicker() {
5336
+ this.groupPickerOpen = true;
5337
+ this.focusRequest = this.draft.groups.length > 0 ? "group-picker-first" : "group-picker-close";
5338
+ this.render();
5339
+ }
5340
+ closeGroupPicker() {
5341
+ this.groupPickerOpen = false;
5342
+ this.focusRequest = "route:groups";
5343
+ this.render();
5344
+ }
5345
+ renderGroupPicker() {
5346
+ const existing = this.host.querySelector("[data-group-picker]");
5347
+ existing?.remove();
5348
+ if (!this.groupPickerOpen) return;
5349
+ const overlay = this.document.createElement("div");
5350
+ overlay.dataset.groupPicker = "true";
5351
+ overlay.setAttribute("role", "presentation");
5352
+ const dialog = this.document.createElement("section");
5353
+ dialog.dataset.editorDialog = "true";
5354
+ dialog.setAttribute("role", "dialog");
5355
+ dialog.setAttribute("aria-modal", "true");
5356
+ const title = this.document.createElement("h2");
5357
+ title.id = `${this.instanceId}-group-picker-title`;
5358
+ title.textContent = "Groups";
5359
+ dialog.setAttribute("aria-labelledby", title.id);
5360
+ dialog.append(title);
5361
+ if (this.draft.groups.length === 0) {
5362
+ const empty = this.document.createElement("p");
5363
+ empty.textContent = "No groups yet.";
5364
+ dialog.append(empty);
5365
+ } else {
5366
+ const list = this.document.createElement("ul");
5367
+ list.dataset.groupPickerList = "true";
5368
+ for (let index = 0; index < this.draft.groups.length; index += 1) {
5369
+ const group = this.draft.groups[index];
5370
+ if (!group) continue;
5371
+ const groupId = safeText(group.id);
5372
+ const name = displayName(group.name, "Unnamed group");
5373
+ const item = this.document.createElement("li");
5374
+ const button = this.document.createElement("button");
5375
+ button.type = "button";
5376
+ button.dataset.route = "group";
5377
+ button.dataset.groupId = groupId;
5378
+ button.dataset.btn = "secondary";
5379
+ button.dataset.editorKey = index === 0 ? "group-picker-first" : `group-picker:${groupId}`;
5380
+ if (this.route.kind === "group" && this.route.groupId === groupId) {
5381
+ button.setAttribute("aria-current", "true");
5382
+ }
5383
+ const nameEl = this.document.createElement("span");
5384
+ nameEl.dataset.groupPickerName = "true";
5385
+ nameEl.textContent = name;
5386
+ const rulesEl = this.document.createElement("span");
5387
+ rulesEl.dataset.groupPickerRules = "true";
5388
+ rulesEl.textContent = ruleCountLabel(group.rules.length);
5389
+ button.append(nameEl, rulesEl);
5390
+ const savedId = this.savedGroupId(group);
5391
+ if (this.options.groupEnablement && savedId) {
5392
+ const stateEl = this.document.createElement("span");
5393
+ stateEl.dataset.groupPickerState = "true";
5394
+ stateEl.textContent = this.options.groupEnablement.isEnabled(savedId) ? "Enabled" : "Disabled";
5395
+ button.append(stateEl);
5396
+ }
5397
+ item.append(button);
5398
+ list.append(item);
5399
+ }
5400
+ dialog.append(list);
5401
+ }
5402
+ const actions = this.document.createElement("div");
5403
+ actions.dataset.editorDialogActions = "true";
5404
+ const close = this.createButton("Close", "group-picker-close");
5405
+ close.dataset.command = "close-group-picker";
5406
+ close.dataset.editorKey = "group-picker-close";
5407
+ close.dataset.btn = "secondary";
5408
+ actions.append(close);
5409
+ dialog.append(actions);
5410
+ overlay.append(dialog);
5411
+ this.host.append(overlay);
5412
+ }
5059
5413
  renderConfirmation() {
5060
5414
  const existing = this.host.querySelector("[data-editor-confirmation]");
5061
5415
  existing?.remove();