style(portal): compact shell and service console

This commit is contained in:
rcourtman
2026-03-27 17:51:42 +00:00
parent 0ccc18cd4c
commit df05227877
5 changed files with 272 additions and 89 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "b5bf3d15198c6bb317c1446735208077a02ef32a47dca5b012200aba131071ef",
"source_hash": "5a37d7a95cfb4c6cfa06bac62785c5638c391bcf52d6472e05c265f548f367e7",
"build_inputs": [
"package.json",
"tsconfig.json",
+102 -24
View File
@@ -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;
}
+8 -5
View File
@@ -1676,8 +1676,11 @@
}
return '<span class="badge badge-checking">Checking</span>';
}
function renderServiceActionRow(id, kicker, title, description, panelID, focusID) {
return '<div class="service-action-row"><div class="service-action-copy"><span class="service-card-kicker">' + kicker + "</span><h3>" + title + "</h3><p>" + description + '</p></div><div class="service-action-cta"><button class="btn-secondary service-action-button" type="button" id="' + id + '" data-account-service-action="open-service-panel" data-account-service-panel="' + panelID + '" data-account-service-focus="' + focusID + '" data-shell-target="services">Open</button></div></div>';
function renderServiceActionRow(id, kicker, title, description, panelID, focusID, highlights) {
var highlightHTML = highlights.map(function(item) {
return '<span class="service-action-highlight">' + escapeHTML(item) + "</span>";
}).join("");
return '<article class="service-action-row"><div class="service-action-tags"><span class="service-card-kicker">' + kicker + '</span><span class="service-action-tag">Self-hosted</span></div><div class="service-action-copy"><h3>' + title + "</h3><p>" + description + '</p></div><div class="service-action-highlights">' + highlightHTML + '</div><div class="service-action-footer"><div class="service-action-caption">Open the dedicated account flow for this request.</div><div class="service-action-cta"><button class="btn-secondary service-action-button" type="button" id="' + id + '" data-account-service-action="open-service-panel" data-account-service-panel="' + panelID + '" data-account-service-focus="' + focusID + '" data-shell-target="services">Open</button></div></div></article>';
}
function renderOverviewQuickAction(section, title, copy) {
return '<button class="overview-quick-action" type="button" data-shell-action="activate-section" data-shell-section="' + section + '"><span class="overview-quick-action-title">' + title + '</span><span class="overview-quick-action-copy">' + copy + "</span></button>";
@@ -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 '<section class="portal-hero"><div class="portal-hero-copy"><div class="portal-hero-kicker">' + (hosted ? "Hosted access is active on this account." : "No hosted workspace access is attached to this account yet.") + "</div><h1>" + title + "</h1><p>" + summary + '</p></div><div class="portal-hero-stats"><div class="portal-hero-stat"><span class="portal-hero-stat-label">Hosted access</span><span class="portal-hero-stat-value">' + (hosted ? "Active" : "Not attached") + '</span></div><div class="portal-hero-stat"><span class="portal-hero-stat-label">Accounts</span><span class="portal-hero-stat-value">' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '</span></div><div class="portal-hero-stat"><span class="portal-hero-stat-label">Workspaces</span><span class="portal-hero-stat-value">' + String(workspaceTotal) + "</span></div></div></section>";
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 '<section class="portal-hero portal-hero-compact"><div class="portal-hero-copy"><div class="portal-hero-kicker">' + (hosted ? "Hosted access is active on this account." : "No hosted workspace access is attached to this account yet.") + '</div><div class="portal-hero-heading-row"><h1>' + title + '</h1><span class="portal-hero-chip">' + (hosted ? "Operator ready" : "Self-hosted only") + "</span></div><p>" + summary + '</p></div><div class="portal-hero-stats"><div class="portal-hero-stat"><span class="portal-hero-stat-label">Hosted access</span><span class="portal-hero-stat-value">' + (hosted ? "Active" : "Not attached") + '</span></div><div class="portal-hero-stat"><span class="portal-hero-stat-label">Accounts</span><span class="portal-hero-stat-value">' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '</span></div><div class="portal-hero-stat"><span class="portal-hero-stat-label">Workspace fleet</span><span class="portal-hero-stat-value">' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : "0 workspaces") + "</span></div></div></section>";
}
function shellSectionButton(section, activeSection, title, copy) {
return '<button class="portal-shell-nav-link' + (activeSection === section ? " active" : "") + '" type="button" data-shell-action="activate-section" data-shell-section="' + section + '"><span class="portal-shell-nav-label">' + title + '</span><span class="portal-shell-nav-copy">' + copy + "</span></button>";
@@ -1802,7 +1805,7 @@
var hostedContent = accounts.map(function(account) {
return '<section class="account-surface"><div class="account-surface-header"><div class="account-heading"><div class="account-eyebrow">' + escapeHTML(accountKindLabel(account)) + "</div><h2>" + escapeHTML(account.name) + '</h2><div class="account-summary">' + escapeHTML(accountKindLabel(account) + " \xB7 " + titleCase(account.role) + " \xB7 " + workspaceCountLabel((account.workspaces || []).length)) + '</div></div><div class="account-badges"><span class="badge badge-' + escapeHTML(account.kind) + '">' + escapeHTML(account.kind_label) + "</span>" + roleBadgeHTML(account.role) + '</div></div><div class="account-surface-body">' + renderAccountOverviewSection(account) + renderAccountWorkspaceSection(account, context.accountAPIBasePath) + renderAccountTeamSection(account) + "</div></section>";
}).join("");
return '<div class="portal-shell" data-shell-section="' + activeSection + '">' + renderOverviewBand(accounts) + '<div class="portal-shell-layout">' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '<div class="portal-shell-main"><section class="portal-content-panel portal-content-panel-overview"><div id="accounts-root">' + hostedContent + '</div></section><section class="portal-content-panel portal-content-panel-services service-section" id="account-services-section"><div class="service-header"><div><div class="account-panel-kicker">Account services</div><h2>' + serviceHeading + '</h2></div><div class="service-note">' + serviceNote + '</div></div><div class="service-shell"><div class="service-shell-main"><div class="service-action-list">' + 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") + '</div><div class="service-panel" id="manage-service-panel"><div id="manage-service-root"></div></div><div class="service-panel" id="retrieve-service-panel"><div id="retrieve-service-root"></div></div><div class="service-panel" id="refund-service-panel"><div id="refund-service-root"></div></div><div class="service-panel" id="data-service-panel"><h3>Data and privacy</h3><p>Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.</p><div class="subsection"><div id="data-export-root"></div></div><div class="subsection"><div id="data-delete-root"></div></div><div class="helper-text">Payment-card data stays with Stripe. For Stripe deletion support, contact <a href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a>.</div></div></div><aside class="service-shell-sidebar"><div class="service-brief-card"><div class="account-panel-kicker">Scope</div><h3>Commercial account actions</h3><p>Use this area for self-hosted billing, license recovery, refunds, and privacy requests. Hosted workspace operations stay in the Workspaces and Team sections.</p></div><div class="service-brief-card"><div class="account-panel-kicker">Support</div><h3>Need help with a commercial request?</h3><p>Use support if billing, refund, privacy, or license actions do not behave as expected for this account.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></aside></div></section><section class="portal-content-panel portal-content-panel-support">' + renderSupportSection(context) + "</section></div></div></div>";
return '<div class="portal-shell" data-shell-section="' + activeSection + '">' + renderOverviewBand(accounts) + '<div class="portal-shell-layout">' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '<div class="portal-shell-main"><section class="portal-content-panel portal-content-panel-overview"><div id="accounts-root">' + hostedContent + '</div></section><section class="portal-content-panel portal-content-panel-services service-section" id="account-services-section"><div class="service-header"><div><div class="account-panel-kicker">Account services</div><h2>' + serviceHeading + '</h2></div><div class="service-note">' + serviceNote + '</div></div><div class="service-shell"><aside class="service-shell-sidebar"><div class="service-brief-card service-brief-card-primary"><div class="account-panel-kicker">Scope</div><h3>Commercial account actions</h3><p>Use this area for self-hosted billing, license recovery, refunds, and privacy requests. Hosted workspace operations stay in the Workspaces and Team sections.</p></div><div class="service-action-list">' + 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", ["Invoices and plan changes", "Subscription self-service"]) + 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", ["Latest active license", "Invoice lookup"]) + 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", ["Eligibility check", "Explicit revocation"]) + 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", ["Export or deletion", "Support escalation path"]) + '</div><div class="service-brief-card"><div class="account-panel-kicker">Support</div><h3>Need help with a commercial request?</h3><p>Use support if billing, refund, privacy, or license actions do not behave as expected for this account.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></aside><div class="service-shell-main"><div class="service-panel" id="manage-service-panel"><div id="manage-service-root"></div></div><div class="service-panel" id="retrieve-service-panel"><div id="retrieve-service-root"></div></div><div class="service-panel" id="refund-service-panel"><div id="refund-service-root"></div></div><div class="service-panel" id="data-service-panel"><h3>Data and privacy</h3><p>Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.</p><div class="subsection"><div id="data-export-root"></div></div><div class="subsection"><div id="data-delete-root"></div></div><div class="helper-text">Payment-card data stays with Stripe. For Stripe deletion support, contact <a href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a>.</div></div></div></div></section><section class="portal-content-panel portal-content-panel-support">' + renderSupportSection(context) + "</section></div></div></div>";
}
function renderSignedOutPortalHTML(context) {
var statusHTML = "";
@@ -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 '<span class="service-action-highlight">' + escapeHTML(item) + '</span>';
}).join('');
return (
'<div class="service-action-row">' +
'<div class="service-action-copy">' +
'<article class="service-action-row">' +
'<div class="service-action-tags">' +
'<span class="service-card-kicker">' + kicker + '</span>' +
'<span class="service-action-tag">Self-hosted</span>' +
'</div>' +
'<div class="service-action-copy">' +
'<h3>' + title + '</h3>' +
'<p>' + description + '</p>' +
'</div>' +
'<div class="service-action-cta">' +
'<button class="btn-secondary service-action-button" type="button" id="' + id + '" data-account-service-action="open-service-panel" data-account-service-panel="' + panelID + '" data-account-service-focus="' + focusID + '" data-shell-target="services">Open</button>' +
'<div class="service-action-highlights">' + highlightHTML + '</div>' +
'<div class="service-action-footer">' +
'<div class="service-action-caption">Open the dedicated account flow for this request.</div>' +
'<div class="service-action-cta">' +
'<button class="btn-secondary service-action-button" type="button" id="' + id + '" data-account-service-action="open-service-panel" data-account-service-panel="' + panelID + '" data-account-service-focus="' + focusID + '" data-shell-target="services">Open</button>' +
'</div>' +
'</div>' +
'</div>'
'</article>'
);
}
@@ -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 (
'<section class="portal-hero">' +
'<section class="portal-hero portal-hero-compact">' +
'<div class="portal-hero-copy">' +
'<div class="portal-hero-kicker">' + (hosted ? 'Hosted access is active on this account.' : 'No hosted workspace access is attached to this account yet.') + '</div>' +
'<h1>' + title + '</h1>' +
'<div class="portal-hero-heading-row">' +
'<h1>' + title + '</h1>' +
'<span class="portal-hero-chip">' + (hosted ? 'Operator ready' : 'Self-hosted only') + '</span>' +
'</div>' +
'<p>' + summary + '</p>' +
'</div>' +
'<div class="portal-hero-stats">' +
@@ -210,8 +224,8 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string {
'<span class="portal-hero-stat-value">' + (accounts.length === 1 ? '1 account' : String(accounts.length) + ' accounts') + '</span>' +
'</div>' +
'<div class="portal-hero-stat">' +
'<span class="portal-hero-stat-label">Workspaces</span>' +
'<span class="portal-hero-stat-value">' + String(workspaceTotal) + '</span>' +
'<span class="portal-hero-stat-label">Workspace fleet</span>' +
'<span class="portal-hero-stat-value">' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : '0 workspaces') + '</span>' +
'</div>' +
'</div>' +
'</section>'
@@ -685,13 +699,30 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
'<div class="service-note">' + serviceNote + '</div>' +
'</div>' +
'<div class="service-shell">' +
'<div class="service-shell-main">' +
'<div class="service-action-list">' +
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') +
'<aside class="service-shell-sidebar">' +
'<div class="service-brief-card service-brief-card-primary">' +
'<div class="account-panel-kicker">Scope</div>' +
'<h3>Commercial account actions</h3>' +
'<p>Use this area for self-hosted billing, license recovery, refunds, and privacy requests. Hosted workspace operations stay in the Workspaces and Team sections.</p>' +
'</div>' +
'<div class="service-action-list">' +
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', ['Invoices and plan changes', 'Subscription self-service']) +
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', ['Latest active license', 'Invoice lookup']) +
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', ['Eligibility check', 'Explicit revocation']) +
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', ['Export or deletion', 'Support escalation path']) +
'</div>' +
'<div class="service-brief-card">' +
'<div class="account-panel-kicker">Support</div>' +
'<h3>Need help with a commercial request?</h3>' +
'<p>Use support if billing, refund, privacy, or license actions do not behave as expected for this account.</p>' +
'<a class="portal-support-link" href="mailto:' +
escapeAttr(context.bootstrap.support_email || '') +
'">' +
escapeHTML(context.bootstrap.support_email || '') +
'</a>' +
'</div>' +
'</aside>' +
'<div class="service-shell-main">' +
'<div class="service-panel" id="manage-service-panel"><div id="manage-service-root"></div></div>' +
'<div class="service-panel" id="retrieve-service-panel"><div id="retrieve-service-root"></div></div>' +
'<div class="service-panel" id="refund-service-panel"><div id="refund-service-root"></div></div>' +
@@ -707,23 +738,6 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
'</a>.</div>' +
'</div>' +
'</div>' +
'<aside class="service-shell-sidebar">' +
'<div class="service-brief-card">' +
'<div class="account-panel-kicker">Scope</div>' +
'<h3>Commercial account actions</h3>' +
'<p>Use this area for self-hosted billing, license recovery, refunds, and privacy requests. Hosted workspace operations stay in the Workspaces and Team sections.</p>' +
'</div>' +
'<div class="service-brief-card">' +
'<div class="account-panel-kicker">Support</div>' +
'<h3>Need help with a commercial request?</h3>' +
'<p>Use support if billing, refund, privacy, or license actions do not behave as expected for this account.</p>' +
'<a class="portal-support-link" href="mailto:' +
escapeAttr(context.bootstrap.support_email || '') +
'">' +
escapeHTML(context.bootstrap.support_email || '') +
'</a>' +
'</div>' +
'</aside>' +
'</div>' +
'</section>' +
'<section class="portal-content-panel portal-content-panel-support">' +
+112 -24
View File
@@ -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;
}