1
0

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

@@ -76,6 +76,10 @@ $csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';
Buffer::set('title', t('Address book'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Address book')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
Buffer::set(
'grid_csrf',

View File

@@ -16,13 +16,6 @@ $filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Address book')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Address book');
$listTitleActionsHtml = '';
require templatePath('partials/app-list-titlebar.phtml');

View File

@@ -36,3 +36,8 @@ Buffer::set('style_groups', json_encode(['address-book']));
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
$title = $name !== '' ? $name : ($user['email'] ?? t('Address book'));
Buffer::set('title', $title);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Address book'), 'path' => 'address-book'],
['label' => $title],
];

View File

@@ -8,4 +8,8 @@ Guard::requireLogin();
Guard::requireAbilityOrForbidden(ApiDocsAuthorizationPolicy::ABILITY_VIEW);
Buffer::set('title', t('API docs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API docs')],
];
Buffer::set('style_groups', json_encode(['api-docs']));

View File

@@ -1,10 +1,5 @@
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API docs')],
];
$specUrl = lurl('admin/api-docs/spec');
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-swagger-container">

View File

@@ -17,6 +17,10 @@ $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
);
Buffer::set('title', t('API audit logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API audit logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$filterSchema = require __DIR__ . '/filter-schema.php';

View File

@@ -11,13 +11,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API audit logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('API audit logs');
ob_start();
?>

View File

@@ -13,3 +13,8 @@ $auditLog = AuditPageHelper::findOrRedirect(
'View API audit entry',
$id ?? null
);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API audit logs'), 'path' => 'audit/api-audit'],
['label' => t('View')],
];

View File

@@ -51,13 +51,6 @@ $tokenTenantId = (int) ($auditLog['token_tenant_id'] ?? 0);
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('API audit logs'), 'path' => 'audit/api-audit'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('View API audit entry'),
'backHref' => 'audit/api-audit',

View File

@@ -17,6 +17,10 @@ $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
);
Buffer::set('title', t('Import audit logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Import audit logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$filterSchema = require __DIR__ . '/filter-schema.php';

View File

@@ -11,13 +11,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Import audit logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Import audit logs');
ob_start();
?>

View File

@@ -13,3 +13,8 @@ $auditRun = AuditPageHelper::findOrRedirect(
'View import audit entry',
$id ?? null
);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Import audit logs'), 'path' => 'audit/import-audit'],
['label' => t('View')],
];

View File

@@ -54,13 +54,6 @@ if ($tenantLabel === '') {
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Import audit logs'), 'path' => 'audit/import-audit'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('View import audit entry'),
'backHref' => 'audit/import-audit',

View File

@@ -17,6 +17,10 @@ $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
);
Buffer::set('title', t('System audit logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System audit logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$filterSchema = require __DIR__ . '/filter-schema.php';

View File

@@ -10,13 +10,6 @@ $filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System audit logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('System audit logs');

View File

@@ -13,3 +13,8 @@ $auditLog = AuditPageHelper::findOrRedirect(
'View system audit entry',
$id ?? null
);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System audit logs'), 'path' => 'audit/system-audit'],
['label' => t('View')],
];

View File

@@ -47,13 +47,6 @@ if ($tenantLabel === '') {
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System audit logs'), 'path' => 'audit/system-audit'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('View system audit entry'),
'backHref' => 'audit/system-audit',

View File

@@ -20,6 +20,10 @@ $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
);
Buffer::set('title', t('User lifecycle logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$filterSchema = require __DIR__ . '/filter-schema.php';

View File

@@ -11,13 +11,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('User lifecycle logs');
ob_start();
?>

View File

@@ -18,6 +18,11 @@ $auditLog = AuditPageHelper::findOrRedirect(
'View user lifecycle audit entry',
$id ?? null
);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs'), 'path' => 'audit/user-lifecycle-audit'],
['label' => t('View')],
];
$auditService = app(UserLifecycleAuditService::class);
$snapshot = null;

View File

@@ -29,13 +29,6 @@ $isRestorable = $canRestore
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs'), 'path' => 'audit/user-lifecycle-audit'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('View user lifecycle audit entry'),
'backHref' => 'audit/user-lifecycle-audit',

View File

@@ -126,3 +126,8 @@ $contactsFilterChipMeta = [
Buffer::set('title', $customerName . ' — ' . t('Helpdesk'));
Buffer::set('style_groups', json_encode(['helpdesk']));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
['label' => $customerName],
];

View File

@@ -37,15 +37,6 @@ $searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
data-controlling-dashboard-url="<?php e(lurl('helpdesk/debitor-controlling-dashboard-data')); ?>"
>
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
['label' => $customerName],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$titlebarActions = [];
if (!$hasError && $customer !== []) {

View File

@@ -38,3 +38,7 @@ $isConfigured = $settingsGateway->isConfigured();
Buffer::set('title', t('Customers'));
Buffer::set('style_groups', json_encode(['helpdesk']));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk')],
];

View File

@@ -9,13 +9,6 @@ $searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
$filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$isConfigured = $isConfigured ?? false;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php require templatePath('partials/app-flash.phtml'); ?>
<?php if (!$isConfigured): ?>

View File

@@ -163,3 +163,8 @@ $controllingConfigSource = (string) ($controllingConfigEnvelope['source'] ?? 'de
Buffer::set('title', t('Helpdesk settings'));
Buffer::set('style_groups', json_encode(['helpdesk']));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
['label' => t('Settings')],
];

View File

@@ -52,13 +52,6 @@ $isOAuth2 = $authMode === HelpdeskSettingsGateway::AUTH_MODE_OAUTH2;
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
['label' => t('Settings')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Helpdesk settings'),
'backHref' => lurl('helpdesk'),

View File

@@ -44,3 +44,11 @@ $ticketCommunicationUrl = lurl('helpdesk/ticket-communication-data');
Buffer::set('title', $ticketDescription . ' — ' . t('Helpdesk'));
Buffer::set('style_groups', json_encode(['helpdesk']));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
];
if ($ticketCustomerNo !== '') {
$breadcrumbs[] = ['label' => (string) ($ticket['Company_Contact_Name'] ?? $ticketCustomerNo), 'path' => 'helpdesk/debitor/' . rawurlencode($ticketCustomerNo)];
}
$breadcrumbs[] = ['label' => t('Ticket') . ' ' . $ticketNo];

View File

@@ -26,18 +26,6 @@ $backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($t
data-ticket-communication-url="<?php e($ticketCommunicationUrl); ?>"
>
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
];
if ($ticketCustomerNo !== '') {
$breadcrumbs[] = ['label' => (string) ($ticket['Company_Contact_Name'] ?? $ticketCustomerNo), 'path' => 'helpdesk/debitor/' . rawurlencode($ticketCustomerNo)];
}
$breadcrumbs[] = ['label' => t('Ticket') . ' ' . $ticketNo];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$titlebar = [
'title' => t('Ticket') . ' ' . $ticketNo,

View File

@@ -100,3 +100,8 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', t('Create department'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => t('Create department')],
];

View File

@@ -10,12 +10,6 @@
<section>
<?php
$hasTenantOptions = !empty($tenants);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => t('Create department')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Create department'),
'backHref' => 'admin/departments',

View File

@@ -151,4 +151,10 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $canUpdateDepartment ? t('Edit department') : t('View department'));
$titleText = $canUpdateDepartment ? t('Edit department') : t('View department');
Buffer::set('title', $titleText);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => $titleText],
];

