@crvouga/mockingbird-service-mailosaur 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog — @crvouga/mockingbird-service-mailosaur
2
2
 
3
- ## 1.6.0 (2026-09-30)
3
+ ## 1.6.1 (2026-09-30)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
8
+ - size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
9
+ - keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
10
+
11
+ ## 1.6.0 (2026-09-29)
4
12
 
5
13
  ### Features
6
14
 
@@ -2387,6 +2387,50 @@ function bootAdmin(config) {
2387
2387
  if (render)
2388
2388
  await render();
2389
2389
  }
2390
+ const delay = (ms) => new Promise((resolve) => {
2391
+ window.setTimeout(resolve, ms);
2392
+ });
2393
+ const confirmAction = async (button, pending, done, work) => {
2394
+ if (button.dataset.pending === "1")
2395
+ return;
2396
+ const label = button.textContent ?? "";
2397
+ const width = button.offsetWidth;
2398
+ button.dataset.pending = "1";
2399
+ button.classList.add("pressed");
2400
+ button.setAttribute("aria-busy", "true");
2401
+ button.style.minWidth = `${width}px`;
2402
+ button.textContent = pending;
2403
+ try {
2404
+ await work();
2405
+ button.textContent = done;
2406
+ await delay(1e3);
2407
+ } catch (error) {
2408
+ showError(error);
2409
+ } finally {
2410
+ button.textContent = label;
2411
+ button.style.minWidth = "";
2412
+ button.classList.remove("pressed");
2413
+ button.removeAttribute("aria-busy");
2414
+ delete button.dataset.pending;
2415
+ }
2416
+ };
2417
+ document.addEventListener("click", (event) => {
2418
+ const target = event.target;
2419
+ if (!(target instanceof Element))
2420
+ return;
2421
+ const button = target.closest("button");
2422
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
2423
+ return;
2424
+ button.classList.add("pressed");
2425
+ const token = String(Date.now());
2426
+ button.dataset.press = token;
2427
+ window.setTimeout(() => {
2428
+ if (button.dataset.press !== token || button.dataset.pending === "1")
2429
+ return;
2430
+ button.classList.remove("pressed");
2431
+ delete button.dataset.press;
2432
+ }, 450);
2433
+ });
2390
2434
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
2391
2435
  state.namespace = el("namespace", HTMLSelectElement).value;
2392
2436
  refresh().catch(showError);
@@ -2397,7 +2441,7 @@ function bootAdmin(config) {
2397
2441
  refresh().catch(showError);
2398
2442
  });
2399
2443
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
2400
- refresh().catch(showError);
2444
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
2401
2445
  });
2402
2446
  el("collections", HTMLElement).addEventListener("click", (event) => {
2403
2447
  const target = event.target;
@@ -2520,10 +2564,10 @@ function bootAdmin(config) {
2520
2564
  }).then(() => renderFaults()).catch(showError);
2521
2565
  });
2522
2566
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2523
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2567
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2524
2568
  });
2525
2569
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2526
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2570
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2527
2571
  });
2528
2572
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2529
2573
  const target = event.target;
@@ -2638,9 +2682,13 @@ function bootAdmin(config) {
2638
2682
  var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2639
2683
  var adminClientSource = (config) => {
2640
2684
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2685
+ const entry = bootAdmin.name;
2686
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2687
+ throw new Error("admin client cannot call its boot function");
2688
+ }
2641
2689
  return `<script>
2642
2690
  ${clientPrelude}
2643
- bootAdmin(${payload});
2691
+ ${entry}(${payload});
2644
2692
  </script>`;
2645
2693
  };
2646
2694
 
@@ -2854,9 +2902,24 @@ var renderAdminDocument = (service) => {
2854
2902
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2855
2903
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2856
2904
  label.field input, label.field select, .controls input[type="password"] {
2857
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2858
- padding: 7px 8px; min-height: 36px;
2859
- }
2905
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2906
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2907
+ }
2908
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2909
+ the text fields in the same form. Paint it like those fields. */
2910
+ label.field select {
2911
+ -webkit-appearance: none; appearance: none;
2912
+ padding-right: 28px;
2913
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
2914
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2915
+ }
2916
+ .stack > .field > input,
2917
+ .stack > .field > select,
2918
+ .stack > .field > textarea,
2919
+ dialog .field > input,
2920
+ dialog .field > select,
2921
+ dialog .field > textarea { width: 100%; }
2922
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2860
2923
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2861
2924
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2862
2925
  background: var(--bg-elev);
@@ -2864,6 +2927,14 @@ var renderAdminDocument = (service) => {
2864
2927
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2865
2928
  .btn-quiet { background: transparent; }
2866
2929
  .btn-danger { color: var(--err); }
2930
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2931
+ so the press itself has to stay visible. */
2932
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2933
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2934
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2935
+ }
2936
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2937
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2867
2938
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2868
2939
  record or the full nav row would widen the page past the viewport. */
2869
2940
  .layout {
@@ -2952,8 +3023,16 @@ var renderAdminDocument = (service) => {
2952
3023
  input, select, textarea { font-size: 16px; }
2953
3024
  .brand, .vendor, .controls { width: 100%; }
2954
3025
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
2955
- .layout { grid-template-columns: minmax(0, 1fr); }
3026
+ /* The layout fills the screen. A single-column grid stretches every auto
3027
+ row, so the tab bar grew with the leftover height and the buttons floated
3028
+ in the middle of it. The bar stays one row; the page below takes the rest. */
3029
+ .layout {
3030
+ grid-template-columns: minmax(0, 1fr);
3031
+ grid-template-rows: auto minmax(0, 1fr);
3032
+ align-content: start;
3033
+ }
2956
3034
  nav.side {
3035
+ align-self: start; height: auto;
2957
3036
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
2958
3037
  border-right: 0; border-bottom: 1px solid var(--line);
2959
3038
  padding: 8px;
@@ -3821,7 +3900,7 @@ var stateAdminRoutes = (routes) => {
3821
3900
  };
3822
3901
 
3823
3902
  // ../core/dist/version.js
3824
- var PACKAGE_VERSION = true ? "1.6.0" : UNRELEASED_VERSION;
3903
+ var PACKAGE_VERSION = true ? "1.6.1" : UNRELEASED_VERSION;
3825
3904
 
3826
3905
  // ../core/dist/signing.js
3827
3906
  var encoder = new TextEncoder();
@@ -5145,4 +5224,4 @@ export {
5145
5224
  matchesCriteria,
5146
5225
  MailosaurAPI
5147
5226
  };
5148
- //# sourceMappingURL=chunk-AZIUWKU3.js.map
5227
+ //# sourceMappingURL=chunk-K4JQOFF4.js.map