From df052278778d196ef11077d32a1d00f83ded902f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 17:51:42 +0000 Subject: [PATCH] style(portal): compact shell and service console --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 126 ++++++++++++---- internal/cloudcp/portal/dist/portal_app.js | 13 +- .../cloudcp/portal/frontend/src/shell_view.ts | 84 ++++++----- .../cloudcp/portal/frontend/src/styles.css | 136 ++++++++++++++---- 5 files changed, 272 insertions(+), 89 deletions(-) diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index 5582d7b9bc..5b7d62c95d 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "b5bf3d15198c6bb317c1446735208077a02ef32a47dca5b012200aba131071ef", + "source_hash": "5a37d7a95cfb4c6cfa06bac62785c5638c391bcf52d6472e05c265f548f367e7", "build_inputs": [ "package.json", "tsconfig.json", diff --git a/internal/cloudcp/portal/dist/portal_app.css b/internal/cloudcp/portal/dist/portal_app.css index 2e397e3631..f865d526f1 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -172,12 +172,22 @@ header .logout-btn:hover, background: #fbfcfd; box-shadow: var(--shadow-sm); } +.portal-hero-compact { + align-items: center; + padding: 14px 18px; +} .portal-hero-copy { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; max-width: 900px; } +.portal-hero-heading-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} .portal-hero-kicker, .account-panel-kicker, .service-card-kicker, @@ -198,17 +208,30 @@ header .logout-btn:hover, .portal-hero h1, .intro-card h1 { margin: 0; - font-size: clamp(1.75rem, 2.6vw, 2.6rem); - line-height: 0.98; + font-size: clamp(1.55rem, 2.2vw, 2.15rem); + line-height: 1; letter-spacing: -0.045em; } .portal-hero p, .intro-card p { margin: 0; - font-size: 14px; - line-height: 1.55; + font-size: 13px; + line-height: 1.5; color: var(--ink-soft); } +.portal-hero-chip { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + background: #edf5f7; + color: var(--accent-strong); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} .portal-hero-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -219,8 +242,8 @@ header .logout-btn:hover, display: flex; flex-direction: column; justify-content: space-between; - min-height: 80px; - padding: 12px 14px; + min-height: 72px; + padding: 11px 13px; border: 1px solid var(--line); border-radius: var(--radius-md); background: #fcfdff; @@ -233,9 +256,9 @@ header .logout-btn:hover, text-transform: uppercase; } .portal-hero-stat-value { - font-size: 22px; + font-size: 18px; font-weight: 800; - line-height: 1.05; + line-height: 1.2; letter-spacing: -0.04em; } .portal-command-strip, @@ -1112,13 +1135,13 @@ header .logout-btn:hover, color: var(--ink-soft); } .service-action-list { - display: flex; - flex-direction: column; - gap: 10px; + display: grid; + grid-template-columns: 1fr; + gap: 12px; } .service-shell { display: grid; - grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.7fr); + grid-template-columns: minmax(320px, 0.72fr) minmax(0, 1.28fr); gap: 16px; align-items: start; } @@ -1134,6 +1157,9 @@ header .logout-btn:hover, border-radius: var(--radius-md); background: var(--panel-muted); } +.service-brief-card-primary { + background: #fff; +} .service-brief-card h3 { margin: 8px 0 0; font-size: 19px; @@ -1147,14 +1173,32 @@ header .logout-btn:hover, color: var(--ink-soft); } .service-action-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: center; + display: flex; + flex-direction: column; + gap: 12px; padding: 16px 18px; border: 1px solid var(--line); border-radius: var(--radius-md); - background: var(--panel-muted); + background: #fff; +} +.service-action-tags { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.service-action-tag { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 0 10px; + border-radius: 999px; + background: #fff; + color: var(--ink-muted); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } .service-action-copy { display: flex; @@ -1163,8 +1207,8 @@ header .logout-btn:hover, } .service-action-copy h3 { margin: 0; - font-size: 19px; - line-height: 1.08; + font-size: 17px; + line-height: 1.16; letter-spacing: -0.03em; } .service-action-copy p { @@ -1174,6 +1218,37 @@ header .logout-btn:hover, color: var(--ink-soft); max-width: 760px; } +.service-action-highlights { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.service-action-highlight { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + background: #fff; + color: var(--ink-soft); + font-size: 12px; + font-weight: 600; +} +.service-action-footer { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; + margin-top: auto; + padding-top: 4px; + border-top: 1px solid var(--line); +} +.service-action-caption { + max-width: 320px; + font-size: 12px; + line-height: 1.5; + color: var(--ink-muted); +} .service-action-cta { display: flex; align-items: center; @@ -1184,6 +1259,7 @@ header .logout-btn:hover, .service-panel { padding: 22px; border-radius: var(--radius-md); + background: #fff; } .service-panel.visible.auth-panel { display: block; @@ -1351,7 +1427,8 @@ header .logout-btn:hover, .account-management-grid, .team-management-grid, .account-overview-secondary, - .service-shell { + .service-shell, + .service-action-list { grid-template-columns: 1fr; } .portal-shell-layout, @@ -1386,8 +1463,7 @@ header .logout-btn:hover, .workspace-row-main { grid-template-columns: 1fr; } - .workspace-list-toolbar, - .service-action-row { + .workspace-list-toolbar { grid-template-columns: 1fr; } } @@ -1414,7 +1490,9 @@ header .logout-btn:hover, .account-surface-header, .workspace-management-header, .team-management-header, - .service-header { + .service-header, + .service-action-footer, + .service-action-tags { flex-direction: column; align-items: flex-start; } diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 61767cff5d..9a7fcf4d5c 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1676,8 +1676,11 @@ } return 'Checking'; } - function renderServiceActionRow(id, kicker, title, description, panelID, focusID) { - return '
' + kicker + "

" + title + "

" + description + '

'; + function renderServiceActionRow(id, kicker, title, description, panelID, focusID, highlights) { + var highlightHTML = highlights.map(function(item) { + return '' + escapeHTML(item) + ""; + }).join(""); + return '
' + kicker + 'Self-hosted

' + title + "

" + description + '

' + highlightHTML + '
'; } function renderOverviewQuickAction(section, title, copy) { return '"; @@ -1712,8 +1715,8 @@ var hosted = hasHostedAccounts(accounts); var workspaceTotal = countWorkspaces(accounts); var title = hosted ? "Pulse Account" : "Self-hosted Pulse Account"; - var summary = hosted ? "Run hosted operations, operator access, and commercial account services from one control surface." : "Use this account for billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email."; - return '
' + (hosted ? "Hosted access is active on this account." : "No hosted workspace access is attached to this account yet.") + "

" + title + "

" + summary + '

Hosted access' + (hosted ? "Active" : "Not attached") + '
Accounts' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '
Workspaces' + String(workspaceTotal) + "
"; + var summary = hosted ? "Hosted operations, operator access, and commercial account services live in one account console." : "Billing, license recovery, refunds, and privacy actions live here until hosted access is attached to this email."; + return '
' + (hosted ? "Hosted access is active on this account." : "No hosted workspace access is attached to this account yet.") + '

' + title + '

' + (hosted ? "Operator ready" : "Self-hosted only") + "

" + summary + '

Hosted access' + (hosted ? "Active" : "Not attached") + '
Accounts' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '
Workspace fleet' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : "0 workspaces") + "
"; } function shellSectionButton(section, activeSection, title, copy) { return '"; @@ -1802,7 +1805,7 @@ var hostedContent = accounts.map(function(account) { return '
"; }).join(""); - return '
' + renderOverviewBand(accounts) + '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '
' + renderServiceActionRow("open-manage-service", "Billing", "Manage subscriptions", "Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.", "manage-service-panel", "manage-inline-email") + renderServiceActionRow("open-retrieve-service", "Licenses", "Retrieve licenses", "Recover the latest active self-hosted license and invoice link for a commercial email address.", "retrieve-service-panel", "retrieve-inline-email") + renderServiceActionRow("open-refund-service", "Refunds", "Refund requests", "Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.", "refund-service-panel", "refund-inline-email") + renderServiceActionRow("open-data-service", "Privacy", "Data and privacy", "Request commercial data export or deletion without leaving the account shell.", "data-service-panel", "data-export-email") + '

Data and privacy

Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.

Payment-card data stays with Stripe. For Stripe deletion support, contact ' + escapeHTML(context.bootstrap.support_email || "") + '.
' + renderSupportSection(context) + "
"; + return '
' + renderOverviewBand(accounts) + '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

Data and privacy

Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.

Payment-card data stays with Stripe. For Stripe deletion support, contact ' + escapeHTML(context.bootstrap.support_email || "") + '.
' + renderSupportSection(context) + "
"; } function renderSignedOutPortalHTML(context) { var statusHTML = ""; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 082e265bdb..fcc4c328c7 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -108,19 +108,30 @@ function renderServiceActionRow( title: string, description: string, panelID: string, - focusID: string + focusID: string, + highlights: string[] ): string { + var highlightHTML = highlights.map(function(item) { + return '' + escapeHTML(item) + ''; + }).join(''); return ( - '
' + - '
' + + '
' + + '
' + '' + kicker + '' + + 'Self-hosted' + + '
' + + '
' + '

' + title + '

' + '

' + description + '

' + '
' + - '
' + - '' + + '
' + highlightHTML + '
' + + '' + - '
' + '
' ); } @@ -190,14 +201,17 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string { var workspaceTotal = countWorkspaces(accounts); var title = hosted ? 'Pulse Account' : 'Self-hosted Pulse Account'; var summary = hosted - ? 'Run hosted operations, operator access, and commercial account services from one control surface.' - : 'Use this account for billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email.'; + ? 'Hosted operations, operator access, and commercial account services live in one account console.' + : 'Billing, license recovery, refunds, and privacy actions live here until hosted access is attached to this email.'; return ( - '
' + + '
' + '
' + '
' + (hosted ? 'Hosted access is active on this account.' : 'No hosted workspace access is attached to this account yet.') + '
' + - '

' + title + '

' + + '
' + + '

' + title + '

' + + '' + (hosted ? 'Operator ready' : 'Self-hosted only') + '' + + '
' + '

' + summary + '

' + '
' + '
' + @@ -210,8 +224,8 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string { '' + (accounts.length === 1 ? '1 account' : String(accounts.length) + ' accounts') + '' + '
' + '
' + - 'Workspaces' + - '' + String(workspaceTotal) + '' + + 'Workspace fleet' + + '' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : '0 workspaces') + '' + '
' + '
' + '' @@ -685,13 +699,30 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string '
' + serviceNote + '
' + '
' + '
' + - '
' + - '
' + - renderServiceActionRow('open-manage-service', 'Billing', 'Manage subscriptions', 'Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.', 'manage-service-panel', 'manage-inline-email') + - renderServiceActionRow('open-retrieve-service', 'Licenses', 'Retrieve licenses', 'Recover the latest active self-hosted license and invoice link for a commercial email address.', 'retrieve-service-panel', 'retrieve-inline-email') + - renderServiceActionRow('open-refund-service', 'Refunds', 'Refund requests', 'Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.', 'refund-service-panel', 'refund-inline-email') + - renderServiceActionRow('open-data-service', 'Privacy', 'Data and privacy', 'Request commercial data export or deletion without leaving the account shell.', 'data-service-panel', 'data-export-email') + + '' + + '
' + '
' + '
' + '
' + @@ -707,23 +738,6 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string '.
' + '
' + '
' + - '' + '
' + '' + '
' + diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index 85946c285b..a0796483b2 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -171,13 +171,25 @@ header .logout-btn:hover, box-shadow: var(--shadow-sm); } +.portal-hero-compact { + align-items: center; + padding: 14px 18px; +} + .portal-hero-copy { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; max-width: 900px; } +.portal-hero-heading-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + .portal-hero-kicker, .account-panel-kicker, .service-card-kicker, @@ -201,19 +213,33 @@ header .logout-btn:hover, .portal-hero h1, .intro-card h1 { margin: 0; - font-size: clamp(1.75rem, 2.6vw, 2.6rem); - line-height: 0.98; + font-size: clamp(1.55rem, 2.2vw, 2.15rem); + line-height: 1; letter-spacing: -0.045em; } .portal-hero p, .intro-card p { margin: 0; - font-size: 14px; - line-height: 1.55; + font-size: 13px; + line-height: 1.5; color: var(--ink-soft); } +.portal-hero-chip { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + background: #edf5f7; + color: var(--accent-strong); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .portal-hero-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -225,8 +251,8 @@ header .logout-btn:hover, display: flex; flex-direction: column; justify-content: space-between; - min-height: 80px; - padding: 12px 14px; + min-height: 72px; + padding: 11px 13px; border: 1px solid var(--line); border-radius: var(--radius-md); background: #fcfdff; @@ -241,9 +267,9 @@ header .logout-btn:hover, } .portal-hero-stat-value { - font-size: 22px; + font-size: 18px; font-weight: 800; - line-height: 1.05; + line-height: 1.2; letter-spacing: -0.04em; } @@ -1249,14 +1275,14 @@ header .logout-btn:hover, } .service-action-list { - display: flex; - flex-direction: column; - gap: 10px; + display: grid; + grid-template-columns: 1fr; + gap: 12px; } .service-shell { display: grid; - grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.7fr); + grid-template-columns: minmax(320px, 0.72fr) minmax(0, 1.28fr); gap: 16px; align-items: start; } @@ -1275,6 +1301,10 @@ header .logout-btn:hover, background: var(--panel-muted); } +.service-brief-card-primary { + background: #fff; +} + .service-brief-card h3 { margin: 8px 0 0; font-size: 19px; @@ -1290,14 +1320,34 @@ header .logout-btn:hover, } .service-action-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: center; + display: flex; + flex-direction: column; + gap: 12px; padding: 16px 18px; border: 1px solid var(--line); border-radius: var(--radius-md); - background: var(--panel-muted); + background: #fff; +} + +.service-action-tags { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.service-action-tag { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 0 10px; + border-radius: 999px; + background: #fff; + color: var(--ink-muted); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } .service-action-copy { @@ -1308,8 +1358,8 @@ header .logout-btn:hover, .service-action-copy h3 { margin: 0; - font-size: 19px; - line-height: 1.08; + font-size: 17px; + line-height: 1.16; letter-spacing: -0.03em; } @@ -1321,6 +1371,41 @@ header .logout-btn:hover, max-width: 760px; } +.service-action-highlights { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.service-action-highlight { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + background: #fff; + color: var(--ink-soft); + font-size: 12px; + font-weight: 600; +} + +.service-action-footer { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; + margin-top: auto; + padding-top: 4px; + border-top: 1px solid var(--line); +} + +.service-action-caption { + max-width: 320px; + font-size: 12px; + line-height: 1.5; + color: var(--ink-muted); +} + .service-action-cta { display: flex; align-items: center; @@ -1333,6 +1418,7 @@ header .logout-btn:hover, .service-panel { padding: 22px; border-radius: var(--radius-md); + background: #fff; } .service-panel.visible.auth-panel { @@ -1524,7 +1610,8 @@ header .logout-btn:hover, .account-management-grid, .team-management-grid, .account-overview-secondary, - .service-shell { + .service-shell, + .service-action-list { grid-template-columns: 1fr; } @@ -1569,8 +1656,7 @@ header .logout-btn:hover, grid-template-columns: 1fr; } - .workspace-list-toolbar, - .service-action-row { + .workspace-list-toolbar { grid-template-columns: 1fr; } } @@ -1602,7 +1688,9 @@ header .logout-btn:hover, .account-surface-header, .workspace-management-header, .team-management-header, - .service-header { + .service-header, + .service-action-footer, + .service-action-tags { flex-direction: column; align-items: flex-start; }