276 lines
13 KiB
HTML
276 lines
13 KiB
HTML
@if (normalizedServer(); as room) {
|
|
<div class="max-w-5xl space-y-4">
|
|
<div class="rounded-lg border border-border/60 bg-background/60 p-4">
|
|
<p class="text-sm text-foreground">
|
|
Roles now define who can moderate, manage channels, upload files, and join voice. Channel overrides are optional and apply on top of the base
|
|
role permissions.
|
|
</p>
|
|
@if (!canManageRoles()) {
|
|
<p class="mt-2 text-xs text-muted-foreground">You can inspect this server's access model, but only members with Manage Roles can edit it.</p>
|
|
}
|
|
</div>
|
|
|
|
<div class="grid gap-4 xl:grid-cols-[16rem,minmax(0,1fr)]">
|
|
<div class="space-y-3 rounded-lg bg-secondary/50 p-3">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<div>
|
|
<p class="text-sm font-medium text-foreground">Roles</p>
|
|
<p class="text-xs text-muted-foreground">Higher roles appear first.</p>
|
|
</div>
|
|
@if (canManageRoles()) {
|
|
<button
|
|
type="button"
|
|
(click)="createRole()"
|
|
class="inline-flex items-center gap-1 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground transition-colors hover:bg-background/80"
|
|
>
|
|
<ng-icon
|
|
name="lucidePlus"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
<span>Role</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
@for (role of roles(); track role.id) {
|
|
<button
|
|
type="button"
|
|
(click)="selectRole(role.id)"
|
|
class="flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-left transition-colors"
|
|
[class.border-primary/60]="selectedRoleKey === role.id"
|
|
[class.bg-background]="selectedRoleKey === role.id"
|
|
[class.border-border/60]="selectedRoleKey !== role.id"
|
|
[class.bg-background/60]="selectedRoleKey !== role.id"
|
|
>
|
|
<span
|
|
class="h-2.5 w-2.5 rounded-full"
|
|
[style.background]="role.color || '#94a3b8'"
|
|
></span>
|
|
<span class="min-w-0 flex-1 truncate text-sm text-foreground">{{ role.name }}</span>
|
|
@if (role.isSystem) {
|
|
<span class="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] uppercase tracking-[0.16em] text-primary">System</span>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="rounded-lg bg-secondary/50 p-4">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-foreground">Slow Mode</p>
|
|
<p class="text-xs text-muted-foreground">Sets the minimum delay between messages for everyone in the server.</p>
|
|
</div>
|
|
<select
|
|
[ngModel]="slowModeValue(room.slowModeInterval)"
|
|
(ngModelChange)="updateSlowMode($event)"
|
|
[disabled]="!canManageServer()"
|
|
class="rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
<option value="0">Off</option>
|
|
<option value="5">5 seconds</option>
|
|
<option value="10">10 seconds</option>
|
|
<option value="30">30 seconds</option>
|
|
<option value="60">1 minute</option>
|
|
<option value="120">2 minutes</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedRole(); as role) {
|
|
<div class="space-y-4 rounded-lg bg-secondary/50 p-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="h-3 w-3 rounded-full"
|
|
[style.background]="role.color || '#94a3b8'"
|
|
></span>
|
|
<p class="text-sm font-medium text-foreground">{{ role.name }}</p>
|
|
</div>
|
|
<p class="mt-1 text-xs text-muted-foreground">
|
|
Edit the role metadata here, then tune its global permissions and per-channel overrides below.
|
|
</p>
|
|
</div>
|
|
@if (role.isSystem) {
|
|
<span class="rounded bg-primary/10 px-2 py-1 text-[10px] uppercase tracking-[0.16em] text-primary">Protected role</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr),8rem]">
|
|
<label class="space-y-1">
|
|
<span class="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Role Name</span>
|
|
<input
|
|
type="text"
|
|
[ngModel]="roleName"
|
|
(ngModelChange)="roleName = $event"
|
|
[disabled]="!canEditSelectedRoleMetadata()"
|
|
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</label>
|
|
|
|
<label class="space-y-1">
|
|
<span class="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Color</span>
|
|
<input
|
|
type="color"
|
|
[ngModel]="roleColor"
|
|
(ngModelChange)="roleColor = $event"
|
|
[disabled]="!canEditSelectedRoleMetadata()"
|
|
class="h-10 w-full rounded-md border border-border bg-background px-1"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
(click)="saveRoleDetails()"
|
|
[disabled]="!canEditSelectedRoleMetadata()"
|
|
class="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ng-icon
|
|
name="lucideCheck"
|
|
class="h-4 w-4"
|
|
/>
|
|
<span>Save Role</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
(click)="moveSelectedRole('up')"
|
|
[disabled]="!canMoveSelectedRoleUp()"
|
|
class="inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-background/80 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ng-icon
|
|
name="lucideArrowUp"
|
|
class="h-4 w-4"
|
|
/>
|
|
<span>Move Up</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
(click)="moveSelectedRole('down')"
|
|
[disabled]="!canMoveSelectedRoleDown()"
|
|
class="inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-background/80 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ng-icon
|
|
name="lucideArrowDown"
|
|
class="h-4 w-4"
|
|
/>
|
|
<span>Move Down</span>
|
|
</button>
|
|
|
|
@if (!role.isSystem) {
|
|
<button
|
|
type="button"
|
|
(click)="deleteSelectedRole()"
|
|
[disabled]="!canEditSelectedRoleMetadata()"
|
|
class="inline-flex items-center gap-1 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive transition-colors hover:bg-destructive/20 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ng-icon
|
|
name="lucideTrash2"
|
|
class="h-4 w-4"
|
|
/>
|
|
<span>Delete</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
@if (role.isSystem) {
|
|
<p class="text-xs text-muted-foreground">
|
|
System roles can still have their permissions tuned, but their name, color, and membership in the base hierarchy stay fixed.
|
|
</p>
|
|
}
|
|
</div>
|
|
|
|
<div class="space-y-3 rounded-lg bg-secondary/50 p-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-foreground">Base Permissions</p>
|
|
<p class="text-xs text-muted-foreground">These defaults apply everywhere unless a channel override changes them.</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
@for (permission of permissionDefinitions; track permission.key) {
|
|
<div class="flex items-center justify-between gap-4 rounded-lg border border-border/50 bg-background/60 p-3">
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-foreground">{{ permission.label }}</p>
|
|
<p class="text-xs text-muted-foreground">{{ permission.description }}</p>
|
|
</div>
|
|
<select
|
|
[ngModel]="permissionState(permission.key)"
|
|
(ngModelChange)="setSelectedRolePermission(permission.key, coercePermissionState($event))"
|
|
[disabled]="!canEditSelectedRole()"
|
|
class="rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
@for (state of permissionStates; track state) {
|
|
<option [value]="state">
|
|
{{ state === 'inherit' ? 'Inherit' : state === 'allow' ? 'Allow' : 'Deny' }}
|
|
</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 rounded-lg bg-secondary/50 p-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-foreground">Channel Overrides</p>
|
|
<p class="text-xs text-muted-foreground">
|
|
Override the selected role inside a specific channel without changing the server-wide default.
|
|
</p>
|
|
</div>
|
|
|
|
@if (channels().length === 0) {
|
|
<p class="text-sm text-muted-foreground">This server has no channels yet.</p>
|
|
} @else {
|
|
<div class="flex items-center gap-3">
|
|
<label class="min-w-0 flex-1 space-y-1">
|
|
<span class="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Channel</span>
|
|
<select
|
|
[ngModel]="selectedChannelKey"
|
|
(ngModelChange)="selectChannel($event)"
|
|
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
@for (channel of channels(); track channel.id) {
|
|
<option [value]="channel.id">{{ channel.name }} ({{ channel.type | titlecase }})</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
@for (permission of permissionDefinitions; track permission.key) {
|
|
<div class="flex items-center justify-between gap-4 rounded-lg border border-border/50 bg-background/60 p-3">
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-foreground">{{ permission.label }}</p>
|
|
<p class="text-xs text-muted-foreground">{{ permission.description }}</p>
|
|
</div>
|
|
<select
|
|
[ngModel]="channelOverrideState(permission.key)"
|
|
(ngModelChange)="setChannelOverride(permission.key, coercePermissionState($event))"
|
|
[disabled]="!canEditSelectedRole()"
|
|
class="rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
@for (state of permissionStates; track state) {
|
|
<option [value]="state">
|
|
{{ state === 'inherit' ? 'Inherit' : state === 'allow' ? 'Allow' : 'Deny' }}
|
|
</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<div class="flex h-40 items-center justify-center text-sm text-muted-foreground">Select a server from the sidebar to manage</div>
|
|
}
|