style(project): format Go with gofmt, set up Prettier for the frontend
Run gofmt -w across the backend, normalising the manually-aligned := blocks to the gofmt standard. No code behaviour changes. Add Prettier (+ prettier-plugin-svelte) to the frontend with the SvelteKit default config (tabs, single quotes) so formatting is reproducible, then run it over the whole tree. Add format / format:check npm scripts and a .prettierignore (build output, generated schema.ts, static assets). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
const tabs = [
|
||||
{ href: '/admin/users', label: 'Users' },
|
||||
{ href: '/admin/audit', label: 'Audit log' },
|
||||
{ href: '/admin/audit', label: 'Audit log' }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -14,17 +14,21 @@
|
||||
<nav class="admin-nav">
|
||||
<button class="back-btn" onclick={() => goto('/files')} aria-label="Back to files">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M10 3L5 8L10 13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M10 3L5 8L10 13"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="admin-title">Admin</span>
|
||||
<div class="tabs">
|
||||
{#each tabs as tab}
|
||||
<a
|
||||
href={tab.href}
|
||||
class="tab"
|
||||
class:active={$page.url.pathname.startsWith(tab.href)}
|
||||
>{tab.label}</a>
|
||||
<a href={tab.href} class="tab" class:active={$page.url.pathname.startsWith(tab.href)}
|
||||
>{tab.label}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -112,4 +116,4 @@
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -9,4 +9,4 @@ export const load = () => {
|
||||
if (!user?.isAdmin) {
|
||||
redirect(307, '/files');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,42 +6,42 @@
|
||||
const OBJECT_TYPES = ['file', 'tag', 'category', 'pool'];
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
// Auth
|
||||
user_login: 'User logged in',
|
||||
user_logout: 'User logged out',
|
||||
user_login: 'User logged in',
|
||||
user_logout: 'User logged out',
|
||||
// Files
|
||||
file_create: 'File uploaded',
|
||||
file_edit: 'File edited',
|
||||
file_delete: 'File deleted',
|
||||
file_restore: 'File restored',
|
||||
file_permanent_delete: 'File permanently deleted',
|
||||
file_replace: 'File replaced',
|
||||
file_create: 'File uploaded',
|
||||
file_edit: 'File edited',
|
||||
file_delete: 'File deleted',
|
||||
file_restore: 'File restored',
|
||||
file_permanent_delete: 'File permanently deleted',
|
||||
file_replace: 'File replaced',
|
||||
// Tags
|
||||
tag_create: 'Tag created',
|
||||
tag_edit: 'Tag edited',
|
||||
tag_delete: 'Tag deleted',
|
||||
tag_create: 'Tag created',
|
||||
tag_edit: 'Tag edited',
|
||||
tag_delete: 'Tag deleted',
|
||||
// Categories
|
||||
category_create: 'Category created',
|
||||
category_edit: 'Category edited',
|
||||
category_delete: 'Category deleted',
|
||||
category_create: 'Category created',
|
||||
category_edit: 'Category edited',
|
||||
category_delete: 'Category deleted',
|
||||
// Pools
|
||||
pool_create: 'Pool created',
|
||||
pool_edit: 'Pool edited',
|
||||
pool_delete: 'Pool deleted',
|
||||
pool_create: 'Pool created',
|
||||
pool_edit: 'Pool edited',
|
||||
pool_delete: 'Pool deleted',
|
||||
// Relations
|
||||
file_tag_add: 'Tag added to file',
|
||||
file_tag_remove: 'Tag removed from file',
|
||||
file_pool_add: 'File added to pool',
|
||||
file_pool_remove: 'File removed from pool',
|
||||
file_tag_add: 'Tag added to file',
|
||||
file_tag_remove: 'Tag removed from file',
|
||||
file_pool_add: 'File added to pool',
|
||||
file_pool_remove: 'File removed from pool',
|
||||
// ACL
|
||||
acl_change: 'ACL changed',
|
||||
acl_change: 'ACL changed',
|
||||
// Admin
|
||||
user_create: 'User created',
|
||||
user_delete: 'User deleted',
|
||||
user_block: 'User blocked',
|
||||
user_unblock: 'User unblocked',
|
||||
user_role_change: 'User role changed',
|
||||
user_create: 'User created',
|
||||
user_delete: 'User deleted',
|
||||
user_block: 'User blocked',
|
||||
user_unblock: 'User unblocked',
|
||||
user_role_change: 'User role changed',
|
||||
// Sessions
|
||||
session_terminate: 'Session terminated',
|
||||
session_terminate: 'Session terminated'
|
||||
};
|
||||
|
||||
// ---- Filters ----
|
||||
@@ -55,7 +55,7 @@
|
||||
// ---- Data ----
|
||||
let entries = $state<AuditEntry[]>([]);
|
||||
let total = $state(0);
|
||||
let page = $state(0); // 0-based
|
||||
let page = $state(0); // 0-based
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let initialLoaded = $state(false);
|
||||
@@ -65,14 +65,23 @@
|
||||
// ---- Users for filter dropdown ----
|
||||
let allUsers = $state<User[]>([]);
|
||||
$effect(() => {
|
||||
api.get<UserOffsetPage>('/users?limit=200').then((r) => { allUsers = r.items ?? []; }).catch(() => {});
|
||||
api
|
||||
.get<UserOffsetPage>('/users?limit=200')
|
||||
.then((r) => {
|
||||
allUsers = r.items ?? [];
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// Unknown action types not in ACTION_LABELS (server may add new ones)
|
||||
let knownActions = $derived([...new Set(entries.map((e) => e.action).filter(Boolean))].sort() as string[]);
|
||||
let knownActions = $derived(
|
||||
[...new Set(entries.map((e) => e.action).filter(Boolean))].sort() as string[]
|
||||
);
|
||||
|
||||
// ---- Reset on filter change ----
|
||||
let filterKey = $derived(`${filterUserId}|${filterAction}|${filterObjectType}|${filterObjectId}|${filterFrom}|${filterTo}`);
|
||||
let filterKey = $derived(
|
||||
`${filterUserId}|${filterAction}|${filterObjectType}|${filterObjectId}|${filterFrom}|${filterTo}`
|
||||
);
|
||||
let prevFilterKey = $state('');
|
||||
|
||||
$effect(() => {
|
||||
@@ -94,12 +103,12 @@
|
||||
error = '';
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: String(LIMIT), offset: String(page * LIMIT) });
|
||||
if (filterUserId) params.set('user_id', filterUserId);
|
||||
if (filterAction) params.set('action', filterAction);
|
||||
if (filterObjectType) params.set('object_type', filterObjectType);
|
||||
if (filterObjectId.trim()) params.set('object_id', filterObjectId.trim());
|
||||
if (filterFrom) params.set('from', new Date(filterFrom).toISOString());
|
||||
if (filterTo) params.set('to', new Date(filterTo).toISOString());
|
||||
if (filterUserId) params.set('user_id', filterUserId);
|
||||
if (filterAction) params.set('action', filterAction);
|
||||
if (filterObjectType) params.set('object_type', filterObjectType);
|
||||
if (filterObjectId.trim()) params.set('object_id', filterObjectId.trim());
|
||||
if (filterFrom) params.set('from', new Date(filterFrom).toISOString());
|
||||
if (filterTo) params.set('to', new Date(filterTo).toISOString());
|
||||
|
||||
const res = await api.get<AuditOffsetPage>(`/audit?${params}`);
|
||||
entries = res.items ?? [];
|
||||
@@ -122,8 +131,12 @@
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,60 +224,74 @@
|
||||
<p class="msg error" role="alert">{error}</p>
|
||||
{:else}
|
||||
<div class="content-area">
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Object</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries as e (e.id)}
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="ts-cell">{formatTs(e.performed_at)}</td>
|
||||
<td class="user-cell">{e.user_name ?? '—'}</td>
|
||||
<td class="action-cell">
|
||||
<span class="action-tag" class:file={e.object_type === 'file'} class:tag={e.object_type === 'tag'} class:pool={e.object_type === 'pool'} class:cat={e.object_type === 'category'}>
|
||||
{actionLabel(e.action)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="obj-type-cell">{e.object_type ?? '—'}</td>
|
||||
<td class="obj-id-cell" title={e.object_id ?? ''}>{shortId(e.object_id)}</td>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Object</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries as e (e.id)}
|
||||
<tr>
|
||||
<td class="ts-cell">{formatTs(e.performed_at)}</td>
|
||||
<td class="user-cell">{e.user_name ?? '—'}</td>
|
||||
<td class="action-cell">
|
||||
<span
|
||||
class="action-tag"
|
||||
class:file={e.object_type === 'file'}
|
||||
class:tag={e.object_type === 'tag'}
|
||||
class:pool={e.object_type === 'pool'}
|
||||
class:cat={e.object_type === 'category'}
|
||||
>
|
||||
{actionLabel(e.action)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="obj-type-cell">{e.object_type ?? '—'}</td>
|
||||
<td class="obj-id-cell" title={e.object_id ?? ''}>{shortId(e.object_id)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#if loading}
|
||||
<tr class="loading-row">
|
||||
<td colspan="5">
|
||||
<span class="spinner" role="status" aria-label="Loading"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<tr class="loading-row">
|
||||
<td colspan="5">
|
||||
<span class="spinner" role="status" aria-label="Loading"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if !loading && initialLoaded && entries.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="empty-cell">No entries match the current filters.</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="pagination">
|
||||
<button class="page-btn" onclick={() => goToPage(page - 1)} disabled={page === 0 || loading}>
|
||||
← Prev
|
||||
</button>
|
||||
<span class="page-info">Page {page + 1} of {totalPages}</span>
|
||||
<button class="page-btn" onclick={() => goToPage(page + 1)} disabled={page >= totalPages - 1 || loading}>
|
||||
Next →
|
||||
</button>
|
||||
{#if !loading && initialLoaded && entries.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="empty-cell">No entries match the current filters.</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="pagination">
|
||||
<button
|
||||
class="page-btn"
|
||||
onclick={() => goToPage(page - 1)}
|
||||
disabled={page === 0 || loading}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
<span class="page-info">Page {page + 1} of {totalPages}</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
onclick={() => goToPage(page + 1)}
|
||||
disabled={page >= totalPages - 1 || loading}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -428,10 +455,22 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-tag.file { background-color: color-mix(in srgb, var(--color-info) 12%, transparent); color: var(--color-info); }
|
||||
.action-tag.tag { background-color: color-mix(in srgb, #7ECBA1 12%, transparent); color: #7ECBA1; }
|
||||
.action-tag.pool { background-color: color-mix(in srgb, var(--color-warning) 12%, transparent); color: var(--color-warning); }
|
||||
.action-tag.cat { background-color: color-mix(in srgb, var(--color-danger) 12%, transparent); color: var(--color-danger); }
|
||||
.action-tag.file {
|
||||
background-color: color-mix(in srgb, var(--color-info) 12%, transparent);
|
||||
color: var(--color-info);
|
||||
}
|
||||
.action-tag.tag {
|
||||
background-color: color-mix(in srgb, #7ecba1 12%, transparent);
|
||||
color: #7ecba1;
|
||||
}
|
||||
.action-tag.pool {
|
||||
background-color: color-mix(in srgb, var(--color-warning) 12%, transparent);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.action-tag.cat {
|
||||
background-color: color-mix(in srgb, var(--color-danger) 12%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.obj-type-cell {
|
||||
color: var(--color-text-muted);
|
||||
@@ -466,7 +505,11 @@
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
@@ -510,4 +553,4 @@
|
||||
color: var(--color-danger);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
name: newName.trim(),
|
||||
password: newPassword.trim(),
|
||||
can_create: newCanCreate,
|
||||
is_admin: newIsAdmin,
|
||||
is_admin: newIsAdmin
|
||||
});
|
||||
users = [u, ...users];
|
||||
total++;
|
||||
@@ -73,7 +73,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => { void load(); });
|
||||
$effect(() => {
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Users — Admin | Tanabata</title></svelte:head>
|
||||
@@ -90,8 +92,20 @@
|
||||
<div class="create-form">
|
||||
{#if createError}<p class="form-error" role="alert">{createError}</p>{/if}
|
||||
<div class="form-row">
|
||||
<input class="input" type="text" placeholder="Username" bind:value={newName} autocomplete="off" />
|
||||
<input class="input" type="password" placeholder="Password" bind:value={newPassword} autocomplete="new-password" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
bind:value={newName}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
bind:value={newPassword}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row checks">
|
||||
<label class="check-label">
|
||||
@@ -140,7 +154,11 @@
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" class:admin={u.is_admin} class:creator={!u.is_admin && u.can_create}>
|
||||
<span
|
||||
class="badge"
|
||||
class:admin={u.is_admin}
|
||||
class:creator={!u.is_admin && u.can_create}
|
||||
>
|
||||
{u.is_admin ? 'Admin' : u.can_create ? 'Creator' : 'Viewer'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -154,12 +172,27 @@
|
||||
<td class="actions-cell">
|
||||
<button class="icon-btn" onclick={() => goto(`/admin/users/${u.id}`)} title="Edit">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path d="M9.5 2.5l2 2-7 7H2.5v-2l7-7z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M9.5 2.5l2 2-7 7H2.5v-2l7-7z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.4"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn danger" onclick={() => (confirmDeleteUser = u)} title="Delete">
|
||||
<button
|
||||
class="icon-btn danger"
|
||||
onclick={() => (confirmDeleteUser = u)}
|
||||
title="Delete"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path d="M2 4h10M5 4V2.5h4V4M5.5 6.5v4M8.5 6.5v4M3 4l.8 7.5h6.4L11 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M2 4h10M5 4V2.5h4V4M5.5 6.5v4M8.5 6.5v4M3 4l.8 7.5h6.4L11 4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
@@ -265,7 +298,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background-color: var(--color-accent);
|
||||
@@ -348,8 +384,8 @@
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background-color: color-mix(in srgb, #7ECBA1 15%, transparent);
|
||||
color: #7ECBA1;
|
||||
background-color: color-mix(in srgb, #7ecba1 15%, transparent);
|
||||
color: #7ecba1;
|
||||
}
|
||||
|
||||
.badge.blocked {
|
||||
@@ -402,14 +438,21 @@
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error, .empty {
|
||||
.error,
|
||||
.empty {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.error { color: var(--color-danger); }
|
||||
</style>
|
||||
.error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,16 +25,20 @@
|
||||
const id = userId;
|
||||
loading = true;
|
||||
error = '';
|
||||
void api.get<User>(`/users/${id}`).then((u) => {
|
||||
user = u;
|
||||
isAdmin = u.is_admin ?? false;
|
||||
canCreate = u.can_create ?? false;
|
||||
isBlocked = u.is_blocked ?? false;
|
||||
}).catch((e) => {
|
||||
error = e instanceof ApiError ? e.message : 'Failed to load user';
|
||||
}).finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
void api
|
||||
.get<User>(`/users/${id}`)
|
||||
.then((u) => {
|
||||
user = u;
|
||||
isAdmin = u.is_admin ?? false;
|
||||
canCreate = u.can_create ?? false;
|
||||
isBlocked = u.is_blocked ?? false;
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e instanceof ApiError ? e.message : 'Failed to load user';
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
});
|
||||
|
||||
async function save() {
|
||||
@@ -46,7 +50,7 @@
|
||||
const updated = await api.patch<User>(`/users/${user.id}`, {
|
||||
is_admin: isAdmin,
|
||||
can_create: canCreate,
|
||||
is_blocked: isBlocked,
|
||||
is_blocked: isBlocked
|
||||
});
|
||||
user = updated;
|
||||
saveSuccess = true;
|
||||
@@ -74,9 +78,7 @@
|
||||
<svelte:head><title>{user?.name ?? 'User'} — Admin | Tanabata</title></svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<button class="back-link" onclick={() => goto('/admin/users')}>
|
||||
← All users
|
||||
</button>
|
||||
<button class="back-link" onclick={() => goto('/admin/users')}> ← All users </button>
|
||||
|
||||
{#if error}
|
||||
<p class="msg error" role="alert">{error}</p>
|
||||
@@ -101,10 +103,12 @@
|
||||
<p class="toggle-hint">Full access to all data and admin panel.</p>
|
||||
</div>
|
||||
<button
|
||||
class="toggle" class:on={isAdmin}
|
||||
role="switch" aria-checked={isAdmin}
|
||||
onclick={() => (isAdmin = !isAdmin)}
|
||||
><span class="thumb"></span></button>
|
||||
class="toggle"
|
||||
class:on={isAdmin}
|
||||
role="switch"
|
||||
aria-checked={isAdmin}
|
||||
onclick={() => (isAdmin = !isAdmin)}><span class="thumb"></span></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="toggle-row">
|
||||
@@ -113,10 +117,12 @@
|
||||
<p class="toggle-hint">Can upload files and create tags, pools, categories.</p>
|
||||
</div>
|
||||
<button
|
||||
class="toggle" class:on={canCreate}
|
||||
role="switch" aria-checked={canCreate}
|
||||
onclick={() => (canCreate = !canCreate)}
|
||||
><span class="thumb"></span></button>
|
||||
class="toggle"
|
||||
class:on={canCreate}
|
||||
role="switch"
|
||||
aria-checked={canCreate}
|
||||
onclick={() => (canCreate = !canCreate)}><span class="thumb"></span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,10 +135,13 @@
|
||||
<p class="toggle-hint">Blocked users cannot log in.</p>
|
||||
</div>
|
||||
<button
|
||||
class="toggle" class:on={isBlocked} class:danger={isBlocked}
|
||||
role="switch" aria-checked={isBlocked}
|
||||
onclick={() => (isBlocked = !isBlocked)}
|
||||
><span class="thumb"></span></button>
|
||||
class="toggle"
|
||||
class:on={isBlocked}
|
||||
class:danger={isBlocked}
|
||||
role="switch"
|
||||
aria-checked={isBlocked}
|
||||
onclick={() => (isBlocked = !isBlocked)}><span class="thumb"></span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,7 +149,11 @@
|
||||
<button class="btn primary" onclick={save} disabled={saving}>
|
||||
{saving ? 'Saving…' : 'Save changes'}
|
||||
</button>
|
||||
<button class="btn danger-outline" onclick={() => (confirmDelete = true)} disabled={deleting}>
|
||||
<button
|
||||
class="btn danger-outline"
|
||||
onclick={() => (confirmDelete = true)}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Delete user'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -179,7 +192,9 @@
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.back-link:hover { color: var(--color-accent); }
|
||||
.back-link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--color-bg-elevated);
|
||||
@@ -257,8 +272,12 @@
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.toggle.on { background-color: var(--color-accent); }
|
||||
.toggle.on.danger { background-color: var(--color-danger); }
|
||||
.toggle.on {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
.toggle.on.danger {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.toggle .thumb {
|
||||
position: absolute;
|
||||
@@ -271,7 +290,9 @@
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.toggle.on .thumb { transform: translateX(18px); }
|
||||
.toggle.on .thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
@@ -290,7 +311,10 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background-color: var(--color-accent);
|
||||
@@ -316,8 +340,12 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.msg.error { color: var(--color-danger); }
|
||||
.msg.success { color: #7ECBA1; }
|
||||
.msg.error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.msg.success {
|
||||
color: #7ecba1;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
@@ -335,5 +363,9 @@
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user