Files
familienarchiv/frontend/src/routes/admin/groups/GroupsListPanel.svelte
Marcel 3c54401bb2 feat(admin): responsive entity nav and collapsible list panels (Phase 9)
EntityNav: hidden on mobile, 48px icon strip at tablet (md), full labels+counts at desktop (lg).
Each list panel collapses to a 32px handle via localStorage-persisted state; auto-collapses when
navigating to the "+New" route. Mobile routing hides the list panel when a detail route is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 07:19:41 +02:00

112 lines
3.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { page } from '$app/state';
import { m } from '$lib/paraglide/messages.js';
type Group = {
id: string;
name: string;
permissions: string[];
};
let {
groups,
autocollapse = false
}: {
groups: Group[];
autocollapse?: boolean;
} = $props();
let isCollapsed = $state(
typeof localStorage !== 'undefined' && localStorage.getItem('admin_list_collapsed') === 'true'
);
$effect(() => {
if (autocollapse) isCollapsed = true;
});
$effect(() => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('admin_list_collapsed', String(isCollapsed));
}
});
</script>
{#if isCollapsed}
<!-- Collapsed handle: 32px -->
<button
onclick={() => (isCollapsed = false)}
aria-label={m.admin_btn_expand_list()}
class="flex w-8 flex-shrink-0 flex-col items-center gap-2 border-r border-line bg-surface pt-2 hover:bg-muted"
>
<span class="text-sm font-bold text-ink-2"></span>
<span
class="text-[8px] font-extrabold tracking-widest text-ink-3 uppercase"
style="writing-mode: vertical-rl; transform: rotate(180deg);"
>
{m.admin_tab_groups()}
</span>
</button>
{:else}
<div
class="flex w-[200px] flex-shrink-0 flex-col overflow-hidden border-r border-line bg-surface"
>
<!-- Panel header -->
<div class="flex items-center justify-between border-b border-line px-3 py-2">
<span class="text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.admin_groups_list_title()}
</span>
<div class="flex items-center gap-1">
<a
href="/admin/groups/new"
class="inline-flex items-center gap-1 rounded-sm px-2 py-1 text-xs font-medium text-ink-2 transition-colors hover:bg-muted hover:text-ink"
title={m.admin_btn_new_group()}
aria-label={m.admin_btn_new_group()}
>
<svg
class="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
</a>
<button
onclick={() => (isCollapsed = true)}
aria-label={m.admin_btn_collapse_list()}
class="flex h-6 w-6 items-center justify-center rounded-sm text-xs font-bold text-ink-2 transition-colors hover:bg-muted"
>
</button>
</div>
</div>
<!-- Scrollable group list -->
<div class="flex-1 overflow-y-auto">
{#if groups.length === 0}
<p class="px-4 py-6 text-center text-xs text-ink-3">
{m.admin_groups_empty()}
</p>
{:else}
{#each groups as group (group.id)}
{@const isActive = page.url.pathname.startsWith('/admin/groups/' + group.id)}
<a
href="/admin/groups/{group.id}"
aria-current={isActive ? 'page' : undefined}
class="block border-l-2 px-3 py-2.5 transition-colors {isActive
? 'border-primary bg-primary/10 dark:bg-primary/15'
: 'border-transparent hover:bg-muted'}"
>
<div class="text-sm font-bold text-ink">{group.name}</div>
<div class="mt-0.5 text-xs text-ink-3">
{m.admin_groups_permission_count({ count: group.permissions.length })}
</div>
</a>
{/each}
{/if}
</div>
</div>
{/if}