mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
style(portal): compact shell and service console
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "b5bf3d15198c6bb317c1446735208077a02ef32a47dca5b012200aba131071ef",
|
||||
"source_hash": "5a37d7a95cfb4c6cfa06bac62785c5638c391bcf52d6472e05c265f548f367e7",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+102
-24
@@ -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
@@ -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">' +
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user