- '?' help dialog (ShortcutHelpDialog) listing the current screen's shortcuts - Esc = Cancel on record forms; '/' focuses the list search - settings forms: Save and close (Ctrl/Cmd+Enter, then /settings) and Ctrl/Cmd+S - My account: Ctrl/Cmd+S - modal forms: Ctrl/Cmd+S and Ctrl/Cmd+Enter submit the innermost dialog only - docs: Keyboard shortcuts section in the admin SPA page
99 lines
4.0 KiB
Vue
99 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
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 ShortcutHelpDialog from './ShortcutHelpDialog.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'
|
|
import { closeShortcutHelp, installHotkeys } from '../../app/hotkeys'
|
|
|
|
// Navigation shell (design: Navigation shell and Top header; D-06, D-10):
|
|
// 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. The keyboard shortcuts (app/hotkeys.ts) are
|
|
// installed here, so only signed-in shell screens answer them.
|
|
const route = useRoute()
|
|
const { collapsed, watchViewport } = useSidebar()
|
|
|
|
const stopViewport = watchViewport()
|
|
onBeforeUnmount(stopViewport)
|
|
|
|
const uninstallHotkeys = installHotkeys()
|
|
onBeforeUnmount(() => {
|
|
uninstallHotkeys()
|
|
closeShortcutHelp()
|
|
})
|
|
|
|
// "View site" (WinterCMS parity) opens the public site in a new tab: the
|
|
// admin page's own origin, without the admin path prefix.
|
|
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 || settingsSection.value) && !collapsed.value
|
|
? 'calc(var(--spacing-rail) + var(--spacing-panel))'
|
|
: 'var(--spacing-rail)',
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<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 />
|
|
<div class="flex shrink-0 items-center gap-1">
|
|
<a
|
|
data-view-site
|
|
:href="siteURL"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
:title="t('backend::lang.nav.view_site')"
|
|
:aria-label="t('backend::lang.nav.view_site')"
|
|
class="flex size-input items-center justify-center rounded-inner text-muted transition-colors duration-150 ease-out hover:bg-hover hover:text-text"
|
|
>
|
|
<ExternalLink class="size-[18px]" aria-hidden="true" />
|
|
</a>
|
|
<UserMenu />
|
|
</div>
|
|
</header>
|
|
<main class="min-w-0 flex-1 px-8 py-7">
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
<Toast />
|
|
<ShortcutHelpDialog />
|
|
</div>
|
|
</template>
|