$value */ $isAssocArray = static function (array $value): bool { if ($value === []) { return false; } return array_keys($value) !== range(0, count($value) - 1); }; /** * @param array $target * @param array $source * @return array */ $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 $root * @param array $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; } $pageTitle = $defaultTitle; ob_start(); Buffer::get('title'); $bufferTitle = trim((string) ob_get_clean()); if ($bufferTitle !== '') { $pageTitle = $bufferTitle . ' - ' . $defaultTitle; } $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' => [ 'inputSelector' => '#side-search', 'resultsSelector' => '[data-global-search-results]', '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, 'logoutUrl' => $sessionLogoutUrl, '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; ?> data-session-idle-seconds="" data-session-ping-url="" data-session-logout-url="" class="no-js" style="" > <?php e($pageTitle); ?>