@rogatio/cli 6.17.0 → 6.18.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.
package/README.md CHANGED
@@ -109,7 +109,7 @@ rogatio ai delete
109
109
 
110
110
  Configuration is stored at `~/.config/rogatio/provider.json` (Linux), `~/Library/Application Support/rogatio/provider.json` (macOS), or `%LOCALAPPDATA%\rogatio\provider.json` (Windows) with `600` permissions.
111
111
 
112
- When AI is configured, `rogatio edit` shows an **AI Assist** button in the command bar. Click it to:
112
+ When AI is configured, `rogatio edit` shows an **AI Assist** button on the project title row. Click it to:
113
113
  - Generate rules from natural language
114
114
  - Fix validation errors by repairing the offending rules in place (keeping their rule ids; max 3 auto-fix iterations)
115
115
 
@@ -213,8 +213,8 @@
213
213
  font-weight: 600;
214
214
  }
215
215
  .rogatio-editor [data-search-wrap] {
216
- margin-left: auto;
217
- flex: 0 0 auto;
216
+ position: relative;
217
+ flex: 0 1 18rem;
218
218
  width: min(18rem, 100%);
219
219
  min-width: 0;
220
220
  }
@@ -223,11 +223,20 @@
223
223
  padding: 1rem clamp(1rem, 3vw, 3rem) 3rem;
224
224
  }
225
225
  .rogatio-editor [data-editor-header] {
226
- display: grid;
227
- grid-template-columns: minmax(0, 1fr);
228
- gap: 1rem;
229
- align-items: end;
230
- margin-bottom: 1rem;
226
+ display: flex;
227
+ flex-wrap: wrap;
228
+ align-items: center;
229
+ justify-content: space-between;
230
+ gap: 0.75rem 1.5rem;
231
+ margin-bottom: 1.25rem;
232
+ }
233
+ .rogatio-editor [data-editor-header-actions] {
234
+ display: flex;
235
+ flex-wrap: wrap;
236
+ align-items: center;
237
+ justify-content: flex-end;
238
+ gap: 0.5rem 0.65rem;
239
+ margin-left: auto;
231
240
  }
232
241
  .rogatio-editor [data-editor-header] h1,
233
242
  .rogatio-editor [data-editor-form] h2,
@@ -302,29 +311,12 @@
302
311
  text-align: left;
303
312
  text-decoration: underline;
304
313
  }
305
- .rogatio-editor [data-editor-command-bar] button[aria-current=page] {
306
- border-color: rgba(59, 158, 255, 0.45);
307
- background: var(--rogatio-primary-bg);
308
- font-weight: 700;
309
- }
310
314
  .rogatio-editor [data-project-actions] {
311
315
  display: flex;
312
316
  flex-wrap: wrap;
313
317
  gap: 0.5rem;
314
318
  margin-top: 0.75rem;
315
319
  }
316
- .rogatio-editor [data-editor-command-bar] {
317
- display: flex;
318
- flex-wrap: wrap;
319
- align-items: center;
320
- justify-content: space-between;
321
- gap: 0.75rem 1rem;
322
- margin-block: 1rem;
323
- padding: 0.65rem 0.75rem;
324
- border: 1px solid var(--rogatio-border);
325
- border-radius: var(--rogatio-radius);
326
- background: var(--rogatio-surface-raised);
327
- }
328
320
  .rogatio-editor [data-action-cluster] {
329
321
  display: flex;
330
322
  flex-wrap: wrap;
@@ -708,6 +700,25 @@
708
700
  .rogatio-editor [data-search-wrap] {
709
701
  position: relative;
710
702
  }
703
+ .rogatio-editor [data-rail-tools] {
704
+ display: flex;
705
+ flex: 0 1 auto;
706
+ align-items: flex-end;
707
+ justify-content: flex-end;
708
+ gap: 0.65rem;
709
+ margin-left: auto;
710
+ min-width: 0;
711
+ max-width: 100%;
712
+ }
713
+ .rogatio-editor [data-rail-tools] [data-route=test] {
714
+ flex: none;
715
+ margin-bottom: 0.05rem;
716
+ }
717
+ .rogatio-editor [data-rail-tools] [data-route=test][aria-expanded=true] {
718
+ border-color: rgba(46, 196, 182, 0.55);
719
+ background: rgba(46, 196, 182, 0.14);
720
+ color: var(--rogatio-text);
721
+ }
711
722
  .rogatio-editor [data-search-label] {
712
723
  display: flex;
713
724
  flex-direction: column;
@@ -755,7 +766,10 @@
755
766
  display: block;
756
767
  }
757
768
  .rogatio-editor [data-editor-header] {
758
- grid-template-columns: 1fr;
769
+ align-items: flex-start;
770
+ }
771
+ .rogatio-editor [data-editor-header-actions] {
772
+ width: 100%;
759
773
  }
760
774
  .rogatio-editor [data-rule-heading],
761
775
  .rogatio-editor [data-group-heading],
@@ -805,7 +819,8 @@
805
819
  .rogatio-editor [data-group-row],
806
820
  .rogatio-editor [data-editor-check-group],
807
821
  .rogatio-editor [data-action-dock],
808
- .rogatio-editor [data-action-cluster=danger] {
822
+ .rogatio-editor [data-action-cluster=danger],
823
+ .rogatio-editor [data-test-dialog] {
809
824
  border-color: ButtonText;
810
825
  background: Canvas;
811
826
  }
@@ -828,7 +843,85 @@
828
843
  transition: none;
829
844
  }
830
845
  }
