Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
66 lines
2.1 KiB
JavaScript
66 lines
2.1 KiB
JavaScript
/**
|
|
* DOM utility helpers — safe element lookup with missing-element warnings and telemetry.
|
|
*/
|
|
import { telemetry } from './app-telemetry.js';
|
|
|
|
const warned = new Set();
|
|
|
|
export const warnOnce = (code, message, details = {}) => {
|
|
const normalizedCode = String(code ?? '').trim() || 'UI_WARN';
|
|
const normalizedMessage = String(message ?? '').trim();
|
|
const key = `${normalizedCode}:${normalizedMessage}`;
|
|
if (warned.has(key)) {return;}
|
|
warned.add(key);
|
|
console.warn(`[${normalizedCode}] ${normalizedMessage}`, details);
|
|
|
|
const warnMeta = {
|
|
source: 'warn_once',
|
|
code: normalizedCode,
|
|
};
|
|
|
|
if (details && typeof details === 'object' && !Array.isArray(details)) {
|
|
if (typeof details.module === 'string' && details.module.trim() !== '') {
|
|
warnMeta.module = details.module.trim();
|
|
}
|
|
if (typeof details.component === 'string' && details.component.trim() !== '') {
|
|
warnMeta.component = details.component.trim();
|
|
}
|
|
if (typeof details.feature === 'string' && details.feature.trim() !== '') {
|
|
warnMeta.feature = details.feature.trim();
|
|
}
|
|
}
|
|
|
|
telemetry.capture('frontend.warn_once', {
|
|
severity: 'warning',
|
|
message: `[${normalizedCode}] ${normalizedMessage}`,
|
|
meta: warnMeta,
|
|
});
|
|
};
|
|
|
|
export const requireEl = (selector, details = {}) => {
|
|
const el = document.querySelector(selector);
|
|
if (!el) {
|
|
warnOnce('UI_EL_MISSING', `Missing element: ${selector}`, details);
|
|
return null;
|
|
}
|
|
return el;
|
|
};
|
|
|
|
export const requireAll = (selector, details = {}) => {
|
|
const list = Array.from(document.querySelectorAll(selector));
|
|
if (!list.length) {
|
|
warnOnce('UI_EL_MISSING', `Missing elements: ${selector}`, details);
|
|
}
|
|
return list;
|
|
};
|
|
|
|
export const optionalEl = (selector) => document.querySelector(selector);
|
|
|
|
/**
|
|
* Resolve a mount root to a valid query host.
|
|
* Components receive a `root` parameter that may be document, an element, or undefined.
|
|
* Returns the root if it supports querySelectorAll, otherwise falls back to document.
|
|
*/
|
|
export const resolveHost = (root) =>
|
|
root && typeof root.querySelectorAll === 'function' ? root : document;
|