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>
46 lines
1.2 KiB
JavaScript
46 lines
1.2 KiB
JavaScript
/**
|
|
* Shared UI event channels for cross-component communication.
|
|
*/
|
|
const SIDEBAR_EVENT = 'app:sidebar';
|
|
const ASIDE_PANELS_EVENT = 'app:aside-panels';
|
|
|
|
const dispatch = (eventName, detail = {}) => {
|
|
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
};
|
|
|
|
export const requestSidebarAction = (action, options = {}) => {
|
|
const normalizedAction = String(action || '').trim();
|
|
if (normalizedAction === '') {
|
|
return;
|
|
}
|
|
dispatch(SIDEBAR_EVENT, {
|
|
action: normalizedAction,
|
|
persist: Boolean(options.persist),
|
|
});
|
|
};
|
|
|
|
export const requestAsidePanelOpen = (panel, options = {}) => {
|
|
const normalizedPanel = String(panel || '').trim();
|
|
if (normalizedPanel === '') {
|
|
return;
|
|
}
|
|
dispatch(ASIDE_PANELS_EVENT, {
|
|
action: 'open',
|
|
panel: normalizedPanel,
|
|
fromUser: options.fromUser !== false,
|
|
});
|
|
};
|
|
|
|
export const bindAsidePanelChannel = (handler) => {
|
|
if (typeof handler !== 'function') {
|
|
return () => {};
|
|
}
|
|
const listener = (event) => {
|
|
handler(event instanceof CustomEvent ? event.detail || {} : {});
|
|
};
|
|
document.addEventListener(ASIDE_PANELS_EVENT, listener);
|
|
return () => {
|
|
document.removeEventListener(ASIDE_PANELS_EVENT, listener);
|
|
};
|
|
};
|