style(portal): split workspace and service detail panes

This commit is contained in:
rcourtman
2026-03-27 17:57:44 +00:00
parent a18bebaeb0
commit 5d3acb580f
9 changed files with 201 additions and 25 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "5a37d7a95cfb4c6cfa06bac62785c5638c391bcf52d6472e05c265f548f367e7",
"source_hash": "b48ed843c8ab211f26d40bdb069e5a6a3f35c206eedef2cf15301ca0f640e8e3",
"build_inputs": [
"package.json",
"tsconfig.json",
+64 -2
View File
@@ -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
View File
@@ -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;
}