feat(portal): replace sidebar shell with top command bar

This commit is contained in:
rcourtman
2026-03-27 16:03:37 +00:00
parent d545154cc4
commit f489116da5
5 changed files with 143 additions and 145 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "f7b726f8e8d6c01630420d83fad1e59f7947f386a570d1715f0165f958c39410",
"source_hash": "3ee5dcea0783b4085b4a7feb3fd0dcee628b4280042752af29256a685db8a466",
"build_inputs": [
"package.json",
"tsconfig.json",
+38 -37
View File
@@ -232,26 +232,18 @@ header .logout-btn:hover,
line-height: 1.05;
letter-spacing: -0.04em;
}
.portal-frame {
.portal-shell-bar {
display: grid;
grid-template-columns: minmax(220px, 250px) minmax(0, 1fr);
gap: 22px;
align-items: start;
}
.portal-rail {
position: sticky;
top: 88px;
display: flex;
flex-direction: column;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: stretch;
}
.portal-section-nav {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.portal-section-link,
.portal-rail-panel,
.intro-card,
.service-panel,
.account-surface,
@@ -287,30 +279,41 @@ header .logout-btn:hover,
line-height: 1.5;
color: var(--ink-soft);
}
.portal-rail-panel {
border-radius: var(--radius-md);
padding: 20px;
}
.portal-rail-panel h3 {
margin: 8px 0 10px;
font-size: 23px;
line-height: 1.05;
letter-spacing: -0.04em;
}
.portal-rail-panel p {
margin: 0;
color: var(--ink-soft);
line-height: 1.6;
}
.portal-support-link,
.support-link,
.service-panel .helper-text a {
color: var(--accent-strong);
font-weight: 600;
}
.portal-support-link {
display: inline-block;
margin-top: 14px;
.portal-support-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-width: 340px;
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.98);
box-shadow: var(--shadow-sm);
}
.portal-support-copy {
display: flex;
flex-direction: column;
gap: 4px;
}
.portal-support-label {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-muted);
}
.portal-support-text {
max-width: 280px;
font-size: 13px;
line-height: 1.5;
color: var(--ink-soft);
}
.portal-stage {
display: flex;
@@ -896,17 +899,15 @@ header .logout-btn:hover,
background: #6e1414;
}
@media (max-width: 1180px) {
.portal-frame,
.account-management-grid,
.team-management-grid {
grid-template-columns: 1fr;
}
.portal-rail {
position: static;
.portal-shell-bar {
grid-template-columns: 1fr;
}
.portal-section-nav {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
.portal-support-strip {
min-width: 0;
}
.account-overview-rail {
grid-template-columns: 1fr;
+1 -1
View File
@@ -1672,7 +1672,7 @@
var hosted = hasHostedAccounts(accounts);
var serviceHeading = hosted ? "Self-hosted licenses and billing" : "Account services";
var serviceNote = hosted ? "Hosted operations live above. Use these commercial tools for self-hosted licenses, billing, refunds, and privacy actions." : "Use these account tools for self-hosted licenses, billing, refunds, and privacy actions.";
return '<div class="portal-shell">' + renderOverviewBand(accounts) + '<div class="portal-frame"><aside class="portal-rail">' + renderShellNavigation(accounts, context.bootstrap.support_email || "") + '<div class="portal-rail-panel"><div class="account-panel-kicker">Support</div><h3>One account surface</h3><p>This portal should be the place where hosted access and commercial account actions meet. Hosted workspaces sit above. Self-hosted services remain available below.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></aside><div class="portal-stage"><section class="portal-top-section" id="hosted-operations-section"><div class="portal-top-section-header"><div class="account-panel-kicker">' + (hosted ? "Hosted operations" : "Hosted access") + "</div><h2>" + (hosted ? "Run hosted accounts from one place" : "No hosted workspaces are attached yet") + "</h2><p>" + (hosted ? "Use this area for workspace access, fleet operations, hosted billing, and team management." : "This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.") + '</p></div><div id="accounts-root">' + renderAccountsHTML(context) + '</div></section><section class="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-grid"><button class="service-card service-card-button" type="button" id="open-manage-service" data-account-service-action="open-service-panel" data-account-service-panel="manage-service-panel" data-account-service-focus="manage-inline-email"><span class="service-card-kicker">Billing</span><h3>Manage subscriptions</h3><p>Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.</p></button><button class="service-card service-card-button" type="button" id="open-retrieve-service" data-account-service-action="open-service-panel" data-account-service-panel="retrieve-service-panel" data-account-service-focus="retrieve-inline-email"><span class="service-card-kicker">Licenses</span><h3>Retrieve licenses</h3><p>Recover the latest active self-hosted license and invoice link for a commercial email address.</p></button><button class="service-card service-card-button" type="button" id="open-refund-service" data-account-service-action="open-service-panel" data-account-service-panel="refund-service-panel" data-account-service-focus="refund-inline-email"><span class="service-card-kicker">Refunds</span><h3>Refund requests</h3><p>Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.</p></button><button class="service-card service-card-button" type="button" id="open-data-service" data-account-service-action="open-service-panel" data-account-service-panel="data-service-panel" data-account-service-focus="data-export-email"><span class="service-card-kicker">Privacy</span><h3>Data and privacy</h3><p>Request commercial data export or deletion without leaving the account shell.</p></button></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></section></div></div></div>";
return '<div class="portal-shell">' + renderOverviewBand(accounts) + '<div class="portal-shell-bar">' + renderShellNavigation(accounts, context.bootstrap.support_email || "") + '<div class="portal-support-strip"><div class="portal-support-copy"><span class="portal-support-label">Support</span><span class="portal-support-text">Hosted operations and commercial account tools now share one account surface.</span></div><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></div><section class="portal-top-section" id="hosted-operations-section"><div class="portal-top-section-header"><div class="account-panel-kicker">' + (hosted ? "Hosted operations" : "Hosted access") + "</div><h2>" + (hosted ? "Run hosted accounts from one place" : "No hosted workspaces are attached yet") + "</h2><p>" + (hosted ? "Use this area for workspace access, fleet operations, hosted billing, and team management." : "This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.") + '</p></div><div id="accounts-root">' + renderAccountsHTML(context) + '</div></section><section class="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-grid"><button class="service-card service-card-button" type="button" id="open-manage-service" data-account-service-action="open-service-panel" data-account-service-panel="manage-service-panel" data-account-service-focus="manage-inline-email"><span class="service-card-kicker">Billing</span><h3>Manage subscriptions</h3><p>Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.</p></button><button class="service-card service-card-button" type="button" id="open-retrieve-service" data-account-service-action="open-service-panel" data-account-service-panel="retrieve-service-panel" data-account-service-focus="retrieve-inline-email"><span class="service-card-kicker">Licenses</span><h3>Retrieve licenses</h3><p>Recover the latest active self-hosted license and invoice link for a commercial email address.</p></button><button class="service-card service-card-button" type="button" id="open-refund-service" data-account-service-action="open-service-panel" data-account-service-panel="refund-service-panel" data-account-service-focus="refund-inline-email"><span class="service-card-kicker">Refunds</span><h3>Refund requests</h3><p>Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.</p></button><button class="service-card service-card-button" type="button" id="open-data-service" data-account-service-action="open-service-panel" data-account-service-panel="data-service-panel" data-account-service-focus="data-export-email"><span class="service-card-kicker">Privacy</span><h3>Data and privacy</h3><p>Request commercial data export or deletion without leaving the account shell.</p></button></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></section></div>";
}
function renderSignedOutPortalHTML(context) {
var statusHTML = "";
@@ -497,74 +497,71 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
return (
'<div class="portal-shell">' +
renderOverviewBand(accounts) +
'<div class="portal-frame">' +
'<aside class="portal-rail">' +
renderShellNavigation(accounts, context.bootstrap.support_email || '') +
'<div class="portal-rail-panel">' +
'<div class="account-panel-kicker">Support</div>' +
'<h3>One account surface</h3>' +
'<p>This portal should be the place where hosted access and commercial account actions meet. Hosted workspaces sit above. Self-hosted services remain available below.</p>' +
'<a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || '') + '">' + escapeHTML(context.bootstrap.support_email || '') + '</a>' +
'<div class="portal-shell-bar">' +
renderShellNavigation(accounts, context.bootstrap.support_email || '') +
'<div class="portal-support-strip">' +
'<div class="portal-support-copy">' +
'<span class="portal-support-label">Support</span>' +
'<span class="portal-support-text">Hosted operations and commercial account tools now share one account surface.</span>' +
'</div>' +
'</aside>' +
'<div class="portal-stage">' +
'<section class="portal-top-section" id="hosted-operations-section">' +
'<div class="portal-top-section-header">' +
'<div class="account-panel-kicker">' + (hosted ? 'Hosted operations' : 'Hosted access') + '</div>' +
'<h2>' + (hosted ? 'Run hosted accounts from one place' : 'No hosted workspaces are attached yet') + '</h2>' +
'<p>' + (hosted
? 'Use this area for workspace access, fleet operations, hosted billing, and team management.'
: 'This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.') + '</p>' +
'</div>' +
'<div id="accounts-root">' + renderAccountsHTML(context) + '</div>' +
'</section>' +
'<section class="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-grid">' +
'<button class="service-card service-card-button" type="button" id="open-manage-service" data-account-service-action="open-service-panel" data-account-service-panel="manage-service-panel" data-account-service-focus="manage-inline-email">' +
'<span class="service-card-kicker">Billing</span>' +
'<h3>Manage subscriptions</h3>' +
'<p>Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-retrieve-service" data-account-service-action="open-service-panel" data-account-service-panel="retrieve-service-panel" data-account-service-focus="retrieve-inline-email">' +
'<span class="service-card-kicker">Licenses</span>' +
'<h3>Retrieve licenses</h3>' +
'<p>Recover the latest active self-hosted license and invoice link for a commercial email address.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-refund-service" data-account-service-action="open-service-panel" data-account-service-panel="refund-service-panel" data-account-service-focus="refund-inline-email">' +
'<span class="service-card-kicker">Refunds</span>' +
'<h3>Refund requests</h3>' +
'<p>Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-data-service" data-account-service-action="open-service-panel" data-account-service-panel="data-service-panel" data-account-service-focus="data-export-email">' +
'<span class="service-card-kicker">Privacy</span>' +
'<h3>Data and privacy</h3>' +
'<p>Request commercial data export or deletion without leaving the account shell.</p>' +
'</button>' +
'</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>' +
'</section>' +
'<a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || '') + '">' + escapeHTML(context.bootstrap.support_email || '') + '</a>' +
'</div>' +
'</div>' +
'<section class="portal-top-section" id="hosted-operations-section">' +
'<div class="portal-top-section-header">' +
'<div class="account-panel-kicker">' + (hosted ? 'Hosted operations' : 'Hosted access') + '</div>' +
'<h2>' + (hosted ? 'Run hosted accounts from one place' : 'No hosted workspaces are attached yet') + '</h2>' +
'<p>' + (hosted
? 'Use this area for workspace access, fleet operations, hosted billing, and team management.'
: 'This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.') + '</p>' +
'</div>' +
'<div id="accounts-root">' + renderAccountsHTML(context) + '</div>' +
'</section>' +
'<section class="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-grid">' +
'<button class="service-card service-card-button" type="button" id="open-manage-service" data-account-service-action="open-service-panel" data-account-service-panel="manage-service-panel" data-account-service-focus="manage-inline-email">' +
'<span class="service-card-kicker">Billing</span>' +
'<h3>Manage subscriptions</h3>' +
'<p>Open Stripe billing access for existing self-hosted subscriptions without leaving the Pulse Account shell.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-retrieve-service" data-account-service-action="open-service-panel" data-account-service-panel="retrieve-service-panel" data-account-service-focus="retrieve-inline-email">' +
'<span class="service-card-kicker">Licenses</span>' +
'<h3>Retrieve licenses</h3>' +
'<p>Recover the latest active self-hosted license and invoice link for a commercial email address.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-refund-service" data-account-service-action="open-service-panel" data-account-service-panel="refund-service-panel" data-account-service-focus="refund-inline-email">' +
'<span class="service-card-kicker">Refunds</span>' +
'<h3>Refund requests</h3>' +
'<p>Request an immediate self-serve refund for eligible self-hosted purchases with explicit revocation confirmation.</p>' +
'</button>' +
'<button class="service-card service-card-button" type="button" id="open-data-service" data-account-service-action="open-service-panel" data-account-service-panel="data-service-panel" data-account-service-focus="data-export-email">' +
'<span class="service-card-kicker">Privacy</span>' +
'<h3>Data and privacy</h3>' +
'<p>Request commercial data export or deletion without leaving the account shell.</p>' +
'</button>' +
'</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>' +
'</section>' +
'</div>'
);
}
+41 -41
View File
@@ -243,29 +243,20 @@ header .logout-btn:hover,
letter-spacing: -0.04em;
}
.portal-frame {
.portal-shell-bar {
display: grid;
grid-template-columns: minmax(220px, 250px) minmax(0, 1fr);
gap: 22px;
align-items: start;
}
.portal-rail {
position: sticky;
top: 88px;
display: flex;
flex-direction: column;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: stretch;
}
.portal-section-nav {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.portal-section-link,
.portal-rail-panel,
.intro-card,
.service-panel,
.account-surface,
@@ -303,24 +294,6 @@ header .logout-btn:hover,
color: var(--ink-soft);
}
.portal-rail-panel {
border-radius: var(--radius-md);
padding: 20px;
}
.portal-rail-panel h3 {
margin: 8px 0 10px;
font-size: 23px;
line-height: 1.05;
letter-spacing: -0.04em;
}
.portal-rail-panel p {
margin: 0;
color: var(--ink-soft);
line-height: 1.6;
}
.portal-support-link,
.support-link,
.service-panel .helper-text a {
@@ -328,9 +301,38 @@ header .logout-btn:hover,
font-weight: 600;
}
.portal-support-link {
display: inline-block;
margin-top: 14px;
.portal-support-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-width: 340px;
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.98);
box-shadow: var(--shadow-sm);
}
.portal-support-copy {
display: flex;
flex-direction: column;
gap: 4px;
}
.portal-support-label {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-muted);
}
.portal-support-text {
max-width: 280px;
font-size: 13px;
line-height: 1.5;
color: var(--ink-soft);
}
.portal-stage {
@@ -1002,19 +1004,17 @@ header .logout-btn:hover,
.toast.error { background: #6e1414; }
@media (max-width: 1180px) {
.portal-frame,
.account-management-grid,
.team-management-grid {
grid-template-columns: 1fr;
}
.portal-rail {
position: static;
.portal-shell-bar {
grid-template-columns: 1fr;
}
.portal-section-nav {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
.portal-support-strip {
min-width: 0;
}
.account-overview-rail {