831
- .rogatio-editor [data-test-description],
846
+ .rogatio-editor [data-editor-confirmation] {
847
+ z-index: 4;
848
+ }
849
+ .rogatio-editor [data-test-console] {
850
+ position: fixed;
851
+ inset: 0;
852
+ z-index: 3;
853
+ display: grid;
854
+ place-items: center;
855
+ padding: clamp(0.75rem, 3vw, 2rem);
856
+ background: rgb(6 8 12 / 72%);
857
+ backdrop-filter: blur(8px);
858
+ }
859
+ .rogatio-editor [data-test-dialog] {
860
+ display: grid;
861
+ grid-template-rows: auto minmax(0, 1fr);
862
+ width: min(68rem, 100%);
863
+ max-height: min(86vh, 52rem);
864
+ overflow: hidden;
865
+ border: 1px solid var(--rogatio-border);
866
+ border-top: 3px solid var(--rogatio-card-rule-edge);
867
+ border-radius: var(--rogatio-radius);
868
+ background:
869
+ linear-gradient(
870
+ 180deg,
871
+ rgba(46, 196, 182, 0.08) 0%,
872
+ transparent 4.5rem),
873
+ var(--rogatio-surface-raised);
874
+ box-shadow: 0 28px 80px rgb(0 0 0 / 55%);
875
+ }
876
+ .rogatio-editor [data-test-dialog-head] {
877
+ display: flex;
878
+ align-items: flex-start;
879
+ justify-content: space-between;
880
+ gap: 1rem;
881
+ padding: 1.1rem 1.25rem 0.9rem;
882
+ border-bottom: 1px solid var(--rogatio-border);
883
+ }
884
+ .rogatio-editor [data-test-dialog-head] h2 {
885
+ margin: 0;
886
+ font-size: 1.65rem;
887
+ letter-spacing: -0.03em;
888
+ }
889
+ .rogatio-editor [data-test-kicker] {
890
+ margin: 0 0 0.2rem;
891
+ color: var(--rogatio-card-rule-edge);
892
+ font-family: var(--rogatio-font-mono);
893
+ font-size: 0.6875rem;
894
+ font-weight: 700;
895
+ letter-spacing: 0.12em;
896
+ text-transform: uppercase;
897
+ }
898
+ .rogatio-editor [data-test-dialog] [data-test-description] {
899
+ margin: 0.35rem 0 0;
900
+ max-width: 40rem;
901
+ color: var(--rogatio-muted);
902
+ }
903
+ .rogatio-editor [data-test-dialog-body] {
904
+ overflow: auto;
905
+ padding: 1.1rem 1.25rem 1.35rem;
906
+ }
907
+ .rogatio-editor [data-test-status] {
908
+ margin: 0;
909
+ color: var(--rogatio-muted);
910
+ }
911
+ @media (min-width: 52rem) {
912
+ .rogatio-editor [data-test-dialog] [data-test-panel] {
913
+ grid-template-columns: minmax(0, 1.45fr) minmax(16rem, 0.7fr);
914
+ align-items: start;
915
+ }
916
+ .rogatio-editor [data-test-dialog] [data-test-panel] > fieldset {
917
+ grid-row: 1 / span 3;
918
+ }
919
+ }
920
+ @media (prefers-reduced-motion: reduce) {
921
+ .rogatio-editor [data-test-console] {
922
+ backdrop-filter: none;
923
+ }
924
+ }
832
925
  .rogatio-editor [data-test-checking] {
833
926
  color: var(--rogatio-muted);
834
927
  margin-block: 0 1rem;
@@ -2517,12 +2517,12 @@ var EditorControllerImpl = class {
2517
2517
  extensions;
2518
2518
  instanceId;
2519
2519
  host;
2520
+ layout;
2520
2521
  rail;
2521
2522
  main;
2522
2523
  header;
2523
2524
  status;
2524
2525
  summary;
2525
- commandBar;
2526
2526
  form;
2527
2527
  searchResults;
2528
2528
  draft;
@@ -2540,6 +2540,7 @@ var EditorControllerImpl = class {
2540
2540
  extensionCleanups = [];
2541
2541
  confirmation;
2542
2542
  groupPickerOpen = false;
2543
+ testConsoleOpen = false;
2543
2544
  projectActions;
2544
2545
  focusRequest;
2545
2546
  saving = false;
@@ -2553,6 +2554,7 @@ var EditorControllerImpl = class {
2553
2554
  testResourceType = "main_frame";
2554
2555
  testResult = void 0;
2555
2556
  testRunning = false;
2557
+ testNotice = "";
2556
2558
  testRequestId = 0;
2557
2559
  aiAssistPanel = null;
2558
2560
  aiAssistInFlight = false;
@@ -2597,8 +2599,8 @@ var EditorControllerImpl = class {
2597
2599
  this.host = this.document.createElement("div");
2598
2600
  this.host.className = "rogatio-editor";
2599
2601
  this.host.dataset.rogatioEditor = "true";
2600
- const layout = this.document.createElement("div");
2601
- layout.dataset.editorLayout = "true";
2602
+ this.layout = this.document.createElement("div");
2603
+ this.layout.dataset.editorLayout = "true";
2602
2604
  this.rail = this.document.createElement("nav");
2603
2605
  this.rail.dataset.desktopRouteRail = "true";
2604
2606
  this.rail.setAttribute("aria-label", "Project sections");
@@ -2613,10 +2615,6 @@ var EditorControllerImpl = class {
2613
2615
  this.summary = this.document.createElement("section");
2614
2616
  this.summary.dataset.editorSummary = "true";
2615
2617
  this.summary.setAttribute("role", "alert");
2616
- this.commandBar = this.document.createElement("div");
2617
- this.commandBar.dataset.editorCommandBar = "true";
2618
- this.commandBar.setAttribute("role", "toolbar");
2619
- this.commandBar.setAttribute("aria-label", "Project actions");
2620
2618
  this.form = this.document.createElement("form");
2621
2619
  this.form.dataset.editorForm = "true";
2622
2620
  this.form.noValidate = true;
@@ -2631,15 +2629,9 @@ var EditorControllerImpl = class {
2631
2629
  this.searchQuery = "";
2632
2630
  this.render();
2633
2631
  });
2634
- this.main.append(
2635
- this.header,
2636
- this.status,
2637
- this.summary,
2638
- this.commandBar,
2639
- this.form
2640
- );
2641
- layout.append(this.rail, this.main);
2642
- this.host.append(layout);
2632
+ this.main.append(this.header, this.status, this.summary, this.form);
2633
+ this.layout.append(this.rail, this.main);
2634
+ this.host.append(this.layout);
2643
2635
  this.root.append(this.host);
2644
2636
  this.host.addEventListener("click", this.handleClick);
2645
2637
  this.host.addEventListener("input", this.handleInput);
@@ -2688,6 +2680,10 @@ var EditorControllerImpl = class {
2688
2680
  if (this.destroyed) return;
2689
2681
  const target = event.target;
2690
2682
  if (!(target instanceof Element)) return;
2683
+ if (this.testConsoleOpen && target instanceof HTMLElement && target.dataset.testConsole !== void 0 && event.target === target) {
2684
+ this.closeTestConsole();
2685
+ return;
2686
+ }
2691
2687
  if (this.groupPickerOpen && target instanceof HTMLElement && target.dataset.groupPicker !== void 0 && event.target === target) {
2692
2688
  this.closeGroupPicker();
2693
2689
  return;
@@ -2804,6 +2800,14 @@ var EditorControllerImpl = class {
2804
2800
  this.closeGroupPicker();
2805
2801
  return;
2806
2802
  }
2803
+ if (event.key === "Escape" && this.testConsoleOpen) {
2804
+ event.preventDefault();
2805
+ this.closeTestConsole();
2806
+ return;
2807
+ }
2808
+ if (event.key === "Tab" && this.testConsoleOpen && !this.confirmation) {
2809
+ this.trapTestConsoleFocus(event);
2810
+ }
2807
2811
  const target = event.target;
2808
2812
  const inRenameInput = target instanceof HTMLElement && target.dataset.renameInput !== void 0;
2809
2813
  if (!inRenameInput) return;
@@ -2951,6 +2955,7 @@ var EditorControllerImpl = class {
2951
2955
  return;
2952
2956
  }
2953
2957
  if (command === "test:open-rule") {
2958
+ this.testConsoleOpen = false;
2954
2959
  this.navigateToRule(element.dataset.groupId, element.dataset.ruleId);
2955
2960
  return;
2956
2961
  }
@@ -2968,6 +2973,10 @@ var EditorControllerImpl = class {
2968
2973
  }
2969
2974
  if (command === "close-group-picker") {
2970
2975
  this.closeGroupPicker();
2976
+ return;
2977
+ }
2978
+ if (command === "close-test-console") {
2979
+ this.closeTestConsole();
2971
2980
  }
2972
2981
  }
2973
2982
  collectDiagnostics(value) {
@@ -3697,11 +3706,13 @@ var EditorControllerImpl = class {
3697
3706
  showDryRunDiagnostics(diagnostics) {
3698
3707
  this.testResult = void 0;
3699
3708
  this.testRunning = false;
3709
+ this.testNotice = "";
3700
3710
  this.errors = [...diagnostics];
3701
3711
  const count = diagnostics.length;
3702
3712
  this.statusMessage = count === 0 ? "The project could not be tested." : `${count} validation error${count === 1 ? "" : "s"} found.`;
3703
3713
  const first = diagnostics[0];
3704
3714
  if (first) {
3715
+ this.testConsoleOpen = false;
3705
3716
  this.navigateToPath(first.path);
3706
3717
  return;
3707
3718
  }
@@ -3709,27 +3720,27 @@ var EditorControllerImpl = class {
3709
3720
  }
3710
3721
  async runTest() {
3711
3722
  if (!this.options.dryRun) {
3712
- this.statusMessage = "Dry-run is not configured for this editor instance.";
3723
+ this.testNotice = "Dry-run is not configured for this editor instance.";
3713
3724
  this.render();
3714
3725
  return;
3715
3726
  }
3716
3727
  if (this.ruleCount() === 0) {
3717
3728
  this.testResult = void 0;
3718
3729
  this.testRunning = false;
3719
- this.statusMessage = "This project has no rules to test.";
3730
+ this.testNotice = "";
3720
3731
  this.render();
3721
3732
  return;
3722
3733
  }
3723
3734
  const lines = this.testLines();
3724
3735
  if (lines.length === 0) {
3725
- this.statusMessage = "Please enter at least one URL to test.";
3736
+ this.testNotice = "Please enter at least one URL to test.";
3726
3737
  this.render();
3727
3738
  return;
3728
3739
  }
3729
3740
  if (lines.length > TEST_CASE_LIMIT) {
3730
3741
  this.testResult = void 0;
3731
3742
  this.testRunning = false;
3732
- this.statusMessage = "Only 256 URLs can be checked at once.";
3743
+ this.testNotice = "Only 256 URLs can be checked at once.";
3733
3744
  this.render();
3734
3745
  return;
3735
3746
  }
@@ -3743,7 +3754,7 @@ var EditorControllerImpl = class {
3743
3754
  });
3744
3755
  const requestId = ++this.testRequestId;
3745
3756
  this.testRunning = true;
3746
- this.statusMessage = "Checking URLs...";
3757
+ this.testNotice = "";
3747
3758
  this.render();
3748
3759
  try {
3749
3760
  const outcome = await this.options.dryRun(this.getDraft(), cases);
@@ -3752,7 +3763,7 @@ var EditorControllerImpl = class {
3752
3763
  if (isDryRunResult(outcome)) {
3753
3764
  this.testResult = outcome;
3754
3765
  const checked = outcome.summary.urlCount;
3755
- 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"}.`;
3766
+ this.testNotice = outcome.errors.length > 0 ? `Checked ${checked} URL${checked === 1 ? "" : "s"}. Some lines are not http(s) URLs.` : `Checked ${checked} URL${checked === 1 ? "" : "s"}.`;
3756
3767
  this.render();
3757
3768
  return;
3758
3769
  }
@@ -3765,12 +3776,12 @@ var EditorControllerImpl = class {
3765
3776
  this.testResult = void 0;
3766
3777
  this.testRunning = false;
3767
3778
  const message = e instanceof Error ? e.message : "Test failed";
3768
- this.statusMessage = `Test error: ${message}`;
3779
+ this.testNotice = `Test error: ${message}`;
3769
3780
  this.render();
3770
3781
  }
3771
3782
  }
3772
- renderTestResults(result) {
3773
- const resultsSection = this.host.querySelector(
3783
+ renderTestResults(result, scope = this.host) {
3784
+ const resultsSection = scope.querySelector(
3774
3785
  "[data-test-results]"
3775
3786
  );
3776
3787
  if (!resultsSection || !isDryRunResult(result)) return;
@@ -3946,16 +3957,19 @@ var EditorControllerImpl = class {
3946
3957
  return enablement.isEnabled(id) !== true;
3947
3958
  }
3948
3959
  navigate(route, groupId) {
3960
+ if (route === "test") {
3961
+ this.openTestConsole();
3962
+ return;
3963
+ }
3949
3964
  const returnFocusToGroups = this.groupPickerOpen;
3950
3965
  if (route === "project") {
3951
3966
  this.route = { kind: "project" };
3952
- } else if (route === "test") {
3953
- this.route = { kind: "test" };
3954
3967
  } else if (route === "group" && groupId && this.groupById(groupId)) {
3955
3968
  this.route = { kind: "group", groupId };
3956
3969
  } else {
3957
3970
  return;
3958
3971
  }
3972
+ this.testConsoleOpen = false;
3959
3973
  this.groupPickerOpen = false;
3960
3974
  if (returnFocusToGroups) this.focusRequest = "route:groups";
3961
3975
  this.closeRenameOnLeave();
@@ -3984,6 +3998,7 @@ var EditorControllerImpl = class {
3984
3998
  }
3985
3999
  navigateToGroup(groupId) {
3986
4000
  this.groupPickerOpen = false;
4001
+ this.testConsoleOpen = false;
3987
4002
  const groupIds = new Set(
3988
4003
  this.draft.groups.map((group) => typeof group.id === "string" ? group.id : "").filter((id) => id.length > 0)
3989
4004
  );
@@ -4057,6 +4072,7 @@ var EditorControllerImpl = class {
4057
4072
  }
4058
4073
  navigateToSearchResult(path) {
4059
4074
  this.groupPickerOpen = false;
4075
+ this.testConsoleOpen = false;
4060
4076
  const segments = decodePointer(path);
4061
4077
  if (segments?.[0] !== "groups") {
4062
4078
  this.route = { kind: "project" };
@@ -4081,6 +4097,7 @@ var EditorControllerImpl = class {
4081
4097
  const segments = decodePointer(path);
4082
4098
  if (segments?.[0] !== "groups") {
4083
4099
  this.route = { kind: "project" };
4100
+ this.testConsoleOpen = false;
4084
4101
  this.focusRequest = path;
4085
4102
  this.render();
4086
4103
  return;
@@ -4090,6 +4107,7 @@ var EditorControllerImpl = class {
4090
4107
  const group = this.draft.groups[groupIndex];
4091
4108
  if (!group || typeof group.id !== "string") return;
4092
4109
  this.route = { kind: "group", groupId: group.id };
4110
+ this.testConsoleOpen = false;
4093
4111
  const last = segments[segments.length - 1];
4094
4112
  const ruleIndex = arrayIndex(segments[3] ?? "");
4095
4113
  const entityPath = last === "name" ? path : segments.length === 2 ? pointer("groups", groupIndex, "name") : segments.length === 4 && ruleIndex !== void 0 ? pointer("groups", groupIndex, "rules", ruleIndex, "name") : path;
@@ -4134,12 +4152,9 @@ var EditorControllerImpl = class {
4134
4152
  }
4135
4153
  this.renderHeader();
4136
4154
  this.renderRail();
4137
- this.renderCommandBar();
4138
4155
  this.form.replaceChildren();
4139
4156
  if (this.route.kind === "project") {
4140
4157
  this.renderProject();
4141
- } else if (this.route.kind === "test") {
4142
- this.renderTest();
4143
4158
  } else {
4144
4159
  this.renderGroup(this.route.groupId);
4145
4160
  }
@@ -4151,6 +4166,7 @@ var EditorControllerImpl = class {
4151
4166
  if (this.confirmation) this.groupPickerOpen = false;
4152
4167
  this.renderConfirmation();
4153
4168
  this.renderGroupPicker();
4169
+ this.renderTestConsole();
4154
4170
  this.restoreFocus();
4155
4171
  this.focusRequest = void 0;
4156
4172
  }
@@ -4163,7 +4179,13 @@ var EditorControllerImpl = class {
4163
4179
  dirty.dataset.dirtyState = "true";
4164
4180
  dirty.textContent = this.isDirty() ? "Unsaved changes" : "All changes saved";
4165
4181
  titleBlock.append(title, dirty);
4166
- this.header.append(titleBlock);
4182
+ const actions = this.document.createElement("div");
4183
+ actions.dataset.editorHeaderActions = "true";
4184
+ actions.setAttribute("role", "toolbar");
4185
+ actions.setAttribute("aria-label", "Project actions");
4186
+ actions.setAttribute("aria-busy", this.saving ? "true" : "false");
4187
+ this.appendProjectActions(actions);
4188
+ this.header.append(titleBlock, actions);
4167
4189
  this.status.textContent = this.statusMessage;
4168
4190
  this.status.setAttribute("aria-busy", this.saving ? "true" : "false");
4169
4191
  }
@@ -4219,12 +4241,24 @@ var EditorControllerImpl = class {
4219
4241
  search.setAttribute("aria-controls", "rogatio-search-results");
4220
4242
  searchLabel.append(search);
4221
4243
  searchWrap.append(searchLabel, this.searchResults);
4222
- this.rail.append(searchWrap);
4244
+ const railTools = this.document.createElement("div");
4245
+ railTools.dataset.railTools = "true";
4246
+ const testBtn = this.createButton("Console", "route:test");
4247
+ testBtn.dataset.route = "test";
4248
+ testBtn.dataset.editorKey = "route:test";
4249
+ testBtn.dataset.btn = "secondary";
4250
+ testBtn.disabled = this.saving;
4251
+ testBtn.setAttribute("aria-label", "Test console");
4252
+ testBtn.setAttribute("aria-haspopup", "dialog");
4253
+ testBtn.setAttribute(
4254
+ "aria-expanded",
4255
+ this.testConsoleOpen ? "true" : "false"
4256
+ );
4257
+ testBtn.setAttribute("aria-controls", `${this.instanceId}-test-console`);
4258
+ railTools.append(testBtn, searchWrap);
4259
+ this.rail.append(railTools);
4223
4260
  }
4224
- renderCommandBar() {
4225
- this.commandBar.replaceChildren();
4226
- this.commandBar.setAttribute("aria-busy", this.saving ? "true" : "false");
4227
- this.commandBar.setAttribute("aria-label", "Project actions");
4261
+ appendProjectActions(actions) {
4228
4262
  const tools = this.document.createElement("div");
4229
4263
  tools.dataset.actionCluster = "tools";
4230
4264
  if (this.aiAssistPanel) {
@@ -4232,18 +4266,7 @@ var EditorControllerImpl = class {
4232
4266
  this.createCommandButton("AI Assist", "ai-assist", this.saving)
4233
4267
  );
4234
4268
  }
4235
- const testBtn = this.createButton("Test console", "route:test");
4236
- testBtn.dataset.route = "test";
4237
- testBtn.dataset.editorKey = "route:test";
4238
- testBtn.dataset.btn = "secondary";
4239
- testBtn.disabled = this.saving;
4240
- if (this.route.kind === "test") {
4241
- testBtn.setAttribute("aria-current", "page");
4242
- }
4243
- tools.append(
4244
- testBtn,
4245
- this.createCommandButton("Validate", "validate", this.saving)
4246
- );
4269
+ tools.append(this.createCommandButton("Validate", "validate", this.saving));
4247
4270
  const commit = this.document.createElement("div");
4248
4271
  commit.dataset.actionCluster = "commit";
4249
4272
  commit.append(
@@ -4256,7 +4279,7 @@ var EditorControllerImpl = class {
4256
4279
  "primary"
4257
4280
  )
4258
4281
  );
4259
- this.commandBar.append(tools, commit);
4282
+ actions.append(tools, commit);
4260
4283
  }
4261
4284
  createGroupEnableButton(savedGroupId, groupName) {
4262
4285
  const enablement = this.options.groupEnablement;
@@ -4325,27 +4348,7 @@ var EditorControllerImpl = class {
4325
4348
  }
4326
4349
  createProjectActionRow() {
4327
4350
  const actions = this.document.createElement("div");
4328
- const tools = this.document.createElement("div");
4329
- tools.dataset.actionCluster = "tools";
4330
- if (this.aiAssistPanel) {
4331
- tools.append(
4332
- this.createCommandButton("AI Assist", "ai-assist", this.saving)
4333
- );
4334
- }
4335
- tools.append(this.createCommandButton("Validate", "validate", this.saving));
4336
- const commit = this.document.createElement("div");
4337
- commit.dataset.actionCluster = "commit";
4338
- commit.append(
4339
- this.createCommandButton("Cancel", "cancel", this.saving),
4340
- this.createCommandButton(
4341
- "Save",
4342
- "save",
4343
- this.saving || !this.isDirty(),
4344
- {},
4345
- "primary"
4346
- )
4347
- );
4348
- actions.append(tools, commit);
4351
+ this.appendProjectActions(actions);
4349
4352
  return actions;
4350
4353
  }
4351
4354
  appendDockRow(dock, label, actions) {
@@ -4359,9 +4362,9 @@ var EditorControllerImpl = class {
4359
4362
  dock.append(row);
4360
4363
  }
4361
4364
  /**
4362
- * Repeats the actions a long page would otherwise hide: under the last rule
4363
- * on a group page, or under the test results. The project page stays short,
4364
- * so Add group lives on the Groups heading and that route has no ledger.
4365
+ * Repeats the actions a long group page would otherwise hide, under the last
4366
+ * rule. The project page stays short, so Add group lives on the Groups
4367
+ * heading and that route has no ledger.
4365
4368
  */
4366
4369
  renderActionDock() {
4367
4370
  if (this.route.kind === "project") return;
@@ -4539,14 +4542,7 @@ var EditorControllerImpl = class {
4539
4542
  rulesSection.append(list);
4540
4543
  this.form.append(rulesSection);
4541
4544
  }
4542
- renderTest() {
4543
- const heading = this.document.createElement("h2");
4544
- heading.textContent = "Test console";
4545
- this.form.append(heading);
4546
- const description = this.document.createElement("p");
4547
- description.dataset.testDescription = "true";
4548
- description.textContent = "Check whether these URLs match your rules. Nothing is contacted, and nothing is saved.";
4549
- this.form.append(description);
4545
+ renderTestBody(parent) {
4550
4546
  const panel = this.document.createElement("div");
4551
4547
  panel.dataset.testPanel = "true";
4552
4548
  const urlsFieldset = this.document.createElement("fieldset");
@@ -4626,26 +4622,33 @@ var EditorControllerImpl = class {
4626
4622
  );
4627
4623
  runBtn.dataset.testRun = "true";
4628
4624
  panel.append(runBtn);
4629
- this.form.append(panel);
4625
+ parent.append(panel);
4626
+ if (this.testNotice.length > 0 && !this.testRunning && this.testNotice !== "Only 256 URLs can be checked at once.") {
4627
+ const note = this.document.createElement("p");
4628
+ note.dataset.testStatus = "true";
4629
+ note.setAttribute("role", "status");
4630
+ note.textContent = this.testNotice;
4631
+ parent.append(note);
4632
+ }
4630
4633
  const resultsSection = this.document.createElement("section");
4631
4634
  resultsSection.dataset.testResults = "true";
4632
- this.form.append(resultsSection);
4635
+ parent.append(resultsSection);
4633
4636
  if (this.ruleCount() === 0) {
4634
4637
  const empty = this.document.createElement("p");
4635
4638
  empty.dataset.testEmpty = "true";
4636
4639
  empty.textContent = "This project has no rules to test.";
4637
4640
  resultsSection.append(empty);
4638
4641
  } else if (this.testResult) {
4639
- this.renderTestResults(this.testResult);
4642
+ this.renderTestResults(this.testResult, parent);
4640
4643
  } else if (this.testRunning) {
4641
4644
  const pending = this.document.createElement("p");
4642
4645
  pending.dataset.testPending = "true";
4643
4646
  pending.textContent = "Checking URLs...";
4644
4647
  resultsSection.append(pending);
4645
- } else if (this.statusMessage === "Only 256 URLs can be checked at once.") {
4648
+ } else if (this.testNotice === "Only 256 URLs can be checked at once.") {
4646
4649
  const limit = this.document.createElement("p");
4647
4650
  limit.dataset.testLimit = "true";
4648
- limit.textContent = this.statusMessage;
4651
+ limit.textContent = this.testNotice;
4649
4652
  resultsSection.append(limit);
4650
4653
  }
4651
4654
  }
@@ -5602,7 +5605,100 @@ var EditorControllerImpl = class {
5602
5605
  }
5603
5606
  fields.append(group);
5604
5607
  }
5608
+ openTestConsole() {
5609
+ this.groupPickerOpen = false;
5610
+ this.testConsoleOpen = true;
5611
+ this.focusRequest = "test-urls";
5612
+ this.render();
5613
+ }
5614
+ closeTestConsole() {
5615
+ if (this.testRunning) {
5616
+ this.testRequestId += 1;
5617
+ this.testRunning = false;
5618
+ this.testNotice = "";
5619
+ }
5620
+ this.testConsoleOpen = false;
5621
+ this.focusRequest = "route:test";
5622
+ this.render();
5623
+ }
5624
+ trapTestConsoleFocus(event) {
5625
+ const dialog = this.host.querySelector("[data-test-dialog]");
5626
+ if (!(dialog instanceof HTMLElement)) return;
5627
+ const focusable = Array.from(
5628
+ dialog.querySelectorAll(
5629
+ "button, input, select, textarea, summary, a[href]"
5630
+ )
5631
+ ).filter((element) => {
5632
+ if (element.hidden) return false;
5633
+ const closedDetails = element.closest("details:not([open])");
5634
+ if (closedDetails && element !== closedDetails.querySelector("summary")) {
5635
+ return false;
5636
+ }
5637
+ if (element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
5638
+ return !element.disabled;
5639
+ }
5640
+ return true;
5641
+ });
5642
+ const first = focusable[0];
5643
+ const last = focusable[focusable.length - 1];
5644
+ if (!first || !last) return;
5645
+ const active = this.document.activeElement;
5646
+ const inside = active instanceof Node && dialog.contains(active);
5647
+ if (!inside) {
5648
+ event.preventDefault();
5649
+ (event.shiftKey ? last : first).focus();
5650
+ return;
5651
+ }
5652
+ if (event.shiftKey && active === first) {
5653
+ event.preventDefault();
5654
+ last.focus();
5655
+ return;
5656
+ }
5657
+ if (!event.shiftKey && active === last) {
5658
+ event.preventDefault();
5659
+ first.focus();
5660
+ }
5661
+ }
5662
+ renderTestConsole() {
5663
+ this.host.querySelector("[data-test-console]")?.remove();
5664
+ if ("inert" in this.layout) this.layout.inert = this.testConsoleOpen;
5665
+ if (!this.testConsoleOpen) return;
5666
+ const overlay = this.document.createElement("div");
5667
+ overlay.dataset.testConsole = "true";
5668
+ overlay.setAttribute("role", "presentation");
5669
+ const dialog = this.document.createElement("section");
5670
+ dialog.dataset.testDialog = "true";
5671
+ dialog.id = `${this.instanceId}-test-console`;
5672
+ dialog.setAttribute("role", "dialog");
5673
+ dialog.setAttribute("aria-modal", "true");
5674
+ const head = this.document.createElement("div");
5675
+ head.dataset.testDialogHead = "true";
5676
+ const titles = this.document.createElement("div");
5677
+ const kicker = this.document.createElement("p");
5678
+ kicker.dataset.testKicker = "true";
5679
+ kicker.textContent = "Offline match";
5680
+ const heading = this.document.createElement("h2");
5681
+ heading.id = `${this.instanceId}-test-console-title`;
5682
+ heading.textContent = "Test console";
5683
+ dialog.setAttribute("aria-labelledby", heading.id);
5684
+ const description = this.document.createElement("p");
5685
+ description.dataset.testDescription = "true";
5686
+ description.textContent = "Check whether these URLs match your rules. Nothing is contacted, and nothing is saved.";
5687
+ titles.append(kicker, heading, description);
5688
+ const close = this.createButton("Close", "test-console-close");
5689
+ close.dataset.command = "close-test-console";
5690
+ close.dataset.editorKey = "test-console-close";
5691
+ close.dataset.btn = "secondary";
5692
+ head.append(titles, close);
5693
+ const body = this.document.createElement("div");
5694
+ body.dataset.testDialogBody = "true";
5695
+ this.renderTestBody(body);
5696
+ dialog.append(head, body);
5697
+ overlay.append(dialog);
5698
+ this.host.append(overlay);
5699
+ }
5605
5700
  openGroupPicker() {
5701
+ this.testConsoleOpen = false;
5606
5702
  this.groupPickerOpen = true;
5607
5703
  this.focusRequest = this.draft.groups.length > 0 ? "group-picker-first" : "group-picker-close";
5608
5704
  this.render();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.17.0",
3
+ "version": "6.18.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first browser request and response rules — editor host, file verification, test runner, and runtime dispatch.",