feat: centralize breadcrumbs in topbar and remove history navigation

Move breadcrumb rendering from individual page templates into the core
topbar. Each page now sets $breadcrumbs in its action .php file; the
topbar renders it automatically via the shared partial.

- Remove global back/forward buttons and app-nav-history.js component
- Remove Alt+Arrow keyboard shortcuts for history navigation
- Render breadcrumb in topbar-left section (replaces button area)
- Clean up breadcrumb CSS: context-neutral base (flex, no margin)
- Recalculate sticky titlebar offset in details container
- Migrate all 41 pages (core + helpdesk, audit, addressbook, api-docs)
- Add missing breadcrumbs to addressbook detail view
- Update architecture contract tests (nav-history references removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 17:17:06 +02:00
parent 7220aa7459
commit b749b5d192
91 changed files with 231 additions and 436 deletions

View File

@@ -6,12 +6,11 @@
* (app-shell.css lines 2844-2956) to avoid layout side-effects.
*/
.app-breadcrumb {
display: block;
justify-content: initial;
overflow: initial;
margin-bottom: 0.375rem;
display: flex;
align-items: center;
margin: 0;
font-size: var(--text-xs);
line-height: var(--app-line-height);
line-height: var(--leading-tight);
}
.app-breadcrumb ol {

View File

@@ -119,24 +119,12 @@
padding-inline: calc(var(--app-spacing) * 2);
}
.app-details-container .app-breadcrumb {
padding-top: calc(var(--app-spacing) * 2);
position: sticky;
top: 49px;
z-index: 10;
background: var(--app-background-color);
}
.app-details-container .app-details-titlebar {
position: sticky;
top: 93px;
z-index: 10;
background: var(--app-background-color);
}
.app-details-container:not(:has(.app-breadcrumb)) .app-details-titlebar {
top: 37px;
padding-block-start: var(--app-spacing);
z-index: 10;
padding-block-start: calc(var(--app-spacing) * 2);
background: var(--app-background-color);
}
@media (max-width: 768px) {

View File

@@ -122,6 +122,10 @@
}
@media (max-width: 576px) {
.app-topbar-left .app-breadcrumb {
display: none;
}
.app-topbar-search-trigger-text,
.app-topbar-search-trigger-shortcut {
display: none;
@@ -160,9 +164,7 @@
color var(--app-transition);
}
.app-topbar-left > li > a,
.app-topbar-right > li > a,
.app-topbar-left button,
.app-topbar-right button {
display: flex;
align-items: center;
@@ -181,39 +183,26 @@
color var(--app-transition);
}
.app-topbar-left > li > a:hover,
.app-topbar-right > li > a:hover,
.app-topbar-left button:hover,
.app-topbar-right button:hover,
.app-header details.dropdown > summary:not([role]):hover {
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
color: var(--app-color);
}
.app-topbar-left > li > a:active,
.app-topbar-right > li > a:active,
.app-topbar-left button:active,
.app-topbar-right button:active,
.app-header details.dropdown > summary:not([role]):active {
background: color-mix(in srgb, var(--app-contrast) 12%, transparent);
}
.app-topbar-left > li > a:focus-visible,
.app-topbar-right > li > a:focus-visible,
.app-topbar-left button:focus-visible,
.app-topbar-right button:focus-visible,
.app-header details.dropdown > summary:not([role]):focus-visible {
outline: 2px solid var(--app-primary);
outline-offset: -2px;
}
.app-header #global-back.is-disabled,
.app-header #global-forward.is-disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.app-topbar-tenant-chip {
display: inline-flex;
align-items: center;

View File

@@ -5,7 +5,6 @@ import { initFlashAutoDismiss } from './components/app-flash-auto-dismiss.js';
import { initPasswordHints } from './components/app-password-hints.js';
import { initBadgeCopy } from './components/app-copy-badge.js';
import { initMultiSelectComponent } from './components/app-multiselect-init.js';
import { initNavHistory } from './components/app-nav-history.js';
import { initSidebarToggle } from './components/app-toggle-sidebar.js';
import { initAsidePanels } from './components/app-toggle-aside-panels.js';
import { initDetailsAsideToggle } from './components/app-toggle-details-aside.js';
@@ -177,10 +176,6 @@ const bootRuntime = async () => {
scope: 'global',
configPath: 'components.multiSelect',
});
runtime.register('nav-history', initNavHistory, {
selector: '[data-app-component="nav-history"]',
configPath: 'components.navHistory',
});
runtime.register('details-state', initDetailsState, {
scope: 'global',
configPath: 'components.detailsState',

View File

@@ -1,96 +0,0 @@
/**
* Browser history integration — handles back/forward navigation state.
*/
import { isEditableTarget } from '../core/app-form-utils.js';
import { resolveHost } from '../core/app-dom.js';
export function initNavHistory(root = document, options = {}) {
const {
backSelector = '#global-back',
forwardSelector = '#global-forward',
} = options;
const host = resolveHost(root);
const back = host.querySelector(backSelector);
const forward = host.querySelector(forwardSelector);
if (!back && !forward) {
return { destroy: () => {} };
}
const setDisabledState = (link, disabled) => {
link.setAttribute('aria-disabled', disabled ? 'true' : 'false');
link.classList.toggle('is-disabled', disabled);
if (disabled) {
link.setAttribute('tabindex', '-1');
} else {
link.removeAttribute('tabindex');
}
};
const updateHistoryButtons = () => {
const hasHistory = window.history.length > 1;
if (back instanceof HTMLElement) {
setDisabledState(back, !hasHistory);
}
if (forward instanceof HTMLElement) {
setDisabledState(forward, !hasHistory);
}
};
const onKeyDown = (event) => {
if (!event.altKey || event.metaKey || event.ctrlKey) {
return;
}
if (isEditableTarget(event.target)) {
return;
}
if (event.key === 'ArrowLeft') {
if (window.history.length > 1) {
event.preventDefault();
window.history.back();
}
} else if (event.key === 'ArrowRight') {
if (window.history.length > 1) {
event.preventDefault();
window.history.forward();
}
}
};
document.addEventListener('keydown', onKeyDown);
const onBackClick = (event) => {
event.preventDefault();
if (window.history.length > 1) {
window.history.back();
}
};
if (back instanceof HTMLElement) {
back.addEventListener('click', onBackClick);
}
const onForwardClick = (event) => {
event.preventDefault();
if (window.history.length > 1) {
window.history.forward();
}
};
if (forward instanceof HTMLElement) {
forward.addEventListener('click', onForwardClick);
}
window.addEventListener('popstate', updateHistoryButtons);
updateHistoryButtons();
const destroy = () => {
document.removeEventListener('keydown', onKeyDown);
if (back instanceof HTMLElement) {
back.removeEventListener('click', onBackClick);
}
if (forward instanceof HTMLElement) {
forward.removeEventListener('click', onForwardClick);
}
window.removeEventListener('popstate', updateHistoryButtons);
};
return { destroy, update: updateHistoryButtons };
}