pandorga 1.4.6 → 1.4.7

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.
data/studio/index.html CHANGED
@@ -18,7 +18,7 @@
18
18
  rel="stylesheet"
19
19
  href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css"
20
20
  />
21
- <script type="module" crossorigin src="/studio/assets/index-CMVzh8F1.js"></script>
21
+ <script type="module" crossorigin src="/studio/assets/index-BJyN-kB3.js"></script>
22
22
  <link rel="stylesheet" crossorigin href="/studio/assets/index-Cd1mlY_H.css">
23
23
  </head>
24
24
  <body>
@@ -907,9 +907,20 @@ function pipelineChipMeta(pipelineState) {
907
907
  }
908
908
  }
909
909
 
910
+ function pipelineChipViewKey() {
911
+ const meta = pipelineChipMeta(state.pipeline.state);
912
+ return JSON.stringify([
913
+ meta.className,
914
+ meta.spinning,
915
+ meta.icon,
916
+ meta.label,
917
+ meta.title,
918
+ state.pipeline.runUrl || "",
919
+ ]);
920
+ }
921
+
910
922
  function renderPipelineChip() {
911
923
  const meta = pipelineChipMeta(state.pipeline.state);
912
- const runUrl = state.pipeline.runUrl;
913
924
  const kids = [
914
925
  el("span", {
915
926
  className:
@@ -928,11 +939,13 @@ function renderPipelineChip() {
928
939
  "aria-label": meta.label,
929
940
  role: "status",
930
941
  "data-studio-action": "pipeline-status",
931
- disabled: !runUrl,
942
+ "data-pipeline-key": pipelineChipViewKey(),
943
+ disabled: !state.pipeline.runUrl,
932
944
  onClick: () => {
933
- if (!runUrl) return;
945
+ const url = state.pipeline.runUrl;
946
+ if (!url) return;
934
947
  try {
935
- window.open(runUrl, "_blank", "noopener,noreferrer");
948
+ window.open(url, "_blank", "noopener,noreferrer");
936
949
  } catch {
937
950
  /* ignore */
938
951
  }
@@ -942,6 +955,18 @@ function renderPipelineChip() {
942
955
  );
943
956
  }
944
957
 
958
+ /**
959
+ * Swap the toolbar chip only. renderShell() wipes #app and remounts the
960
+ * editor, so a poll tick would steal focus for the whole pipeline run.
961
+ */
962
+ function syncPipelineChip() {
963
+ const current = document.querySelector("[data-studio-action='pipeline-status']");
964
+ if (!current) return;
965
+ const key = pipelineChipViewKey();
966
+ if (current.getAttribute("data-pipeline-key") === key) return;
967
+ current.replaceWith(renderPipelineChip());
968
+ }
969
+
945
970
  function applyPipelinePayload(data) {
946
971
  state.pipeline.runUrl = data.run?.html_url || state.pipeline.runUrl || "";
947
972
  if (data.sha) state.pipeline.sha = String(data.sha).toLowerCase();
@@ -966,11 +991,11 @@ async function refreshPipelineStatus(sha = null) {
966
991
  try {
967
992
  const data = await studioApi.pipelineStatus(sha || null);
968
993
  applyPipelinePayload(data);
969
- renderShell();
994
+ syncPipelineChip();
970
995
  } catch (err) {
971
996
  state.pipeline.state = "error";
972
997
  state.pipeline.message = String(err.message || err);
973
- renderShell();
998
+ syncPipelineChip();
974
999
  }
975
1000
  }
976
1001
 
@@ -985,7 +1010,7 @@ function armPipelineWatch(commitSha) {
985
1010
  state.pipeline.sha = sha;
986
1011
  state.pipeline.runUrl = "";
987
1012
  state.pipeline.message = "";
988
- renderShell();
1013
+ syncPipelineChip();
989
1014
  void pollPipelineStatus(gen, sha);
990
1015
  }
991
1016
 
@@ -1001,7 +1026,7 @@ async function pollPipelineStatus(gen, sha) {
1001
1026
  if (state.pipeline.state === "running" || state.pipeline.state === "awaiting_run") {
1002
1027
  state.pipeline.state = "stalled";
1003
1028
  state.pipeline.message = "Timed out waiting for the content pipeline.";
1004
- renderShell();
1029
+ syncPipelineChip();
1005
1030
  }
1006
1031
  return;
1007
1032
  }
@@ -1013,15 +1038,15 @@ async function pollPipelineStatus(gen, sha) {
1013
1038
  applyPipelinePayload(data);
1014
1039
 
1015
1040
  if (data.state === "unconfigured" || data.state === "ok" || data.state === "error") {
1016
- renderShell();
1041
+ syncPipelineChip();
1017
1042
  return;
1018
1043
  }
1019
1044
  if (data.state === "awaiting_run" && elapsed > STALL_MS) {
1020
1045
  state.pipeline.state = "stalled";
1021
- renderShell();
1046
+ syncPipelineChip();
1022
1047
  return;
1023
1048
  }
1024
- renderShell();
1049
+ syncPipelineChip();
1025
1050
  } catch (err) {
1026
1051
  if (gen !== state.pipeline.pollGen) return;
1027
1052
  softFails += 1;
@@ -1029,13 +1054,13 @@ async function pollPipelineStatus(gen, sha) {
1029
1054
  if (code === "actions_forbidden" || code === "github_unconfigured") {
1030
1055
  state.pipeline.state = "error";
1031
1056
  state.pipeline.message = String(err.message || err);
1032
- renderShell();
1057
+ syncPipelineChip();
1033
1058
  return;
1034
1059
  }
1035
1060
  if (softFails >= 5) {
1036
1061
  state.pipeline.state = "error";
1037
1062
  state.pipeline.message = String(err.message || err);
1038
- renderShell();
1063
+ syncPipelineChip();
1039
1064
  return;
1040
1065
  }
1041
1066
  }
@@ -1117,6 +1142,61 @@ function toolbarActionButton({
1117
1142
  );
1118
1143
  }
1119
1144
 
1145
+ function localExportPresentation() {
1146
+ const exportBusy = state.reexporting || state.pipeline.state === "syncing";
1147
+ return {
1148
+ exportBusy,
1149
+ className:
1150
+ "btn btn-tool btn-reexport" +
1151
+ (exportBusy ? " pipeline-running" : "") +
1152
+ (state.pipeline.state === "ok" && !exportBusy ? " pipeline-ok" : "") +
1153
+ (state.pipeline.state === "error" && !exportBusy ? " pipeline-error" : ""),
1154
+ title: exportBusy
1155
+ ? "Exporting content JSON locally"
1156
+ : "Export content JSON locally and reload",
1157
+ ariaLabel: exportBusy ? "Exporting locally" : "Export locally",
1158
+ icon: exportBusy ? "progress_activity" : "sync",
1159
+ label: exportBusy ? "Exporting…" : "Export",
1160
+ };
1161
+ }
1162
+
1163
+ /** Patch the local Export button without remounting the editor. */
1164
+ function syncLocalExportButton() {
1165
+ const btn = document.querySelector("[data-studio-action='reexport']");
1166
+ if (!btn) return;
1167
+ const exp = localExportPresentation();
1168
+ btn.className = exp.className;
1169
+ btn.title = exp.title;
1170
+ btn.setAttribute("aria-label", exp.ariaLabel);
1171
+ btn.disabled = exp.exportBusy;
1172
+ const icon = btn.querySelector(".material-symbols-outlined");
1173
+ if (icon) {
1174
+ icon.textContent = exp.icon;
1175
+ icon.classList.toggle("studio-icon-spin", exp.exportBusy);
1176
+ }
1177
+ const label = btn.querySelector(".btn-label");
1178
+ if (label) label.textContent = exp.label;
1179
+ }
1180
+
1181
+ function syncToolbarStatus() {
1182
+ const brand = document.querySelector(".studio-toolbar-brand");
1183
+ if (!brand) return;
1184
+ const statusText = state.error ? "Error" : state.status || "";
1185
+ let pill = brand.querySelector(".studio-toolbar-status");
1186
+ if (!statusText) {
1187
+ if (pill) pill.remove();
1188
+ return;
1189
+ }
1190
+ if (!pill) {
1191
+ pill = el("span", { className: "status-pill studio-toolbar-status" });
1192
+ brand.append(pill);
1193
+ }
1194
+ pill.className =
1195
+ "status-pill studio-toolbar-status" + (state.error ? " error" : "");
1196
+ pill.textContent = statusText;
1197
+ pill.title = state.error || state.errorDetail || state.status || "";
1198
+ }
1199
+
1120
1200
  function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
1121
1201
  const saveLabel = state.saving ? "Saving…" : state.dirty ? "Save" : "Saved";
1122
1202
  const saveIcon = state.saving ? "progress_activity" : state.dirty ? "save" : "check";
@@ -1137,24 +1217,18 @@ function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
1137
1217
 
1138
1218
  // Theme → pipeline/Export → Revert → Save. Pipeline chip is always present.
1139
1219
  if (isLocalDevHost()) {
1140
- const exportBusy = state.reexporting || state.pipeline.state === "syncing";
1220
+ const exp = localExportPresentation();
1141
1221
  actions.push(
1142
1222
  toolbarActionButton({
1143
- className:
1144
- "btn btn-tool btn-reexport" +
1145
- (exportBusy ? " pipeline-running" : "") +
1146
- (state.pipeline.state === "ok" && !exportBusy ? " pipeline-ok" : "") +
1147
- (state.pipeline.state === "error" && !exportBusy ? " pipeline-error" : ""),
1148
- title: exportBusy
1149
- ? "Exporting content JSON locally"
1150
- : "Export content JSON locally and reload",
1151
- ariaLabel: exportBusy ? "Exporting locally" : "Export locally",
1223
+ className: exp.className,
1224
+ title: exp.title,
1225
+ ariaLabel: exp.ariaLabel,
1152
1226
  action: "reexport",
1153
- disabled: exportBusy,
1227
+ disabled: exp.exportBusy,
1154
1228
  onClick: () => reexportLocalContent(),
1155
- icon: exportBusy ? "progress_activity" : "sync",
1156
- label: exportBusy ? "Exporting…" : "Export",
1157
- spinning: exportBusy,
1229
+ icon: exp.icon,
1230
+ label: exp.label,
1231
+ spinning: exp.exportBusy,
1158
1232
  })
1159
1233
  );
1160
1234
  } else {
@@ -1430,6 +1504,21 @@ function renderErrorBanner() {
1430
1504
  ]);
1431
1505
  }
1432
1506
 
1507
+ /** Show or hide the failure banner without rebuilding the editor. */
1508
+ function syncErrorBanner() {
1509
+ const main = document.querySelector(".studio-main");
1510
+ if (!main) return;
1511
+ const existing = main.querySelector(":scope > .studio-error-banner");
1512
+ const next = renderErrorBanner();
1513
+ if (existing && next) existing.replaceWith(next);
1514
+ else if (existing && !next) existing.remove();
1515
+ else if (!existing && next) {
1516
+ const crumb = main.querySelector(".studio-breadcrumb");
1517
+ if (crumb) crumb.before(next);
1518
+ else main.prepend(next);
1519
+ }
1520
+ }
1521
+
1433
1522
  function renderShell() {
1434
1523
  app.innerHTML = "";
1435
1524
  const groups = collectionGroups(state.schema);
@@ -4008,7 +4097,9 @@ async function reexportLocalContent() {
4008
4097
  state.pipeline.message = "";
4009
4098
  state.status = "Reexporting…";
4010
4099
  clearError();
4011
- renderShell();
4100
+ syncToolbarStatus();
4101
+ syncErrorBanner();
4102
+ syncLocalExportButton();
4012
4103
  try {
4013
4104
  const res = await fetch("/__dev/reexport", {
4014
4105
  method: "POST",
@@ -4023,9 +4114,11 @@ async function reexportLocalContent() {
4023
4114
  if (!res.ok || !data.ok) {
4024
4115
  throw new Error((data && data.error) || `Export failed: ${res.status}`);
4025
4116
  }
4117
+ state.reexporting = false;
4026
4118
  state.pipeline.state = "ok";
4027
4119
  state.status = "Reexported — reloading…";
4028
- renderShell();
4120
+ syncToolbarStatus();
4121
+ syncLocalExportButton();
4029
4122
  setTimeout(() => {
4030
4123
  try {
4031
4124
  const url = new URL(window.location.href);
@@ -4036,12 +4129,13 @@ async function reexportLocalContent() {
4036
4129
  }
4037
4130
  }, 200);
4038
4131
  } catch (err) {
4132
+ state.reexporting = false;
4039
4133
  state.pipeline.state = "error";
4040
4134
  state.pipeline.message = String(err.message || err);
4041
4135
  captureError(err);
4042
- renderShell();
4043
- } finally {
4044
- state.reexporting = false;
4136
+ syncToolbarStatus();
4137
+ syncErrorBanner();
4138
+ syncLocalExportButton();
4045
4139
  }
4046
4140
  }
4047
4141
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.6
4
+ version: 1.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -413,6 +413,7 @@ files:
413
413
  - scripts/test/test-studio-content-files.rb
414
414
  - scripts/test/test-studio-liquid-roundtrip.mjs
415
415
  - scripts/test/test-studio-pipeline-config.mjs
416
+ - scripts/test/test-studio-pipeline-focus.mjs
416
417
  - scripts/test/test-studio-rate-limit.mjs
417
418
  - scripts/test/test-studio-rewrite-api-contract.rb
418
419
  - scripts/test/test-studio-rewrite-sanitize.mjs
@@ -450,7 +451,7 @@ files:
450
451
  - studio-app/src/preview/render.js
451
452
  - studio-app/src/styles/studio.css
452
453
  - studio-app/vite.config.js
453
- - studio/assets/index-CMVzh8F1.js
454
+ - studio/assets/index-BJyN-kB3.js
454
455
  - studio/assets/index-Cd1mlY_H.css
455
456
  - studio/index.html
456
457
  - studio/schema.yml