diff --git a/licensing-service/src/api/buy_page.rs b/licensing-service/src/api/buy_page.rs index 0435773..94b3904 100644 --- a/licensing-service/src/api/buy_page.rs +++ b/licensing-service/src/api/buy_page.rs @@ -305,6 +305,10 @@ h1 {{ 1fr /* row 7: features (fills) */ auto; /* row 8: button */ }} +/* Single-policy products: one centered card at a comfortable width. + Wide enough to read entitlements + marketing bullets clearly without + stretching across the full container. */ +.tiers-1 {{ grid-template-columns:minmax(0, 480px); justify-content:center; }} .tiers-2 {{ grid-template-columns:repeat(2, 1fr); }} .tiers-3 {{ grid-template-columns:repeat(3, 1fr); }} .tiers-4 {{ grid-template-columns:repeat(2, 1fr); }} @@ -1093,19 +1097,23 @@ footer.kfooter a:hover {{ color:var(--navy-900); }} } /// Build the server-rendered tier-picker HTML. Returns an empty string -/// when the product has fewer than 2 public policies (i.e., the existing -/// single-price view is sufficient). +/// only when the product has zero public policies (the bare price-card + +/// form fallback covers that case). For one public policy, we still +/// render a single tier card so the operator-configured entitlements +/// and marketing bullets surface — without this, single-tier products +/// showed only price + form, eating the operator's tier copy. fn render_tier_picker( policies: &[crate::models::Policy], initial: &Option, product: &crate::models::Product, featured_by_policy: &std::collections::HashMap, ) -> String { - if policies.len() < 2 { + if policies.is_empty() { return String::new(); } let n = policies.len().min(4); let class_n = match n { + 1 => "tiers-1", 2 => "tiers-2", 3 => "tiers-3", _ => "tiers-4", diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 703cb1a..17a8b7b 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -58,6 +58,8 @@ const RELEASE_NOTES = [ // in RELEASE_NOTES above (the milestone). Subsequent revisions // append here. const ROUTINE_NOTES = [ + '0.2.0:39 — **Buy page now renders a tier card for single-public-policy products.** Previously the tier picker only rendered when a product had two or more public policies; single-public-policy products fell back to a bare price card + form, swallowing all the operator-configured entitlements, marketing bullets, and tier descriptions. Fixed: render a single centered tier card (new `.tiers-1` grid class, ~480px max-width) whenever there\'s at least one public policy. Operators who keep most tiers private and only expose one (e.g. "Pro" public, "Core" and "Max" admin-only) now see the same rich tier-card render that multi-tier products get. The price card below still renders unchanged as the buy-confirmation summary.', + '', '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.', @@ -505,7 +507,7 @@ const ROUTINE_NOTES = [ ].join('\n\n') export const v0_2_0 = VersionInfo.of({ - version: '0.2.0:38', + version: '0.2.0:39', releaseNotes: { en_US: ROUTINE_NOTES }, // No on-disk transformation needed — v0.2.0:0 is a label change. // SQLite-level migrations live separately under