A new "Members" tab is rendered between Domains and Settings for callers whose `my_role` on the app is `app_admin` (owners always; explicit member-app_admins; admins do not see it — they're only implicit editors and can't manage memberships). The tab lets the caller: - See every explicit member of the app with username, email, instance- role chip, app-role chip, and joined date. Inactive users render greyed-out so admins know the row exists. - Pick a `member`-instance user from a dropdown and grant viewer / editor / app_admin access. The dropdown is populated from `/admin/admins` filtered to active members not already on the app. - Promote / demote / remove existing members via the shared `ActionMenu` kebab. Removal goes through `ConfirmModal`. Member-with-app_admin callers see a disabled add form with an explanatory message — they have authority to manage memberships but can't browse the user directory (gated on `InstanceManageUsers`), which is a known phase-3.5 caveat to revisit in a follow-up. Also extends `RoleChip` with an `appRole` prop and palette for app roles, and adds an `appMembers` namespace to api.ts mirroring the `domains` shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
1.4 KiB
Svelte
70 lines
1.4 KiB
Svelte
<script lang="ts">
|
|
import type { InstanceRole } from '$lib/auth';
|
|
import type { AppRole } from '$lib/api';
|
|
|
|
interface Props {
|
|
role?: InstanceRole;
|
|
appRole?: AppRole;
|
|
size?: 'sm' | 'md';
|
|
}
|
|
|
|
let { role, appRole, size = 'md' }: Props = $props();
|
|
|
|
// Display label: app roles read better with a space ("app admin")
|
|
// than their wire form ("app_admin").
|
|
const label = $derived(
|
|
appRole ? appRole.replace('_', ' ') : (role ?? '')
|
|
);
|
|
const cls = $derived(appRole ? `chip-${appRole}` : `chip-${role}`);
|
|
</script>
|
|
|
|
<span class="chip {cls}" class:sm={size === 'sm'}>{label}</span>
|
|
|
|
<style>
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.15rem 0.55rem;
|
|
border-radius: 999px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
border: 1px solid transparent;
|
|
}
|
|
.chip.sm {
|
|
font-size: 0.625rem;
|
|
padding: 0.1rem 0.45rem;
|
|
}
|
|
.chip-owner {
|
|
background: #78350f;
|
|
color: #fbbf24;
|
|
border-color: #b45309;
|
|
}
|
|
.chip-admin {
|
|
background: #164e63;
|
|
color: #67e8f9;
|
|
border-color: #0e7490;
|
|
}
|
|
.chip-member {
|
|
background: #1e293b;
|
|
color: #cbd5e1;
|
|
border-color: #334155;
|
|
}
|
|
.chip-app_admin {
|
|
background: #4c1d95;
|
|
color: #c4b5fd;
|
|
border-color: #6d28d9;
|
|
}
|
|
.chip-editor {
|
|
background: #1e3a8a;
|
|
color: #93c5fd;
|
|
border-color: #1d4ed8;
|
|
}
|
|
.chip-viewer {
|
|
background: #1f2937;
|
|
color: #9ca3af;
|
|
border-color: #374151;
|
|
}
|
|
</style>
|