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>
112 lines
3.1 KiB
Svelte
112 lines
3.1 KiB
Svelte
<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}
|