feat(quick-261022-imo): settings section panel scoped to the current category
- settingsSectionOf returns the current entry's category group (null for one-entry categories) - SettingsPanel mirrors the plugin side menu, collapse included - AppShell shows it on settings forms and settings-linked controllers without own navigation - docs/backend/settings.md describes moving between settings pages
This commit is contained in:
@@ -3,16 +3,20 @@ import { computed, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import PluginRail from './PluginRail.vue'
|
||||
import SectionPanel from './SectionPanel.vue'
|
||||
import SettingsPanel from './SettingsPanel.vue'
|
||||
import Breadcrumbs from './Breadcrumbs.vue'
|
||||
import UserMenu from './UserMenu.vue'
|
||||
import Toast from '../ui/Toast.vue'
|
||||
import { ExternalLink } from '@lucide/vue'
|
||||
import { controllerIdFromPath } from '../../app/controllerRoutes'
|
||||
import { activeEntry } from '../../state/useNavigation'
|
||||
import { settingsEntry, settingsLinkFor, settingsSectionOf } from '../../state/useSettings'
|
||||
import { useSidebar } from '../../state/useSidebar'
|
||||
import { t } from '../../app/i18n'
|
||||
|
||||
// Navigation shell (design: Navigation shell and Top header; D-06, D-10):
|
||||
// the plugin rail, the active plugin's section panel (hidden when the
|
||||
// the plugin rail, the section panel (the active plugin's side menu or, on a
|
||||
// settings page, the settings panel of its category; 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.
|
||||
@@ -28,11 +32,27 @@ const siteURL = `${window.location.origin}/`
|
||||
|
||||
const active = computed(() => activeEntry(String(route.params.vendor ?? ''), String(route.params.plugin ?? '')))
|
||||
|
||||
// The settings panel shows on a singleton settings form and on a controller a
|
||||
// settings link entry opens, unless that controller's plugin has a navigation
|
||||
// entry of its own (it keeps its plugin panel, as the rail's Settings rule
|
||||
// does). The /settings index has no controller ID and resolves to null.
|
||||
const settingsSection = computed(() => {
|
||||
if (active.value) {
|
||||
return null
|
||||
}
|
||||
if (route.name === 'settings-form') {
|
||||
return settingsSectionOf(settingsEntry(String(route.params.code ?? '')))
|
||||
}
|
||||
return settingsSectionOf(settingsLinkFor(controllerIdFromPath(route.path)))
|
||||
})
|
||||
|
||||
// --shell-offset is the width the sidebar occupies (the rail, plus the
|
||||
// section panel when it shows). Fixed bottom save bars start there, so the
|
||||
// rail's Settings item is never covered.
|
||||
const shellOffset = computed(() =>
|
||||
active.value && !collapsed.value ? 'calc(var(--spacing-rail) + var(--spacing-panel))' : 'var(--spacing-rail)',
|
||||
(active.value || settingsSection.value) && !collapsed.value
|
||||
? 'calc(var(--spacing-rail) + var(--spacing-panel))'
|
||||
: 'var(--spacing-rail)',
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -40,6 +60,7 @@ const shellOffset = computed(() =>
|
||||
<div class="flex min-h-screen bg-bg text-text" :style="{ '--shell-offset': shellOffset }">
|
||||
<PluginRail />
|
||||
<SectionPanel v-if="active && !collapsed" :entry="active" />
|
||||
<SettingsPanel v-else-if="settingsSection && !collapsed" :section="settingsSection" />
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="flex h-header shrink-0 items-center justify-between gap-4 border-b border-border bg-surface pr-6 pl-8">
|
||||
<Breadcrumbs />
|
||||
|
||||
68
admin/src/components/shell/SettingsPanel.vue
Normal file
68
admin/src/components/shell/SettingsPanel.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick } from 'vue'
|
||||
import { PanelLeftClose } from '@lucide/vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { iconFor } from '../../app/icons'
|
||||
import { t } from '../../app/i18n'
|
||||
import { setCollapsed } from '../../state/useSidebar'
|
||||
import { settingsPath, type SettingsSection } from '../../state/useSettings'
|
||||
|
||||
// Settings section panel (design: Navigation shell, level 2): on a settings
|
||||
// page, the permitted settings pages of the current entry's category, 224px,
|
||||
// with the same look and the same "Zwiń menu" collapse as the plugin side
|
||||
// menu (SectionPanel). It lists only the current category; switching
|
||||
// category goes through /settings. Categories arrive translated from the
|
||||
// server, so the header renders the category as is.
|
||||
//
|
||||
// There is no settings flyout on the rail: one would list either every
|
||||
// category (the panel never shows other categories) or only the current one
|
||||
// (meaningless off a settings page). The rail's Settings item opens
|
||||
// /settings, which lists every permitted page grouped by category one click
|
||||
// away; on narrow viewports, where the panel is forced closed, that index is
|
||||
// also how settings pages are switched.
|
||||
|
||||
defineProps<{ section: SettingsSection }>()
|
||||
|
||||
function collapse(): void {
|
||||
setCollapsed(true)
|
||||
void nextTick(() => document.querySelector<HTMLElement>('[data-sidebar-expand]')?.focus())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
data-settings-panel
|
||||
:aria-label="t('backend::lang.nav.sections')"
|
||||
class="sticky top-0 flex h-screen w-panel shrink-0 flex-col gap-0.5 self-start overflow-y-auto border-r border-border bg-surface px-3 py-3.5"
|
||||
>
|
||||
<div class="mb-3.5 flex h-10 items-center justify-between gap-2 pl-2">
|
||||
<span class="truncate text-[16px] font-bold">{{ section.category || t('backend::lang.nav.settings') }}</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="entry in section.entries"
|
||||
:key="entry.code"
|
||||
:to="settingsPath(entry)"
|
||||
:data-settings-panel-item="entry.code"
|
||||
:aria-current="entry.code === section.current.code ? 'page' : undefined"
|
||||
:class="
|
||||
entry.code === section.current.code
|
||||
? '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 transition-colors duration-150 ease-out"
|
||||
>
|
||||
<component :is="iconFor(entry.icon)" :size="18" aria-hidden="true" />
|
||||
<span>{{ entry.label }}</span>
|
||||
</RouterLink>
|
||||
</aside>
|
||||
</template>
|
||||
Reference in New Issue
Block a user