mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 12:15:42 +00:00
style(portal): split workspace and service detail panes
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "5a37d7a95cfb4c6cfa06bac62785c5638c391bcf52d6472e05c265f548f367e7",
|
||||
"source_hash": "b48ed843c8ab211f26d40bdb069e5a6a3f35c206eedef2cf15301ca0f640e8e3",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+64
-2
@@ -885,6 +885,16 @@ header .logout-btn:hover,
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
.workspace-operations-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.9fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.workspace-operations-main,
|
||||
.workspace-operations-detail {
|
||||
min-width: 0;
|
||||
}
|
||||
.workspace-management-panel,
|
||||
.team-management-panel {
|
||||
min-height: 100%;
|
||||
@@ -894,7 +904,7 @@ header .logout-btn:hover,
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.workspace-management-panel {
|
||||
background: var(--panel-muted);
|
||||
background: #fff;
|
||||
}
|
||||
.workspace-management-content {
|
||||
display: grid;
|
||||
@@ -1151,6 +1161,14 @@ header .logout-btn:hover,
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.service-detail-shell {
|
||||
min-height: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.service-brief-card {
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -1180,6 +1198,15 @@ header .logout-btn:hover,
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
transition:
|
||||
border-color 140ms ease,
|
||||
box-shadow 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
.service-action-row:hover,
|
||||
.service-action-row.active {
|
||||
border-color: rgba(15, 109, 115, 0.28);
|
||||
box-shadow: 0 10px 24px rgba(10, 25, 41, 0.08);
|
||||
}
|
||||
.service-action-tags {
|
||||
display: flex;
|
||||
@@ -1260,10 +1287,43 @@ header .logout-btn:hover,
|
||||
padding: 22px;
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
min-height: 100%;
|
||||
}
|
||||
.service-panel.visible.auth-panel {
|
||||
display: block;
|
||||
}
|
||||
.service-panel-empty {
|
||||
display: none;
|
||||
}
|
||||
.service-panel-empty.visible {
|
||||
display: block;
|
||||
}
|
||||
.service-empty-points {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.service-empty-point {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
.service-empty-point strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.service-empty-point span {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.service-section-auth {
|
||||
max-width: 760px;
|
||||
}
|
||||
@@ -1428,7 +1488,9 @@ header .logout-btn:hover,
|
||||
.team-management-grid,
|
||||
.account-overview-secondary,
|
||||
.service-shell,
|
||||
.service-action-list {
|
||||
.service-action-list,
|
||||
.service-empty-points,
|
||||
.workspace-operations-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.portal-shell-layout,
|
||||
|
||||
+14
-3
@@ -80,7 +80,7 @@
|
||||
if (!empty || !content || !title || !meta || !summary || !actionButton || !closeButton) return;
|
||||
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
|
||||
var hasSelection = !!workspace;
|
||||
panel.classList.toggle("visible", hasSelection);
|
||||
panel.classList.add("visible");
|
||||
empty.hidden = hasSelection;
|
||||
content.hidden = !hasSelection;
|
||||
if (!workspace) {
|
||||
@@ -1102,11 +1102,22 @@
|
||||
}
|
||||
function renderOpenPanels(openPanelID) {
|
||||
var panels = ["manage-service-panel", "retrieve-service-panel", "refund-service-panel", "data-service-panel"];
|
||||
var emptyPanel = getElement3("service-panel-empty");
|
||||
if (emptyPanel) {
|
||||
emptyPanel.classList.toggle("visible", !openPanelID);
|
||||
}
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement3(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle("visible", panels[i] === openPanelID);
|
||||
}
|
||||
var serviceButtons = document.querySelectorAll('[data-account-service-action="open-service-panel"]');
|
||||
for (var j = 0; j < serviceButtons.length; j += 1) {
|
||||
var button = serviceButtons[j];
|
||||
var row = button.closest(".service-action-row");
|
||||
if (!row) continue;
|
||||
row.classList.toggle("active", button.getAttribute("data-account-service-panel") === openPanelID);
|
||||
}
|
||||
}
|
||||
function renderRefundPanel(refundState, bootstrap) {
|
||||
var root = getElement3("refund-service-root");
|
||||
@@ -1782,7 +1793,7 @@
|
||||
var workspaceHTML = workspaces.length ? '<div class="workspace-list-wrap"><div class="workspace-list-toolbar"><div class="workspace-list-summary">Review the hosted fleet, open a workspace, and keep lifecycle actions explicit.</div><div class="workspace-list-stats"><span class="workspace-list-stat"><strong>' + String(workspaces.length) + '</strong> total</span><span class="workspace-list-stat"><strong>' + String(healthyCount) + '</strong> healthy</span><span class="workspace-list-stat"><strong>' + String(checkingCount) + '</strong> checking</span><span class="workspace-list-stat workspace-list-stat-attention"><strong>' + String(unhealthyCount) + '</strong> needs attention</span></div></div><div class="workspace-list-head"><span>Workspace</span><span>Health</span><span>Lifecycle</span><span>Actions</span></div><div class="workspace-list">' + workspaces.map(function(workspace) {
|
||||
return renderWorkspaceCard(account, workspace, accountAPIBasePath);
|
||||
}).join("") + "</div></div>" : '<div class="empty-state"><p>No hosted workspaces yet. Create one to get started.</p></div>';
|
||||
return '<section class="account-content-panel account-content-panel-workspaces"><div class="account-stage-header"><div><div class="account-panel-kicker">Workspace fleet</div><h3>Hosted fleet</h3><p>Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.</p></div></div>' + workspaceHTML + '<div class="account-management-grid">' + workspaceManagement + "</div></section>";
|
||||
return '<section class="account-content-panel account-content-panel-workspaces"><div class="account-stage-header"><div><div class="account-panel-kicker">Workspace fleet</div><h3>Hosted fleet</h3><p>Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.</p></div></div><div class="workspace-operations-shell"><div class="workspace-operations-main">' + workspaceHTML + '</div><div class="workspace-operations-detail">' + workspaceManagement + "</div></div></section>";
|
||||
}
|
||||
function renderAccountTeamSection(account) {
|
||||
return '<section class="account-content-panel account-content-panel-team"><section class="team-management-panel team-section team-section-shell" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><div class="team-management-header"><div><div class="account-panel-kicker">Team management</div><h3>Control who can operate this account</h3><p>Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.</p></div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Done</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><div class="team-roster"><div class="team-panel-heading"><h4>People on this account</h4><p>Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.</p></div><div class="team-roster-list" id="team-list-' + escapeAttr(account.id) + '"><div class="team-list-message">Loading\u2026</div></div></div><div class="team-invite-panel"><div class="team-panel-heading"><h4>Invite someone new</h4><p>Add another operator with the minimum role they need for this account.</p></div><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><div class="team-invite-guide"><div class="team-invite-guide-row"><strong>Admin</strong><span>Billing and hosted operations.</span></div><div class="team-invite-guide-row"><strong>Tech</strong><span>Operational access without billing control.</span></div><div class="team-invite-guide-row"><strong>Read-only</strong><span>Review hosted state without making changes.</span></div></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div></div></section></section>';
|
||||
@@ -1805,7 +1816,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"><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>";
|
||||
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-detail-shell"><div class="service-panel service-panel-empty visible" id="service-panel-empty"><div class="account-panel-kicker">Select a service</div><h3>Choose a commercial account action</h3><p>Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one working surface instead of a list of disconnected tools.</p><div class="service-empty-points"><div class="service-empty-point"><strong>Billing</strong><span>Open Stripe customer portal access after verification.</span></div><div class="service-empty-point"><strong>Licenses</strong><span>Recover the latest active self-hosted license and invoice link.</span></div><div class="service-empty-point"><strong>Privacy</strong><span>Request export or deletion without leaving Pulse Account.</span></div></div></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></div></div></section><section class="portal-content-panel portal-content-panel-support">' + renderSupportSection(context) + "</section></div></div></div>";
|
||||
}
|
||||
function renderSignedOutPortalHTML(context) {
|
||||
var statusHTML = "";
|
||||
|
||||
@@ -98,7 +98,7 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
|
||||
|
||||
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
|
||||
var hasSelection = !!workspace;
|
||||
panel.classList.toggle('visible', hasSelection);
|
||||
panel.classList.add('visible');
|
||||
empty.hidden = hasSelection;
|
||||
content.hidden = !hasSelection;
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ describe('services view', function() {
|
||||
|
||||
it('toggles the visible service panel by id', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="service-panel-empty" class="service-panel visible"></div>' +
|
||||
'<article class="service-action-row"><button data-account-service-action="open-service-panel" data-account-service-panel="manage-service-panel"></button></article>' +
|
||||
'<article class="service-action-row"><button data-account-service-action="open-service-panel" data-account-service-panel="retrieve-service-panel"></button></article>' +
|
||||
'<div id="manage-service-panel" class="service-panel"></div>' +
|
||||
'<div id="retrieve-service-panel" class="service-panel"></div>' +
|
||||
'<div id="refund-service-panel" class="service-panel"></div>' +
|
||||
@@ -90,6 +93,8 @@ describe('services view', function() {
|
||||
expect(document.getElementById('retrieve-service-panel')?.classList.contains('visible')).toBe(true);
|
||||
expect(document.getElementById('manage-service-panel')?.classList.contains('visible')).toBe(false);
|
||||
expect(document.getElementById('refund-service-panel')?.classList.contains('visible')).toBe(false);
|
||||
expect(document.getElementById('service-panel-empty')?.classList.contains('visible')).toBe(false);
|
||||
expect(document.querySelectorAll('.service-action-row.active')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders retrieve panel result state with invoice and token metadata', function() {
|
||||
|
||||
@@ -69,11 +69,22 @@ export function renderButton(id: string | undefined, disabled: boolean, label: s
|
||||
|
||||
export function renderOpenPanels(openPanelID: string): void {
|
||||
var panels = ['manage-service-panel', 'retrieve-service-panel', 'refund-service-panel', 'data-service-panel'];
|
||||
var emptyPanel = getElement('service-panel-empty');
|
||||
if (emptyPanel) {
|
||||
emptyPanel.classList.toggle('visible', !openPanelID);
|
||||
}
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle('visible', panels[i] === openPanelID);
|
||||
}
|
||||
var serviceButtons = document.querySelectorAll<HTMLElement>('[data-account-service-action="open-service-panel"]');
|
||||
for (var j = 0; j < serviceButtons.length; j += 1) {
|
||||
var button = serviceButtons[j];
|
||||
var row = button.closest('.service-action-row');
|
||||
if (!row) continue;
|
||||
row.classList.toggle('active', button.getAttribute('data-account-service-panel') === openPanelID);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderRefundPanel(refundState: RefundState, bootstrap: PortalBootstrapData): void {
|
||||
|
||||
@@ -169,6 +169,8 @@ describe('shell view', function() {
|
||||
expect(html).toContain('data-action="workspace-action"');
|
||||
expect(html).toContain('service-action-row');
|
||||
expect(html).toContain('service-action-button');
|
||||
expect(html).toContain('id="service-panel-empty"');
|
||||
expect(html).toContain('Choose a commercial account action');
|
||||
expect(html).toContain('id="open-retrieve-service"');
|
||||
expect(html).toContain('id="data-service-panel"');
|
||||
});
|
||||
|
||||
@@ -525,9 +525,13 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
|
||||
'<p>Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
workspaceHTML +
|
||||
'<div class="account-management-grid">' +
|
||||
workspaceManagement +
|
||||
'<div class="workspace-operations-shell">' +
|
||||
'<div class="workspace-operations-main">' +
|
||||
workspaceHTML +
|
||||
'</div>' +
|
||||
'<div class="workspace-operations-detail">' +
|
||||
workspaceManagement +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</section>'
|
||||
);
|
||||
@@ -723,19 +727,31 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
|
||||
'</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 class="service-detail-shell">' +
|
||||
'<div class="service-panel service-panel-empty visible" id="service-panel-empty">' +
|
||||
'<div class="account-panel-kicker">Select a service</div>' +
|
||||
'<h3>Choose a commercial account action</h3>' +
|
||||
'<p>Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one working surface instead of a list of disconnected tools.</p>' +
|
||||
'<div class="service-empty-points">' +
|
||||
'<div class="service-empty-point"><strong>Billing</strong><span>Open Stripe customer portal access after verification.</span></div>' +
|
||||
'<div class="service-empty-point"><strong>Licenses</strong><span>Recover the latest active self-hosted license and invoice link.</span></div>' +
|
||||
'<div class="service-empty-point"><strong>Privacy</strong><span>Request export or deletion without leaving Pulse Account.</span></div>' +
|
||||
'</div>' +
|
||||
'</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>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
@@ -986,6 +986,18 @@ header .logout-btn:hover,
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.workspace-operations-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.9fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.workspace-operations-main,
|
||||
.workspace-operations-detail {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workspace-management-panel,
|
||||
.team-management-panel {
|
||||
min-height: 100%;
|
||||
@@ -997,7 +1009,7 @@ header .logout-btn:hover,
|
||||
}
|
||||
|
||||
.workspace-management-panel {
|
||||
background: var(--panel-muted);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.workspace-management-content {
|
||||
@@ -1294,6 +1306,15 @@ header .logout-btn:hover,
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.service-detail-shell {
|
||||
min-height: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.service-brief-card {
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -1327,6 +1348,13 @@ header .logout-btn:hover,
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.service-action-row:hover,
|
||||
.service-action-row.active {
|
||||
border-color: rgba(15, 109, 115, 0.28);
|
||||
box-shadow: 0 10px 24px rgba(10, 25, 41, 0.08);
|
||||
}
|
||||
|
||||
.service-action-tags {
|
||||
@@ -1419,12 +1447,51 @@ header .logout-btn:hover,
|
||||
padding: 22px;
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.service-panel.visible.auth-panel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-panel-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.service-panel-empty.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-empty-points {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.service-empty-point {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
|
||||
.service-empty-point strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.service-empty-point span {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.service-section-auth {
|
||||
max-width: 760px;
|
||||
}
|
||||
@@ -1611,7 +1678,9 @@ header .logout-btn:hover,
|
||||
.team-management-grid,
|
||||
.account-overview-secondary,
|
||||
.service-shell,
|
||||
.service-action-list {
|
||||
.service-action-list,
|
||||
.service-empty-points,
|
||||
.workspace-operations-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user