Files
PiCloud/dashboard/src/lib/RoleChip.svelte
MechaCat02 816a13b920 feat(dashboard): Members tab on the app detail page
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>
2026-05-27 21:37:36 +02:00

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>