mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
style(portal): harden team management surface
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "c17b1452a1c3fa53f55c51b5219dddbd5ee5faec28d79f4743271b3a3be0e4f1",
|
||||
"source_hash": "b5bf3d15198c6bb317c1446735208077a02ef32a47dca5b012200aba131071ef",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+53
-13
@@ -980,7 +980,7 @@ header .logout-btn:hover,
|
||||
}
|
||||
.team-roster-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
padding: 0 8px 2px;
|
||||
font-size: 11px;
|
||||
@@ -991,10 +991,10 @@ header .logout-btn:hover,
|
||||
}
|
||||
.team-member-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px auto;
|
||||
align-items: center;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
@@ -1002,7 +1002,14 @@ header .logout-btn:hover,
|
||||
.team-member-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.team-member-topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.team-member-email {
|
||||
@@ -1011,7 +1018,8 @@ header .logout-btn:hover,
|
||||
color: var(--ink);
|
||||
}
|
||||
.team-member-caption {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.team-member-controls {
|
||||
@@ -1022,13 +1030,6 @@ header .logout-btn:hover,
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.team-member-role-summary {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.team-role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1040,6 +1041,20 @@ header .logout-btn:hover,
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.team-inline-role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--panel-muted);
|
||||
color: var(--ink-soft);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.team-role-select {
|
||||
min-width: 140px;
|
||||
}
|
||||
@@ -1071,6 +1086,31 @@ header .logout-btn:hover,
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.team-invite-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
.team-invite-guide-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.team-invite-guide-row strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.team-invite-guide-row span {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.service-action-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+27
-8
@@ -16,6 +16,22 @@
|
||||
return role || "Member";
|
||||
}
|
||||
}
|
||||
function roleCapabilityCopy(role) {
|
||||
switch (role) {
|
||||
case "owner":
|
||||
return "Full account control, including billing, team access, and hosted operations.";
|
||||
case "admin":
|
||||
return "Can manage hosted operations and billing for this account.";
|
||||
case "tech":
|
||||
return "Can operate hosted workspaces without billing ownership.";
|
||||
case "read_only":
|
||||
return "Can review hosted state without making control-plane changes.";
|
||||
case "member":
|
||||
return "Has access through the account roster.";
|
||||
default:
|
||||
return "Has access through the account roster.";
|
||||
}
|
||||
}
|
||||
function getElement(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
@@ -157,24 +173,27 @@
|
||||
row.className = "team-member-row";
|
||||
var identity = document.createElement("div");
|
||||
identity.className = "team-member-identity";
|
||||
var topline = document.createElement("div");
|
||||
topline.className = "team-member-topline";
|
||||
var email = document.createElement("div");
|
||||
email.className = "team-member-email";
|
||||
email.textContent = member.email;
|
||||
identity.appendChild(email);
|
||||
topline.appendChild(email);
|
||||
var roleBadge = document.createElement("span");
|
||||
roleBadge.className = "team-inline-role-badge";
|
||||
roleBadge.textContent = roleLabel(member.role);
|
||||
topline.appendChild(roleBadge);
|
||||
identity.appendChild(topline);
|
||||
var caption = document.createElement("div");
|
||||
caption.className = "team-member-caption";
|
||||
caption.textContent = member.role === "owner" ? "Full account control" : "Hosted account access";
|
||||
caption.textContent = roleCapabilityCopy(member.role);
|
||||
identity.appendChild(caption);
|
||||
var roleSummary = document.createElement("div");
|
||||
roleSummary.className = "team-member-role-summary";
|
||||
roleSummary.textContent = roleLabel(member.role);
|
||||
var controls = document.createElement("div");
|
||||
controls.className = "team-member-controls";
|
||||
controls.appendChild(renderTeamRoleControl(accountID, member, isOwner));
|
||||
var action = renderTeamMemberAction(accountID, member, isOwner);
|
||||
if (action) controls.appendChild(action);
|
||||
row.appendChild(identity);
|
||||
row.appendChild(roleSummary);
|
||||
row.appendChild(controls);
|
||||
return row;
|
||||
}
|
||||
@@ -183,7 +202,7 @@
|
||||
if (existing) return;
|
||||
var head = document.createElement("div");
|
||||
head.className = "team-roster-head";
|
||||
head.innerHTML = "<span>Operator</span><span>Access</span><span>Controls</span>";
|
||||
head.innerHTML = "<span>Operator</span><span>Controls</span>";
|
||||
container.appendChild(head);
|
||||
}
|
||||
function renderAddWorkspaceSection(accountID, entry) {
|
||||
@@ -1763,7 +1782,7 @@
|
||||
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>";
|
||||
}
|
||||
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><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>';
|
||||
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>';
|
||||
}
|
||||
function renderSupportSection(context) {
|
||||
return '<section class="portal-support-panel"><div class="account-panel-kicker">Support</div><h2>Support and escalation</h2><p>Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.</p><div class="portal-support-card-grid"><div class="portal-support-card"><h3>Account support</h3><p>For access, tenant handoff, team, and billing issues, contact the hosted operations desk.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div><div class="portal-support-card"><h3>Commercial services</h3><p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.</p><button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button></div></div></section>';
|
||||
|
||||
@@ -25,6 +25,23 @@ function roleLabel(role: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function roleCapabilityCopy(role: string): string {
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
return 'Full account control, including billing, team access, and hosted operations.';
|
||||
case 'admin':
|
||||
return 'Can manage hosted operations and billing for this account.';
|
||||
case 'tech':
|
||||
return 'Can operate hosted workspaces without billing ownership.';
|
||||
case 'read_only':
|
||||
return 'Can review hosted state without making control-plane changes.';
|
||||
case 'member':
|
||||
return 'Has access through the account roster.';
|
||||
default:
|
||||
return 'Has access through the account roster.';
|
||||
}
|
||||
}
|
||||
|
||||
export function getElement<T extends HTMLElement = HTMLElement>(id: string): T | null {
|
||||
return document.getElementById(id) as T | null;
|
||||
}
|
||||
@@ -191,20 +208,26 @@ function renderTeamMemberRow(accountID: string, member: PortalTeamMember, isOwne
|
||||
var identity = document.createElement('div');
|
||||
identity.className = 'team-member-identity';
|
||||
|
||||
var topline = document.createElement('div');
|
||||
topline.className = 'team-member-topline';
|
||||
|
||||
var email = document.createElement('div');
|
||||
email.className = 'team-member-email';
|
||||
email.textContent = member.email;
|
||||
identity.appendChild(email);
|
||||
topline.appendChild(email);
|
||||
|
||||
var roleBadge = document.createElement('span');
|
||||
roleBadge.className = 'team-inline-role-badge';
|
||||
roleBadge.textContent = roleLabel(member.role);
|
||||
topline.appendChild(roleBadge);
|
||||
|
||||
identity.appendChild(topline);
|
||||
|
||||
var caption = document.createElement('div');
|
||||
caption.className = 'team-member-caption';
|
||||
caption.textContent = member.role === 'owner' ? 'Full account control' : 'Hosted account access';
|
||||
caption.textContent = roleCapabilityCopy(member.role);
|
||||
identity.appendChild(caption);
|
||||
|
||||
var roleSummary = document.createElement('div');
|
||||
roleSummary.className = 'team-member-role-summary';
|
||||
roleSummary.textContent = roleLabel(member.role);
|
||||
|
||||
var controls = document.createElement('div');
|
||||
controls.className = 'team-member-controls';
|
||||
controls.appendChild(renderTeamRoleControl(accountID, member, isOwner));
|
||||
@@ -212,7 +235,6 @@ function renderTeamMemberRow(accountID: string, member: PortalTeamMember, isOwne
|
||||
if (action) controls.appendChild(action);
|
||||
|
||||
row.appendChild(identity);
|
||||
row.appendChild(roleSummary);
|
||||
row.appendChild(controls);
|
||||
return row;
|
||||
}
|
||||
@@ -224,7 +246,6 @@ function ensureRosterHead(container: HTMLElement): void {
|
||||
head.className = 'team-roster-head';
|
||||
head.innerHTML =
|
||||
'<span>Operator</span>' +
|
||||
'<span>Access</span>' +
|
||||
'<span>Controls</span>';
|
||||
container.appendChild(head);
|
||||
}
|
||||
|
||||
@@ -566,6 +566,11 @@ function renderAccountTeamSection(account: PortalAccountSummary): string {
|
||||
'">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>' +
|
||||
|
||||
@@ -1098,7 +1098,7 @@ header .logout-btn:hover,
|
||||
|
||||
.team-roster-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
padding: 0 8px 2px;
|
||||
font-size: 11px;
|
||||
@@ -1110,10 +1110,10 @@ header .logout-btn:hover,
|
||||
|
||||
.team-member-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px auto;
|
||||
align-items: center;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
@@ -1122,7 +1122,15 @@ header .logout-btn:hover,
|
||||
.team-member-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.team-member-topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1133,7 +1141,8 @@ header .logout-btn:hover,
|
||||
}
|
||||
|
||||
.team-member-caption {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
@@ -1146,14 +1155,6 @@ header .logout-btn:hover,
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.team-member-role-summary {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.team-role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1166,6 +1167,21 @@ header .logout-btn:hover,
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.team-inline-role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--panel-muted);
|
||||
color: var(--ink-soft);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-role-select {
|
||||
min-width: 140px;
|
||||
}
|
||||
@@ -1203,6 +1219,35 @@ header .logout-btn:hover,
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.team-invite-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.team-invite-guide-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.team-invite-guide-row strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.team-invite-guide-row span {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.service-action-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user