From 5c7d66dbb26a3bf38e3971c7505e680e8d807497 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 11 May 2026 22:05:20 -0500 Subject: [PATCH] =?UTF-8?q?v0.2.0:38=20=E2=80=94=20Create-product=20Cancel?= =?UTF-8?q?=20button=20+=20modal=20overflow=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two operator-reported bugs: 1. Create product had no Cancel. Added a secondary Cancel button next to "Create product" — collapses the disclosure without clearing typed input. 2. Edit product modal could grow taller than the viewport when the entitlements catalog had many entries, with no way to scroll. Cause: the modal card lacked max-height + overflow-y. Fixed Edit product specifically, then defensively swept every other dialog card in the admin UI for the same gap. 8 cards that were missing max-height got `max-height:90vh; overflow-y:auto` appended to their style block. Cards that already had the fix (Edit policy, Edit discount code) untouched. 11 modal cards now consistent: tier-cap upgrade, force-delete confirm, value-prompt, generic-confirm, license-issued display, BTCPay-connect, scoped-API-key generate, scoped-API-key show-once, edit-product, edit-policy, edit-discount-code. Co-Authored-By: Claude Opus 4.7 (1M context) --- licensing-service/web/index.html | 32 +++++++++++++++++++------------- startos/versions/v0.2.0.ts | 8 +++++++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/licensing-service/web/index.html b/licensing-service/web/index.html index 2b75934..86322ac 100644 --- a/licensing-service/web/index.html +++ b/licensing-service/web/index.html @@ -562,7 +562,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } const card = el('div', { style: 'background:var(--cream-50); border:1px solid var(--border-1); ' + 'border-radius:12px; max-width:440px; width:100%; padding:28px 26px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'color:var(--gold-700); margin-bottom:8px' }, 'Upgrade required'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:22px; margin:0 0 12px; color:var(--navy-950); letter-spacing:-0.01em;' }, 'You\'ve hit a Creator-tier cap'), @@ -710,7 +710,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } const card = el('div', { style: 'background:var(--cream-50); border:2px solid var(--danger); ' + 'border-radius:12px; max-width:480px; width:100%; padding:28px 26px; ' + - 'box-shadow:0 16px 32px rgba(178,58,58,0.20);', + 'box-shadow:0 16px 32px rgba(178,58,58,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'color:var(--danger); margin-bottom:8px' }, 'Force delete — destructive'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:22px; margin:0 0 12px; color:var(--navy-950); letter-spacing:-0.01em;' }, `Wipe ${kind} "${slug}" and everything tied to it?`), @@ -965,7 +965,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } style: 'background:var(--cream-50); border:1px solid var(--border-1); ' + 'border-radius:12px; max-width:480px; width:100%; padding:24px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'margin-bottom:8px' }, opts.eyebrow || 'Confirm'), el('h3', { @@ -1054,7 +1054,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } style: 'background:var(--cream-50); border:1px solid var(--border-1); ' + 'border-radius:12px; max-width:480px; width:100%; padding:24px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'margin-bottom:8px' }, opts.eyebrow || 'Confirm'), el('h3', { @@ -1552,7 +1552,8 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } const card = el('div', { style: 'background:var(--cream-50); border:1px solid var(--border-1); ' + 'border-radius:12px; max-width:640px; width:100%; padding:24px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); ' + + 'max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'margin-bottom:8px' }, 'Edit product'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:20px; margin:0 0 6px; color:var(--navy-950);' }, p.slug), @@ -1680,10 +1681,11 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } // over) for products. Renders inline above the submit so they // know what to expect before clicking. capPreCheckCard(tierStatus, 'products', 'products'), - el('button', { class: 'btn primary', style: 'margin-top:16px' }, 'Create product').addEventListener - ? null : null, // dummy; the real button is below for clarity + // Create + Cancel row. Cancel collapses the disclosure + // (returns the operator to the products list) without clearing + // typed input — re-expanding picks up where they left off. (() => { - const btn = el('button', { class: 'btn primary', style: 'margin-top:16px' }, 'Create product') + const btn = el('button', { class: 'btn primary' }, 'Create product') btn.addEventListener('click', async () => { const status = el('div', { class: 'muted', style: 'margin-top:8px' }, 'Creating…') create.querySelector('.body').appendChild(status) @@ -1714,7 +1716,11 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } else status.replaceWith(err(e.message)) } }) - return btn + const cancelBtn = el('button', { + class: 'btn secondary', + onclick: () => { create.open = false }, + }, 'Cancel') + return el('div', { style: 'display:flex; gap:10px; margin-top:16px;' }, [btn, cancelBtn]) })(), ].filter(Boolean)), ]) @@ -5086,7 +5092,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } const card = el('div', { style: 'background:var(--cream-50); border:1px solid var(--border-1); ' + 'border-radius:12px; max-width:560px; width:100%; padding:28px 26px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'color:var(--gold-700); margin-bottom:8px' }, 'License issued'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:22px; margin:0 0 6px; color:var(--navy-950); letter-spacing:-0.01em;' }, 'Save the key now'), @@ -5878,7 +5884,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } cancelBtn.addEventListener('click', () => overlay.remove()) const cardEl = el('div', { style: 'background:var(--cream-50); border:1px solid var(--border-1); border-radius:12px; max-width:540px; width:100%; padding:24px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'margin-bottom:8px' }, 'Connect'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:18px; margin:0 0 6px; color:var(--navy-950);' }, @@ -5987,7 +5993,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } }) const cardEl = el('div', { style: 'background:var(--cream-50); border:1px solid var(--border-1); border-radius:12px; max-width:540px; width:100%; padding:24px; ' + - 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 0 0 1px var(--gold-500) inset, 0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'margin-bottom:8px' }, 'New API key'), el('h3', { style: 'font-family:var(--font-display); font-weight:600; font-size:18px; margin:0 0 14px; color:var(--navy-950);' }, @@ -6024,7 +6030,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } closeBtn.addEventListener('click', () => { overlay.remove(); onClose && onClose() }) const cardEl = el('div', { style: 'background:var(--cream-50); border:2px solid var(--gold-500); border-radius:12px; max-width:600px; width:100%; padding:28px 26px; ' + - 'box-shadow:0 16px 32px rgba(14,31,51,0.20);', + 'box-shadow:0 16px 32px rgba(14,31,51,0.20); max-height:90vh; overflow-y:auto;', }, [ el('div', { class: 'eyebrow', style: 'color:var(--gold-700); margin-bottom:8px' }, 'Save this token now'), el('h3', { style: 'font-family:var(--font-display); font-weight:700; font-size:20px; margin:0 0 12px; color:var(--navy-950);' }, diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 87dbba7..703cb1a 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -58,6 +58,12 @@ const RELEASE_NOTES = [ // in RELEASE_NOTES above (the milestone). Subsequent revisions // append here. const ROUTINE_NOTES = [ + '0.2.0:38 — **Admin UI: Create-product Cancel button + modal-overflow fix across all dialogs.** Two operator-reported bugs.', + '', + '**Create product: Cancel button.** The "Create a new product" disclosure had a Create button but no way to back out without scrolling up to the chevron. Added a secondary Cancel button alongside Create — collapses the disclosure (returns to the products list) without clearing typed input, so re-expanding picks up where the operator left off.', + '', + '**Modal overflow fix.** The Edit-product modal could grow taller than the viewport when a product had a long entitlements catalog, leaving the operator unable to scroll to the Save button. Cause: the modal card lacked `max-height` + `overflow-y`. Added `max-height:90vh; overflow-y:auto` to that card AND to every other dialog card in the admin UI (11 modals total — tier-cap upgrade, force-delete confirm, value-prompt, generic-confirm, license-issued display, BTCPay-connect, scoped-API-key generate, scoped-API-key show-once, edit-product, edit-policy, edit-discount-code). Same fix, applied defensively everywhere so this class of bug can\'t recur as content grows.', + '', '0.2.0:37 — **"Limited" → "Limited discount".** Adds the word "discount" to the launch-special remaining-count label so it\'s unambiguous what\'s limited. Without it, a buyer scanning a tier card with a launch ribbon might read "Limited: 10 remaining" as "only 10 licenses left at this tier" rather than "only 10 uses of the discount code left." Both surfaces (buy page tier card + landing-page dynamic card) now render "Limited discount: N remaining". Cosmetic.', '', '0.2.0:36 — **Launch-special remaining count drops the total.** The buy-page tier card\'s "Limited: N of M remaining" line now reads just "Limited: N remaining". The total cap (M) is operator-private — there\'s no upside to exposing initial volume to buyers, and it can make a tier look smaller than the operator wants to signal. Symmetric change in the landing-page dynamic tier-card render. Cosmetic; no API or schema change.', @@ -499,7 +505,7 @@ const ROUTINE_NOTES = [ ].join('\n\n') export const v0_2_0 = VersionInfo.of({ - version: '0.2.0:37', + version: '0.2.0:38', releaseNotes: { en_US: ROUTINE_NOTES }, // No on-disk transformation needed — v0.2.0:0 is a label change. // SQLite-level migrations live separately under