feat(10-04): collapsible panel with flyout, user menu with logout, breadcrumbs and dark mode

- useSidebar: collapsed below 1100px (matchMedia) or by the admin's choice,
  persisted as a boolean under summer-admin.sidebar; the viewport never
  overwrites the stored choice
- SectionPanel collapse and rail expand buttons; SectionFlyout (role menu)
  opens on hover, focus, Enter or ArrowDown on a rail item, closes on Esc or
  about 200 ms after leaving and returns focus to the rail item
- Breadcrumbs with plugin, controller and record crumbs; UserMenu (Reka
  DropdownMenu) with initials, name, role and Wyloguj
- useAuth.logout POSTs /auth/logout, clears user, navigation and settings
  and routes to login even when the call fails
- applyColorScheme toggles .dark from prefers-color-scheme (no toggle, A6)
- newest toast first, 200 ms fade and scale for dialogs and toasts
- one relation-manager type constant in the registry
- shell smoke tests; tests default to a desktop, light matchMedia
This commit is contained in:
Jakub Zych
2026-09-27 17:26:44 +02:00
parent f4e97cccad
commit 445404e394
24 changed files with 1056 additions and 77 deletions

View File

@@ -1,10 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, nextTick } from 'vue'
import { PanelLeftClose } from '@lucide/vue'
import { RouterLink, useRoute } from 'vue-router'
import type { NavigationEntry } from '../../api/types'
import { controllerIdFromPath, controllerPath } from '../../app/controllerRoutes'
import { iconFor } from '../../app/icons'
import { t } from '../../app/i18n'
import { setCollapsed } from '../../state/useSidebar'
// Section panel (design: Navigation shell, level 2): the active plugin's
// permitted side menu, 224px, with the plugin label and a "Zwiń menu" button
// in its header. Collapsing hides it and leaves only the rail (D-06).
const props = defineProps<{ entry: NavigationEntry }>()
const route = useRoute()
@@ -20,6 +26,11 @@ const items = computed(() =>
return [{ item, to, icon: iconFor(item.icon), current: currentController.value === item.controller }]
}),
)
function collapse(): void {
setCollapsed(true)
void nextTick(() => document.querySelector<HTMLElement>('[data-sidebar-expand]')?.focus())
}
</script>
<template>
@@ -27,8 +38,18 @@ const items = computed(() =>
:aria-label="t('backend::lang.nav.sections')"
class="flex w-panel shrink-0 flex-col gap-0.5 border-r border-border bg-surface px-3 py-3.5"
>
<div class="mb-3.5 flex h-10 items-center px-2">
<span class="text-[16px] font-bold">{{ entry.label }}</span>
<div class="mb-3.5 flex h-10 items-center justify-between gap-2 pl-2">
<span class="truncate text-[16px] font-bold">{{ entry.label }}</span>
<button
type="button"
data-sidebar-collapse
:aria-label="t('backend::lang.nav.collapse')"
:title="t('backend::lang.nav.collapse')"
class="flex size-[30px] shrink-0 items-center justify-center rounded-tab text-muted transition-colors duration-150 ease-out hover:bg-hover hover:text-text"
@click="collapse"
>
<PanelLeftClose :size="18" aria-hidden="true" />
</button>
</div>
<RouterLink
v-for="link in items"
@@ -36,7 +57,7 @@ const items = computed(() =>
:to="link.to"
:aria-current="link.current ? 'page' : undefined"
:class="link.current ? 'bg-sel font-semibold text-text' : 'font-medium text-muted hover:bg-hover hover:text-text'"
class="flex h-nav items-center gap-3 rounded-control px-3 no-underline"
class="flex h-nav items-center gap-3 rounded-control px-3 no-underline transition-colors duration-150 ease-out"
>
<component :is="link.icon" :size="18" aria-hidden="true" />
<span>{{ link.item.label }}</span>