diff --git a/frontend/css/main.css b/frontend/css/main.css index b34fa35..4ceca37 100644 --- a/frontend/css/main.css +++ b/frontend/css/main.css @@ -106,6 +106,62 @@ body { } .workspace-switcher-item:hover .workspace-switcher-pencil { visibility: visible; } .workspace-switcher-pencil:hover { color: var(--accent); background: var(--bg-input); } +/* Members icon - same shape as the pencil; navigates to #/workspace/:id/members. */ +.workspace-switcher-members { + flex-shrink: 0; visibility: hidden; + background: none; border: none; padding: 4px; + color: var(--text-muted); cursor: pointer; + border-radius: 4px; transition: all var(--transition); +} +.workspace-switcher-item:hover .workspace-switcher-members { visibility: visible; } +.workspace-switcher-members:hover { color: var(--accent); background: var(--bg-input); } + +/* Workspace members page (Phase 2 user-mgmt, slice 2A read-only). Three + sections render via JS: direct members, via_org access, pending invites. + Row layout mirrors the sidebar user card's avatar pattern for visual + continuity. via_org rows are opacity-reduced and invite rows use the + input-bg shade so the three states are distinguishable at a glance. */ +.members-list { display: flex; flex-direction: column; gap: 4px; } +.member-row { + display: flex; align-items: center; gap: 12px; + padding: 10px 12px; border: 1px solid var(--border); + border-radius: var(--radius); background: var(--bg-card); +} +.member-row--via-org { opacity: 0.75; } +.member-row--invited { background: var(--bg-input); } +.member-avatar { + flex-shrink: 0; + width: 32px; height: 32px; border-radius: 50%; + background: var(--accent); color: white; + display: flex; align-items: center; justify-content: center; + font-size: 13px; font-weight: 600; +} +.member-avatar--muted { background: var(--text-muted); } +.member-meta { flex: 1; min-width: 0; } +.member-name { + font-size: 13px; font-weight: 500; color: var(--text-primary); + display: flex; align-items: center; gap: 8px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.member-email { + font-size: 11px; color: var(--text-muted); margin-top: 1px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.member-role { + flex-shrink: 0; font-size: 12px; color: var(--text-secondary); + padding: 4px 8px; background: var(--bg-input); + border-radius: 4px; min-width: 60px; text-align: center; +} +.member-detail { + flex-shrink: 0; font-size: 11px; color: var(--text-muted); + min-width: 110px; text-align: right; +} +.member-via-org { font-style: italic; } +.member-badge { + font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; + padding: 2px 6px; background: var(--bg-input); color: var(--text-muted); + border-radius: 3px; font-weight: 600; +} .nav-links { flex: 1; diff --git a/frontend/js/api.js b/frontend/js/api.js index 543537f..7cc1fb2 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -159,6 +159,10 @@ export const api = { switchWorkspace: (workspaceId) => request('/auth/switch-workspace', { method: 'POST', body: JSON.stringify({ workspace_id: workspaceId }) }), renameWorkspace: (id, data) => request(`/workspaces/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), + // Workspace members + invites (slice 2A read-only; mutation helpers land in 2B) + getWorkspaceMembers: (id) => request(`/workspaces/${id}/members`), + getWorkspaceInvites: (id) => request(`/workspaces/${id}/invites`), + // Admin - Users getUsers: () => request('/auth/users'), deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }), diff --git a/frontend/js/app.js b/frontend/js/app.js index 237ea1f..9a71624 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -19,6 +19,7 @@ import * as admin from './views/admin.js'; import * as adminPlayerDebug from './views/admin-player-debug.js'; import * as designer from './views/designer.js'; import * as playlists from './views/playlists.js'; +import * as workspaceMembers from './views/workspace-members.js'; import { applyBranding } from './branding.js'; import { t } from './i18n.js'; import { isPlatformAdmin } from './utils.js'; @@ -211,6 +212,10 @@ function route() { } else if (hash === '#/teams' || hash.startsWith('#/team/')) { currentView = teams; teams.render(app); + } else if (hash.startsWith('#/workspace/') && hash.includes('/members')) { + const wsId = hash.split('#/workspace/')[1].split('/')[0]; + currentView = workspaceMembers; + workspaceMembers.render(app, wsId); } else if (hash === '#/help' || hash.startsWith('#/help')) { currentView = help; help.render(app); diff --git a/frontend/js/components/workspace-switcher.js b/frontend/js/components/workspace-switcher.js index 1c8d213..1f324b9 100644 --- a/frontend/js/components/workspace-switcher.js +++ b/frontend/js/components/workspace-switcher.js @@ -67,6 +67,14 @@ export function renderWorkspaceSwitcher(me) {
${subtitle}
${w.can_admin ? ` +