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:
@@ -1,72 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ChevronRight } from '@lucide/vue'
|
||||
import PluginRail from './PluginRail.vue'
|
||||
import SectionPanel from './SectionPanel.vue'
|
||||
import Breadcrumbs from './Breadcrumbs.vue'
|
||||
import UserMenu from './UserMenu.vue'
|
||||
import Toast from '../ui/Toast.vue'
|
||||
import { t } from '../../app/i18n'
|
||||
import { controllerIdFromPath } from '../../app/controllerRoutes'
|
||||
import { currentUser } from '../../state/useAuth'
|
||||
import { activeEntry } from '../../state/useNavigation'
|
||||
import { useSidebar } from '../../state/useSidebar'
|
||||
|
||||
// Navigation shell (design: Navigation shell and Top header; D-06, D-10):
|
||||
// the plugin rail, the active plugin's section panel (hidden when the
|
||||
// sidebar is collapsed by the viewport or by the admin), and the 64px header
|
||||
// with breadcrumbs and the user menu. The toast queue is mounted here so
|
||||
// toasts survive navigation.
|
||||
const route = useRoute()
|
||||
const { collapsed, watchViewport } = useSidebar()
|
||||
|
||||
const stopViewport = watchViewport()
|
||||
onBeforeUnmount(stopViewport)
|
||||
|
||||
const active = computed(() => activeEntry(String(route.params.vendor ?? ''), String(route.params.plugin ?? '')))
|
||||
|
||||
const section = computed(() => {
|
||||
const id = controllerIdFromPath(route.path)
|
||||
return active.value?.sideMenu.find((item) => item.controller === id) ?? null
|
||||
})
|
||||
|
||||
const displayName = computed(() => {
|
||||
const user = currentUser.value
|
||||
if (!user) {
|
||||
return ''
|
||||
}
|
||||
const name = `${user.first_name} ${user.last_name}`.trim()
|
||||
return name === '' ? user.login : name
|
||||
})
|
||||
|
||||
const initials = computed(() =>
|
||||
displayName.value
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part.charAt(0).toUpperCase())
|
||||
.join(''),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-screen bg-bg text-text">
|
||||
<PluginRail />
|
||||
<SectionPanel v-if="active" :entry="active" />
|
||||
<SectionPanel v-if="active && !collapsed" :entry="active" />
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="flex h-header shrink-0 items-center justify-between border-b border-border bg-surface pr-6 pl-8">
|
||||
<nav :aria-label="t('backend::lang.nav.breadcrumbs')" class="flex items-center gap-2">
|
||||
<span v-if="route.name === 'settings' || route.name === 'settings-form'" class="font-semibold">
|
||||
{{ t('backend::lang.nav.settings') }}
|
||||
</span>
|
||||
<template v-else-if="active">
|
||||
<span :class="section ? 'text-muted' : 'font-semibold'">{{ active.label }}</span>
|
||||
<template v-if="section">
|
||||
<ChevronRight :size="14" class="text-muted" aria-hidden="true" />
|
||||
<span class="font-semibold" aria-current="page">{{ section.label }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
<div v-if="currentUser" class="flex h-input items-center gap-2.5 rounded-inner px-2">
|
||||
<span
|
||||
class="flex size-[34px] items-center justify-center rounded-full bg-accent text-[13px] font-bold text-on-accent"
|
||||
aria-hidden="true"
|
||||
>{{ initials }}</span
|
||||
>
|
||||
<span class="flex flex-col leading-tight">
|
||||
<span class="font-semibold">{{ displayName }}</span>
|
||||
<span v-if="currentUser.role" class="text-[12px] text-muted">{{ currentUser.role.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<header class="flex h-header shrink-0 items-center justify-between gap-4 border-b border-border bg-surface pr-6 pl-8">
|
||||
<Breadcrumbs />
|
||||
<UserMenu />
|
||||
</header>
|
||||
<main class="min-w-0 flex-1 px-8 py-7">
|
||||
<slot />
|
||||
|
||||
Reference in New Issue
Block a user