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:
Jakub Zych
2026-10-22 13:50:39 +02:00
parent a97743a39f
commit 834ef98572
5 changed files with 152 additions and 6 deletions

View File

@@ -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 />