v0.2.0:38 — Create-product Cancel button + modal overflow fix

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) <noreply@anthropic.com>
This commit is contained in:
Grant
2026-05-11 22:05:20 -05:00
parent 11e30ffb21
commit 5c7d66dbb2
2 changed files with 26 additions and 14 deletions
+19 -13
View File
@@ -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);' },
+7 -1
View File
@@ -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