View File

@@ -21,12 +21,6 @@ $titleText = $isReadOnly ? t('View department') : t('Edit department');
<section>
<?php
$hasTenantOptions = !empty($tenants);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/departments',

View File

@@ -42,6 +42,10 @@ usort($tenants, static function ($a, $b) {
});
Buffer::set('title', t('Departments'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -14,13 +14,6 @@ $activeTenant = (string) ($activeTenant ?? ($toolbarFilterState['tenant'] ?? '')
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Departments');
ob_start();
?>

View File

@@ -54,5 +54,11 @@ $currentSlug = $slug;
$currentTitle = (string) $allDocs[$slug];
Buffer::set('title', $currentTitle . ' - ' . t('Documentation'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Documentation'), 'path' => 'admin/docs/' . $defaultSlug],
['label' => $currentTitle],
];
Buffer::set('style_groups', json_encode(['docs']));
Buffer::set('docs_rendered_html', $renderedHtml);

View File

@@ -44,14 +44,6 @@
</aside>
<article class="app-docs-content">
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Documentation'), 'path' => 'admin/docs/' . $defaultSlug],
['label' => $currentTitle],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php MintyPHP\Buffer::get('docs_rendered_html'); ?>
</article>

View File

@@ -136,3 +136,8 @@ foreach ($profileOptions as $option) {
}
Buffer::set('title', t('Imports'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Imports')],
];

View File

@@ -1,10 +1,5 @@
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Imports')],
];
$hasAllowedProfile = !empty($allowedProfileOptions);
$renderErrorTable = static function (array $rows): void {
@@ -43,7 +38,6 @@ $renderErrorTable = static function (array $rows): void {
<div class="app-details-container">
<section>
<?php require templatePath('partials/app-breadcrumb.phtml'); ?>
<div class="app-details-titlebar" data-detail-unsaved-message="<?php e(t('You have unsaved changes. Leave without saving?')); ?>">
<h1><?php e(t('Imports')); ?></h1>
</div>

View File

@@ -11,3 +11,8 @@ $user = $session['user'] ?? null;
$first_name = $user['first_name'] ?? '';
Buffer::set('title', t('Admin'));
$breadcrumbs = [
['label' => t('Login'), 'path' => 'logout'],
['label' => t('Home')],
];

View File

@@ -6,13 +6,6 @@
*/
?>
<?php
$breadcrumbs = [
['label' => t('Login'), 'path' => 'logout'],
['label' => t('Home')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-dashboard-titlebar">
<h1><?php e(t('Welcome back')); ?> <?php e($first_name )?></h1>

View File

@@ -41,6 +41,10 @@ $clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
Buffer::set('title', t('Mail logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -10,13 +10,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Mail logs');
require templatePath('partials/app-list-titlebar.phtml');
?>

View File

@@ -17,3 +17,8 @@ if (!$mailLog) {
}
Buffer::set('title', t('View mail log'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs'), 'path' => 'admin/mail-log'],
['label' => t('View')],
];

View File

@@ -13,14 +13,6 @@ $statusBadge = $status->badgeVariant();
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs'), 'path' => 'admin/mail-log'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/mail-log" title="<?php e(t('Back')); ?>" class="app-icon-button"><i class="bi bi-arrow-left"></i></a>

View File

@@ -66,3 +66,8 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', t('Create permission'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => t('Create permission')],
];

View File

@@ -9,12 +9,6 @@
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => t('Create permission')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Create permission'),
'backHref' => 'admin/permissions',

View File

@@ -112,4 +112,10 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $canUpdatePermission ? t('Edit permission') : t('View permission'));
$titleText = $canUpdatePermission ? t('Edit permission') : t('View permission');
Buffer::set('title', $titleText);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => $titleText],
];

View File

@@ -13,18 +13,12 @@ use MintyPHP\Session;
$canUpdatePermission = (bool) ($canUpdatePermission ?? false);
$canDeletePermission = (bool) ($canDeletePermission ?? false);
$isReadOnly = !$canUpdatePermission;
$titleText = $isReadOnly ? t('View permission') : t('Edit permission');
$titleText = $titleText ?? ($isReadOnly ? t('View permission') : t('Edit permission'));
?>
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/permissions',

View File

@@ -49,6 +49,10 @@ $clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
Buffer::set('title', t('Permissions'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -12,13 +12,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Permissions');
ob_start();
?>

View File

@@ -116,3 +116,8 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', t('Create role'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => t('Create role')],
];

View File

@@ -9,12 +9,6 @@
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => t('Create role')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Create role'),
'backHref' => 'admin/roles',

View File

@@ -128,4 +128,10 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $canUpdateRole ? t('Edit role') : t('View role'));
$titleText = $canUpdateRole ? t('Edit role') : t('View role');
Buffer::set('title', $titleText);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => $titleText],
];

View File

@@ -21,12 +21,6 @@ $titleText = $isReadOnly ? t('View role') : t('Edit role');
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/roles',

View File

@@ -49,6 +49,10 @@ $clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
Buffer::set('title', t('Roles'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -12,13 +12,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Roles');
ob_start();
?>

View File

@@ -73,5 +73,11 @@ $scheduleSummary = $scheduledJobService->scheduleSummary($job ?? []);
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $canManage ? t('Edit scheduled job') : t('View scheduled job'));
$titleText = $canManage ? t('Edit scheduled job') : t('View scheduled job');
Buffer::set('title', $titleText);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Scheduled jobs'), 'path' => 'admin/scheduled-jobs'],
['label' => $titleText],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

View File

@@ -39,13 +39,6 @@ if ($canRunNow) {
<section>
<?php
$titleText = $canManage ? t('Edit scheduled job') : t('View scheduled job');
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Scheduled jobs'), 'path' => 'admin/scheduled-jobs'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/scheduled-jobs',

View File

@@ -46,4 +46,8 @@ $clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
Buffer::set('title', t('Scheduled jobs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Scheduled jobs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

View File

@@ -10,13 +10,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Scheduled jobs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Scheduled jobs');
ob_start();
?>

View File

@@ -81,3 +81,8 @@ if ($request->isMethod('POST')) {
}
Buffer::set('title', t('Settings'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Settings')],
];

View File

@@ -104,11 +104,6 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Settings')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Settings'),
'actions' => $canUpdateSettings ? [

View File

@@ -17,3 +17,8 @@ $currentUserId = (int) ($session['user']['id'] ?? 0);
$viewAuth['page'] = app(UiAccessService::class)->pageCapabilities($currentUserId, UiCapabilityMap::PAGE_STATS_INDEX);
Buffer::set('title', t('Statistics'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Statistics')],
];

View File

@@ -107,13 +107,6 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
|| ($canViewUserLifecycleAudit && (bool) ($userLifecycleAuditStatsAvailable ?? false))
);
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Statistics')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-dashboard-titlebar">
<h1><?php e(t('Statistics')); ?></h1>

View File

@@ -14,3 +14,8 @@ $permissions = $pageData['permissions'] ?? [];
$healthChecks = $overview['health_checks'] ?? [];
Buffer::set('title', t('System Info'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System Info')],
];

View File

@@ -12,13 +12,6 @@ $modules = $modules ?? [];
$permissions = $permissions ?? [];
$healthChecks = $healthChecks ?? [];
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('System Info')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-dashboard-titlebar">
<h1><?php e(t('System Info')); ?></h1>

View File

@@ -135,3 +135,8 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', t('Create tenant'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants'), 'path' => 'admin/tenants'],
['label' => t('Create tenant')],
];

View File

@@ -11,12 +11,6 @@
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants'), 'path' => 'admin/tenants'],
['label' => t('Create tenant')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Create tenant'),
'backHref' => 'admin/tenants',

View File

@@ -182,4 +182,10 @@ if ($canManageSso && empty($ldapUiState)) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $canUpdateTenant ? t('Edit tenant') : t('View tenant'));
$titleText = $canUpdateTenant ? t('Edit tenant') : t('View tenant');
Buffer::set('title', $titleText);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants'), 'path' => 'admin/tenants'],
['label' => $titleText],
];

View File

@@ -27,12 +27,6 @@ $hasFavicon = $avatarUuid !== '' && $tenantFaviconService->hasFavicon($avatarUui
<div class="app-details-container">
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants'), 'path' => 'admin/tenants'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/tenants',

View File

@@ -46,6 +46,10 @@ $clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
Buffer::set('title', t('Tenants'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -12,13 +12,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Tenants')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Tenants');
ob_start();
?>

View File

@@ -214,3 +214,8 @@ if (!$customFieldDefinitionsByTenant && $selectedTenantIds) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', t('Create user'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users'), 'path' => 'admin/users'],
['label' => t('Create user')],
];

View File

@@ -12,12 +12,6 @@
<section>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users'), 'path' => 'admin/users'],
['label' => t('Create user')],
];
require templatePath('partials/app-breadcrumb.phtml');
$titlebar = [
'title' => t('Create user'),
'backHref' => 'admin/users',

View File

@@ -300,4 +300,12 @@ if ($request->isMethod('POST')) {
$validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList();
Buffer::set('title', $isOwnAccount ? t('My account') : ($canEditUser ? t('Edit user') : t('View user')));
$titleText = $isOwnAccount ? t('My account') : ($canEditUser ? t('Edit user') : t('View user'));
Buffer::set('title', $titleText);
if (!($isOwnAccount && !$canViewUsers)) {
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users'), 'path' => 'admin/users'],
['label' => $titleText],
];
}

View File

@@ -46,16 +46,6 @@ if ($lastLoginAt !== '') {
<div class="app-details-container">
<section>
<?php if (!$hideNavigation): ?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users'), 'path' => 'admin/users'],
['label' => $titleText],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php endif; ?>
<?php
$asideActions = [];
$moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleSlots'] : [];

View File

@@ -56,6 +56,10 @@ $viewAuth['page'] = [
];
Buffer::set('title', t('Users'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';

View File

@@ -22,13 +22,6 @@ $currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Users');
ob_start();
?>

View File

@@ -13,7 +13,6 @@ $hotkeyRows = HotkeyService::list();
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Keyboard shortcuts')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php

View File

@@ -29,6 +29,11 @@ $csrfKey = Session::$csrfSessionKey;
$csrfToken = $session[$csrfKey] ?? '';
Buffer::set('title', t('Search results'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Search')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
Buffer::set(
'grid_csrf',

View File

@@ -8,13 +8,6 @@ $clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchem
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Search')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Search results');
require templatePath('partials/app-list-titlebar.phtml');
?>

View File

@@ -1,6 +1,5 @@
<?php
$breadcrumbs = $breadcrumbs ?? [];
$items = array_values(array_filter($breadcrumbs, static function ($item) {
$items = array_values(array_filter($breadcrumbs ?? [], static function ($item) {
return !empty($item['label']);
}));
if (!$items) {

View File

@@ -27,17 +27,16 @@ $moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleS
$moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $moduleSlots['topbar.right_item'] : [];
?>
<header class="app-header" data-app-component="nav-history">
<header class="app-header">
<nav class="app-topbar">
<ul class="app-topbar-left">
<li>
<a id="global-back" data-tooltip-pos="bottom" data-tooltip="<?php e(t('Back')); ?> (Alt+←)" title="<?php e(t('Back')); ?> (Alt+←)" aria-label="<?php e(t('Back')); ?> (Alt+←)" href="admin"><i class="bi bi-arrow-left"></i></a>
</li>
<li>
<a id="global-forward" data-tooltip-pos="bottom" data-tooltip="<?php e(t('Forward')); ?> (Alt+→)" title="<?php e(t('Forward')); ?> (Alt+→)" aria-label="<?php e(t('Forward')); ?> (Alt+→)" href="admin"><i class="bi bi-arrow-right"></i></a>
</li>
<li id="async-messages" role="status" aria-live="polite" aria-hidden="true"></li>
</ul>
<div class="app-topbar-left">
<?php
if (is_array($breadcrumbs ?? null) && $breadcrumbs !== []) {
require __DIR__ . '/app-breadcrumb.phtml';
}
?>
<span id="async-messages" role="status" aria-live="polite" aria-hidden="true"></span>
</div>
<div class="app-topbar-center">
<button type="button" class="app-topbar-search-trigger" data-app-search-trigger aria-label="<?php e(t('Search')); ?>" title="<?php e(t('Search')); ?>">
<i class="bi bi-search" aria-hidden="true"></i>

View File

@@ -15,7 +15,6 @@ trait FrontendRuntimeContractSupport
'web/js/components/app-password-hints.js',
'web/js/components/app-copy-badge.js',
'web/js/components/app-multiselect-init.js',
'web/js/components/app-nav-history.js',
'web/js/components/app-toggle-sidebar.js',
'web/js/components/app-toggle-aside-panels.js',
'web/js/components/app-toggle-details-aside.js',

View File

@@ -25,7 +25,6 @@ class FrontendRuntimeHostContractTest extends TestCase
$this->assertStringNotContainsString("assetVersion('js/components/app-bookmark-init.js')", $defaultTemplate);
$this->assertStringContainsString('data-app-component="flash-auto-dismiss"', $flashPartial);
$this->assertStringContainsString('data-app-component="nav-history"', $topbarPartial);
$this->assertStringContainsString('data-app-component="tenant-switcher"', $topbarPartial);
$this->assertStringContainsString('data-app-component="theme-controls"', $topbarPartial);
$this->assertStringContainsString('data-app-component="contrast-toggle"', $topbarPartial);

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 };
}