1
0
Files
breadcrumb-the-shire/templates/default.phtml

373 lines
13 KiB
PHTML
Raw Normal View History

2026-02-04 23:31:53 +01:00
<?php
use MintyPHP\Buffer;
2026-03-06 00:44:52 +01:00
use MintyPHP\Http\RequestContext;
use MintyPHP\Session;
2026-02-04 23:31:53 +01:00
$defaultTitle = appTitle();
$user = $_SESSION['user'] ?? [];
2026-02-11 19:28:12 +01:00
$theme = currentTheme();
2026-02-04 23:31:53 +01:00
$primaryVars = appPrimaryCssVars();
2026-03-06 00:44:52 +01:00
$csrfKey = Session::$csrfSessionKey;
$csrfToken = (string) ($_SESSION[$csrfKey] ?? '');
$isLoggedIn = !empty($user['id']);
2026-03-13 14:38:58 +01:00
$sessionIdleMinutes = (int) (appSetting('session_idle_timeout_minutes') ?? 30);
$sessionIdleSeconds = max($sessionIdleMinutes, 5) * 60;
$sessionPingUrl = lurl('admin/session-ping/data');
$sessionLogoutUrl = lurl('logout');
$sessionLogoutFormId = 'app-logout-form';
2026-03-06 00:44:52 +01:00
$telemetryEnabled = frontendTelemetryEnabled();
$telemetrySampleRate = frontendTelemetrySampleRate();
$telemetryAllowedEvents = implode(',', frontendTelemetryAllowedEvents());
$telemetryUrl = lurl('admin/frontend-telemetry/ingest');
$pageRequestId = RequestContext::id();
2026-03-04 15:56:58 +01:00
$viewAuth = is_array($viewAuth ?? null) ? $viewAuth : [];
$viewAuth['layout'] = is_array($viewAuth['layout'] ?? null) ? $viewAuth['layout'] : [];
$layoutAuth = $viewAuth['layout'];
$layoutNav = is_array($layoutNav ?? null) ? $layoutNav : [];
$moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleSlots'] : [];
$moduleHeadStyleSlots = is_array($moduleSlots['layout.head_style'] ?? null) ? $moduleSlots['layout.head_style'] : [];
$moduleBodyEndTemplateSlots = is_array($moduleSlots['layout.body_end_template'] ?? null) ? $moduleSlots['layout.body_end_template'] : [];
$moduleRuntimeComponentSlots = is_array($moduleSlots['runtime.component'] ?? null) ? $moduleSlots['runtime.component'] : [];
/**
* @param array<mixed> $value
*/
$isAssocArray = static function (array $value): bool {
if ($value === []) {
return false;
}
return array_keys($value) !== range(0, count($value) - 1);
};
/**
* @param array<string, mixed> $target
* @param array<string, mixed> $source
* @return array<string, mixed>
*/
$strictMergeConfig = static function (array $target, array $source, string $context, callable $isAssocArray) use (&$strictMergeConfig): array {
foreach ($source as $key => $sourceValue) {
if (!is_string($key) || trim($key) === '') {
throw new RuntimeException("Invalid default_config key in {$context}.");
}
if (!array_key_exists($key, $target)) {
$target[$key] = $sourceValue;
continue;
}
$targetValue = $target[$key];
if (is_array($targetValue) && is_array($sourceValue) && $isAssocArray($targetValue) && $isAssocArray($sourceValue)) {
$target[$key] = $strictMergeConfig($targetValue, $sourceValue, $context . '.' . $key, $isAssocArray);
continue;
}
if ($targetValue !== $sourceValue) {
throw new RuntimeException("default_config collision at {$context}.{$key}.");
}
}
return $target;
};
/**
* @param array<string, mixed> $root
* @param array<string, mixed> $value
*/
$mergeConfigAtPath = static function (array &$root, string $path, array $value, string $context, callable $strictMergeConfig): void {
$segments = array_values(array_filter(array_map('trim', explode('.', $path)), static fn (string $item): bool => $item !== ''));
if ($segments === []) {
throw new RuntimeException("Invalid config_path in {$context}: '{$path}'");
}
$cursor = &$root;
$fullPath = '';
foreach ($segments as $segment) {
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $segment)) {
throw new RuntimeException("Invalid config_path segment '{$segment}' in {$context}.");
}
$fullPath = $fullPath === '' ? $segment : $fullPath . '.' . $segment;
if (!array_key_exists($segment, $cursor)) {
$cursor[$segment] = [];
}
if (!is_array($cursor[$segment])) {
throw new RuntimeException("Config path '{$fullPath}' in {$context} is not an object.");
}
$cursor = &$cursor[$segment];
}
$cursor = $strictMergeConfig(
is_array($cursor) ? $cursor : [],
$value,
$context,
static fn (array $raw): bool => $raw !== [] && array_keys($raw) !== range(0, count($raw) - 1)
);
};
$normalizeConfigPathSegment = static function (string $raw): string {
$segment = preg_replace('/[^a-zA-Z0-9_]+/', '_', strtolower(trim($raw))) ?? '';
$segment = trim($segment, '_');
if ($segment === '') {
return 'item';
}
if (preg_match('/^[0-9]/', $segment)) {
return 'n_' . $segment;
}
return $segment;
};
$moduleHeadStylePaths = [];
$moduleHeadSeen = [];
foreach ($moduleHeadStyleSlots as $slot) {
if (!is_array($slot)) {
continue;
}
$slotPermission = trim((string) ($slot['permission'] ?? ''));
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
continue;
}
$path = ltrim(trim((string) ($slot['path'] ?? '')), '/');
if ($path === '') {
continue;
}
if (isset($moduleHeadSeen[$path])) {
continue;
}
$moduleHeadSeen[$path] = true;
$moduleHeadStylePaths[] = $path;
}
2026-02-11 19:28:12 +01:00
$pageTitle = $defaultTitle;
ob_start();
Buffer::get('title');
$bufferTitle = trim((string) ob_get_clean());
if ($bufferTitle !== '') {
$pageTitle = $bufferTitle . ' - ' . $defaultTitle;
2026-02-04 23:31:53 +01:00
}
$componentPageConfig = [
'components' => [
'confirmActions' => [],
'flashAutoDismiss' => [
'selector' => '.notice[data-flash-timeout]',
],
'passwordHints' => [],
'copyBadge' => [],
'multiSelect' => [
'selector' => '[data-multi-select]',
],
'navHistory' => [],
'detailsState' => [
'selector' => '[data-details-storage]',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'details-open',
],
'settingsTelemetry' => [],
'autoSubmit' => [
'selector' => '[data-auto-submit]',
],
'sidebarToggle' => [
'buttonSelector' => '[data-sidebar-toggle]',
'asideSelector' => '#app-sidebar',
'containerSelector' => '.app-container',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'sidebar',
'collapsedStorageKey' => 'sidebar-collapsed',
'hiddenStorageKey' => 'sidebar-hidden',
],
'asidePanels' => [
'barSelector' => '.aside-icon-bar',
'panelSelector' => '.app-sidebar-panel',
'titleSelector' => '.app-sidebar-title',
'toolsSelector' => '[data-aside-tools]',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'aside-panels',
'detailsStorageScope' => 'aside-details',
],
'detailsAsideToggle' => [
'buttonSelector' => '#toggle-main-content-aside',
'asideSelector' => '#app-details-aside-section',
'containerSelector' => '.app-details-container',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'details-aside',
],
'themeControls' => [
'menuSelector' => '[data-theme-menu]',
'toggleSelector' => '[data-theme-toggle]',
],
'contrastToggle' => [
'selector' => '[data-contrast-toggle]',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'contrast',
'storageKey' => 'contrast-mode',
],
'tenantSwitcher' => [
'rootSelector' => '[data-tenant-switcher]',
'linkSelector' => '[data-switch-tenant]',
],
'colorDefaultToggle' => [],
'customFieldOptionsToggle' => [],
'tenantSsoToggle' => [],
'standardDetailPage' => [],
'globalSearch' => [
'appBase' => localeBase(),
],
'tabs' => [
'selector' => '[data-app-component="tabs"]',
'storageNamespace' => 'app-ui',
'storageVersion' => 'v1',
'storageScope' => 'tabs',
],
'sessionWarning' => [
'enabled' => $isLoggedIn,
'selector' => '[data-app-session-warning-dialog]',
'idleSeconds' => $sessionIdleSeconds,
'pingUrl' => $sessionPingUrl,
'logoutFormId' => $sessionLogoutFormId,
'csrfKey' => $csrfKey,
'csrfToken' => $csrfToken,
],
'fslightboxRefresh' => [],
],
];
$moduleRuntimeComponents = [];
foreach ($moduleRuntimeComponentSlots as $slot) {
if (!is_array($slot)) {
continue;
}
$slotPermission = trim((string) ($slot['permission'] ?? ''));
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
continue;
}
$slotKey = trim((string) ($slot['key'] ?? ''));
$moduleId = trim((string) ($slot['module_id'] ?? ''));
$scriptPath = ltrim(trim((string) ($slot['script'] ?? '')), '/');
if ($slotKey === '' || $moduleId === '' || $scriptPath === '') {
continue;
}
$phase = strtolower(trim((string) ($slot['phase'] ?? 'late')));
if ($phase !== 'early' && $phase !== 'default' && $phase !== 'late') {
throw new RuntimeException("Invalid runtime.component phase '{$phase}' for slot '{$slotKey}'.");
}
$configPath = trim((string) ($slot['config_path'] ?? ''));
if ($configPath === '') {
$configPath = 'components.modules.' . $normalizeConfigPathSegment($moduleId) . '.' . $normalizeConfigPathSegment($slotKey);
}
$defaultConfig = is_array($slot['default_config'] ?? null) ? $slot['default_config'] : [];
if ($defaultConfig !== []) {
$mergeConfigAtPath($componentPageConfig, $configPath, $defaultConfig, "runtime.component '{$slotKey}'", $strictMergeConfig);
}
$moduleRuntimeComponents[] = [
'key' => $slotKey,
'moduleId' => $moduleId,
'componentName' => $moduleId . '.' . $slotKey,
'script' => assetVersion($scriptPath),
'export' => trim((string) ($slot['export'] ?? '')),
'selector' => trim((string) ($slot['selector'] ?? '')),
'scope' => trim((string) ($slot['scope'] ?? '')),
'configPath' => $configPath,
'phase' => $phase,
'order' => (int) ($slot['order'] ?? 100),
];
}
$componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
2026-02-04 23:31:53 +01:00
?>
<!DOCTYPE html>
2026-03-06 00:44:52 +01:00
<html
data-theme="<?php e($theme); ?>"
data-contrast="normal"
data-asset-base="<?php e(asset('')); ?>"
data-request-id="<?php e($pageRequestId); ?>"
data-telemetry-url="<?php e($telemetryUrl); ?>"
data-telemetry-enabled="<?php e($telemetryEnabled ? '1' : '0'); ?>"
data-telemetry-sample-rate="<?php e((string) $telemetrySampleRate); ?>"
data-telemetry-allowed-events="<?php e($telemetryAllowedEvents); ?>"
data-csrf-key="<?php e($csrfKey); ?>"
data-csrf-token="<?php e($csrfToken); ?>"
2026-03-13 14:38:58 +01:00
<?php if ($isLoggedIn): ?>
data-session-idle-seconds="<?php e((string) $sessionIdleSeconds); ?>"
data-session-ping-url="<?php e($sessionPingUrl); ?>"
data-session-logout-form-id="<?php e($sessionLogoutFormId); ?>"
<?php endif; ?>
2026-03-06 00:44:52 +01:00
class="no-js"
<?php if ($primaryVars !== ''): ?>style="<?php e($primaryVars); ?>" <?php endif; ?>
>
2026-02-04 23:31:53 +01:00
<head>
2026-03-05 08:26:51 +01:00
<script src="<?php e(assetVersion('js/app-boot.js')); ?>"></script>
2026-02-04 23:31:53 +01:00
<base href="<?php e(localeBase()); ?>">
2026-02-11 19:28:12 +01:00
<title><?php e($pageTitle); ?></title>
2026-02-04 23:31:53 +01:00
<meta property="og:site_name" content="<?php e($defaultTitle); ?>">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="32x32" href="<?php e(appFaviconUrl('favicon-32x32.png')); ?>">
<link rel="icon" type="image/png" sizes="16x16" href="<?php e(appFaviconUrl('favicon-16x16.png')); ?>">
<link rel="apple-touch-icon" href="<?php e(appFaviconUrl('apple-touch-icon.png')); ?>">
<link rel="manifest" href="<?php e(asset('favicon/site.webmanifest')); ?>">
<?php renderTemplateStyles('default'); ?>
<?php renderStylePaths($moduleHeadStylePaths); ?>
<?php renderPageStyles(); ?>
2026-02-04 23:31:53 +01:00
</head>
<body>
2026-03-05 12:51:53 +01:00
<a class="app-skip-link" href="#app-main-content"><?php e(t('Skip to main content')); ?></a>
<?php require __DIR__ . '/partials/app-topbar.phtml'; ?>
2026-02-04 23:31:53 +01:00
<div class="app-container">
<?php require __DIR__ . '/partials/app-main-aside-icon-bar.phtml'; ?>
<?php require __DIR__ . '/partials/app-main-aside.phtml'; ?>
2026-02-04 23:31:53 +01:00
<div class="app-main">
<main>
2026-03-05 12:51:53 +01:00
<div id="app-main-content" class="app-main-content" tabindex="-1">
2026-02-04 23:31:53 +01:00
<?php Buffer::get('html'); ?>
</div>
</main>
<?php require __DIR__ . '/partials/app-footer.phtml'; ?>
</div>
</div>
<div class="app-drawer-backdrop" aria-hidden="true"></div>
<?php if ($isLoggedIn): ?>
<form id="<?php e($sessionLogoutFormId); ?>" method="post" action="<?php e($sessionLogoutUrl); ?>" hidden>
<input type="hidden" name="<?php e($csrfKey); ?>" value="<?php e($csrfToken); ?>">
</form>
<?php endif; ?>
2026-02-04 23:31:53 +01:00
2026-03-05 08:26:51 +01:00
<script src="<?php e(assetVersion('vendor/fslightbox/fslightbox.js')); ?>"></script>
2026-03-06 00:44:52 +01:00
<?php require __DIR__ . '/partials/app-confirm-dialog.phtml'; ?>
2026-03-13 14:38:58 +01:00
<?php if ($isLoggedIn): ?>
2026-04-01 16:31:59 +02:00
<?php require __DIR__ . '/partials/app-search-dialog.phtml'; ?>
<?php require __DIR__ . '/partials/app-session-warning-dialog.phtml'; ?>
<?php require __DIR__ . '/partials/app-detail-drawer.phtml'; ?>
<?php endif; ?>
<?php foreach ($moduleBodyEndTemplateSlots as $slot): ?>
<?php
if (!is_array($slot)) {
continue;
}
$slotPermission = trim((string) ($slot['permission'] ?? ''));
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
continue;
}
$slotTemplate = trim((string) ($slot['template'] ?? ''));
if ($slotTemplate === '' || !is_file($slotTemplate)) {
continue;
}
include $slotTemplate;
?>
<?php endforeach; ?>
<script type="application/json" id="page-config-app-components"><?php gridJsonForJs($componentPageConfig); ?></script>
<?php require __DIR__ . '/partials/app-toast-stack.phtml'; ?>
<script type="module" src="<?php e(assetVersion('js/app-init.js')); ?>"></script>
2026-02-04 23:31:53 +01:00
</body>
2026-03-04 15:56:58 +01:00
</html>