diff --git a/src/index.css b/src/index.css index 23cc354..ce08e2e 100644 --- a/src/index.css +++ b/src/index.css @@ -513,6 +513,168 @@ dd { font-size: .92rem; font-family: 'JetBrains Mono', monospace; } .tab.active { color: var(--accent); border-bottom-color: var(--accent); } .tab-content { margin-top: 18px; animation: fadeIn .25s ease; } +/* ---- Role Editor -------------------------------------------------------- */ +.role-editor-page { animation: deck-in .35s cubic-bezier(.16,1,.3,1) both; } +.role-editor-page h2 { display: flex; align-items: center; gap: 10px; } +.role-editor-page .lead { + color: var(--text-dim); font-size: .82rem; + text-transform: uppercase; letter-spacing: .12em; margin: 8px 0 22px; +} + +.role-editor-banner { + border: var(--hair); border-radius: var(--radius); + padding: 12px 16px; margin-bottom: 18px; + font-family: 'JetBrains Mono', monospace; font-size: .85rem; + display: flex; align-items: center; gap: 10px; + animation: fadeIn .25s ease; +} +.role-editor-banner.ok { background: rgba(70,180,135,.10); border-color: var(--success); color: var(--success); } +.role-editor-banner.err { background: rgba(226,85,60,.10); border-color: var(--danger); color: var(--danger); } + +.role-editor-create { + background: var(--bg-card); border: var(--hair); border-radius: var(--radius); + padding: 22px 22px 18px; margin-bottom: 22px; + position: relative; max-width: 480px; + animation: fadeIn .2s ease; +} +.role-editor-create::before { + content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; + background: var(--ember); border-radius: var(--radius) 0 0 var(--radius); +} +.role-editor-create h3 { margin-bottom: 14px; font-size: 1.05rem; letter-spacing: .04em; } +.role-editor-create label { + display: block; margin-bottom: 5px; + font-size: .72rem; text-transform: uppercase; letter-spacing: .12em; + color: var(--text-dim); font-family: 'JetBrains Mono', monospace; +} +.role-editor-create input { + width: 100%; padding: 9px 12px; margin-bottom: 14px; + background: var(--bg-sink); color: var(--text); + border: var(--hair); border-radius: var(--radius); + font-family: inherit; font-size: .9rem; transition: .15s; +} +.role-editor-create input:focus { border-color: var(--accent); outline: none; } +.role-editor-create .actions { display: flex; gap: 8px; } + +/* ---- two-column body ---- */ +.role-editor-grid { + display: grid; grid-template-columns: 280px 1fr; gap: 22px; + align-items: start; +} +@media (max-width: 980px) { + .role-editor-grid { grid-template-columns: 1fr; } +} + +.role-editor-sidebar h3, +.role-editor-detail h3 { + font-size: .82rem; text-transform: uppercase; letter-spacing: .14em; + color: var(--text-dim); margin-bottom: 12px; + border-bottom: var(--hair); padding-bottom: 8px; +} +.role-editor-detail h3 .name { + color: var(--accent); font-family: 'JetBrains Mono', monospace; + letter-spacing: .04em; text-transform: none; +} + +.role-list { display: flex; flex-direction: column; gap: 8px; } +.role-card { + position: relative; padding: 12px 14px; + background: var(--bg-card); border: var(--hair); border-radius: var(--radius); + cursor: pointer; transition: .15s; overflow: hidden; +} +.role-card:hover { background: var(--bg-hover); border-color: var(--border-bright); } +.role-card.active { + background: var(--ember-soft); border-color: var(--accent); + box-shadow: var(--glow); +} +.role-card.active::before { + content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; + background: var(--ember); +} +.role-card .role-name { + font-family: 'Saira Condensed', sans-serif; font-weight: 700; + font-size: .98rem; color: var(--text); letter-spacing: .04em; + display: flex; align-items: center; gap: 6px; +} +.role-card .role-desc { + color: var(--text-dim); font-size: .78rem; margin-top: 3px; + line-height: 1.35; +} +.role-card .role-meta { + color: var(--steel); font-size: .68rem; margin-top: 6px; + font-family: 'JetBrains Mono', monospace; letter-spacing: .04em; +} +.role-card .role-badge-global { + font-size: .72rem; color: var(--warning); + font-family: 'JetBrains Mono', monospace; +} + +/* ---- permissions ---- */ +.perm-group { + background: var(--bg-card); border: var(--hair); border-radius: var(--radius); + padding: 14px 16px 16px; margin-bottom: 14px; +} +.perm-group-header { + font-family: 'Saira Condensed', sans-serif; font-weight: 600; + font-size: .85rem; text-transform: uppercase; letter-spacing: .14em; + color: var(--steel); margin-bottom: 12px; + padding-bottom: 8px; border-bottom: 1px dashed var(--border); +} +.perm-grid { + display: grid; gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); +} +.perm-item { + display: flex; align-items: flex-start; gap: 10px; + padding: 9px 11px; + background: var(--bg-sink); border: 1px solid var(--border); border-radius: 3px; + cursor: pointer; transition: .12s; + user-select: none; +} +.perm-item:hover { background: var(--bg-hover); border-color: var(--border-bright); } +.perm-item.checked { + background: var(--ember-soft); + border-color: var(--accent); + box-shadow: inset 0 0 0 1px rgba(255,122,31,.18); +} +.perm-item input[type="checkbox"] { + appearance: none; -webkit-appearance: none; + width: 14px; height: 14px; margin-top: 2px; + background: var(--bg); border: 1px solid var(--border-bright); + border-radius: 2px; cursor: pointer; flex: 0 0 14px; + position: relative; transition: .12s; +} +.perm-item input[type="checkbox"]:checked { + background: var(--accent); border-color: var(--accent); +} +.perm-item input[type="checkbox"]:checked::after { + content: ''; position: absolute; left: 3px; top: 0px; + width: 4px; height: 8px; + border: solid #1a0d02; border-width: 0 2px 2px 0; + transform: rotate(45deg); +} +.perm-item .perm-body { min-width: 0; } +.perm-item .perm-name { + font-family: 'JetBrains Mono', monospace; font-size: .82rem; + color: var(--text); font-weight: 500; word-break: break-word; +} +.perm-item.checked .perm-name { color: var(--accent); } +.perm-item .perm-desc { + color: var(--text-dim); font-size: .72rem; line-height: 1.35; margin-top: 2px; +} + +.role-editor-actions { + display: flex; gap: 10px; margin-top: 22px; + padding-top: 18px; border-top: var(--hair); +} + +.role-editor-empty { + background: var(--bg-card); border: 1px dashed var(--border); + border-radius: var(--radius); padding: 40px 28px; + text-align: center; color: var(--text-dim); + font-family: 'JetBrains Mono', monospace; font-size: .85rem; +} + /* ---- Responsive --------------------------------------------------------- */ @media (max-width: 860px) { :root { --sidebar-w: 0px; } diff --git a/src/pages/RoleEditorPage.tsx b/src/pages/RoleEditorPage.tsx index 3ce15fe..8808694 100644 --- a/src/pages/RoleEditorPage.tsx +++ b/src/pages/RoleEditorPage.tsx @@ -25,6 +25,7 @@ export default function RoleEditorPage() { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') + const [messageOk, setMessageOk] = useState(true) const [showCreateForm, setShowCreateForm] = useState(false) const [newRoleName, setNewRoleName] = useState('') const [newRoleDesc, setNewRoleDesc] = useState('') @@ -32,15 +33,13 @@ export default function RoleEditorPage() { const isAdmin = user?.is_admin === true - useEffect(() => { - fetchData() - }, []) + useEffect(() => { fetchData() }, []) const fetchData = async () => { try { const [rolesRes, permsRes] = await Promise.all([ api.get('/roles'), - api.get('/roles/permissions') + api.get('/roles/permissions'), ]) setRoles(rolesRes.data) setPermissions(permsRes.data) @@ -54,63 +53,67 @@ export default function RoleEditorPage() { const handlePermissionToggle = (permId: number) => { if (!selectedRole) return const newPermIds = selectedRole.permission_ids.includes(permId) - ? selectedRole.permission_ids.filter(id => id !== permId) + ? selectedRole.permission_ids.filter((id) => id !== permId) : [...selectedRole.permission_ids, permId] setSelectedRole({ ...selectedRole, permission_ids: newPermIds }) } + const flashMessage = (msg: string, ok: boolean) => { + setMessage(msg); setMessageOk(ok) + } + const handleSave = async () => { if (!selectedRole) return - setSaving(true) - setMessage('') + setSaving(true); setMessage('') try { await api.post(`/roles/${selectedRole.id}/permissions`, { - permission_ids: selectedRole.permission_ids + permission_ids: selectedRole.permission_ids, }) - setMessage('Saved successfully!') + flashMessage('Saved successfully', true) fetchData() } catch (err: any) { - setMessage(err.response?.data?.detail || 'Failed to save') + flashMessage(err.response?.data?.detail || 'Failed to save', false) } finally { setSaving(false) } } const handleDeleteRole = async () => { - if (!selectedRole || !confirm(`Are you sure you want to delete the "${selectedRole.name}" role?`)) return - setSaving(true) - setMessage('') + if (!selectedRole) return + if (!confirm(`Delete the "${selectedRole.name}" role?`)) return + setSaving(true); setMessage('') try { await api.delete(`/roles/${selectedRole.id}`) - setMessage('Role deleted successfully!') + flashMessage('Role deleted', true) setSelectedRole(null) fetchData() } catch (err: any) { - setMessage(err.response?.data?.detail || 'Failed to delete role') + flashMessage(err.response?.data?.detail || 'Failed to delete role', false) } finally { setSaving(false) } } - const canDeleteRole = selectedRole && !['admin', 'guest', 'account-manager'].includes(selectedRole.name) && isAdmin + const canDeleteRole = + selectedRole && + !['admin', 'guest', 'account-manager'].includes(selectedRole.name) && + isAdmin const handleCreateRole = async () => { if (!newRoleName.trim()) return - setCreating(true) - setMessage('') + setCreating(true); setMessage('') try { await api.post('/roles', { name: newRoleName.trim(), description: newRoleDesc.trim() || null, - is_global: false + is_global: false, }) - setMessage('Role created successfully!') + flashMessage('Role created', true) setShowCreateForm(false) - setNewRoleName('') - setNewRoleDesc('') + setNewRoleName(''); setNewRoleDesc('') fetchData() } catch (err: any) { - setMessage(err.response?.data?.detail || 'Failed to create role') + flashMessage(err.response?.data?.detail || 'Failed to create role', false) } finally { setCreating(false) } @@ -122,195 +125,127 @@ export default function RoleEditorPage() { return acc }, {} as Record) - if (loading) return
Loading...
+ if (loading) return
Loading roles…
return ( -
+

🔐 Role Editor

-

- Configure permissions for each role. Only admins can edit roles. -

+

Configure permissions for each role. Only admins can edit roles.

{isAdmin && !showCreateForm && ( - )} {showCreateForm && ( -
-

Create New Role

-
-
- - setNewRoleName(e.target.value)} - placeholder="e.g., developer, manager" - style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }} - /> -
-
- - setNewRoleDesc(e.target.value)} - placeholder="Role description" - style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }} - /> -
-
- - -
+
+

Create new role

+ + setNewRoleName(e.target.value)} + placeholder="e.g. developer, manager" + /> + + setNewRoleDesc(e.target.value)} + placeholder="(optional) short description" + /> +
+ +
)} {message && ( -
- {message} +
+ {messageOk ? '✓' : '✗'} + {message}
)} -
- {/* Role List */} -
+
+ {/* Roles sidebar */} + - {/* Permission Editor */} -
+ {/* Permission editor */} +
{selectedRole ? ( <> -

Permissions for: {selectedRole.name}

-
- {Object.entries(groupedPermissions).map(([category, perms]) => ( -
-

- {category} -

-
- {perms.map(perm => ( -
+ ))} +
+ - )} + {canDeleteRole && ( + + )} +
) : ( -
- Select a role to edit its permissions +
+ ◆ Select a role on the left to edit its permissions
)} -
+
)