feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Real-time password validation hints — checks length, uppercase, digit, special char.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
|
||||
const rules = {
|
||||
min: (value, min) => value.length >= min,
|
||||
@@ -19,9 +19,15 @@ const rules = {
|
||||
}
|
||||
};
|
||||
|
||||
export function initPasswordHints() {
|
||||
const containers = document.querySelectorAll('[data-password-hints]');
|
||||
if (!containers.length) {return;}
|
||||
export function initPasswordHints(root = document, options = {}) {
|
||||
const selector = String(options.selector || '[data-password-hints]').trim() || '[data-password-hints]';
|
||||
const host = resolveHost(root);
|
||||
const containers = Array.from(host.querySelectorAll(selector));
|
||||
if (!containers.length) {
|
||||
return { destroy: () => {} };
|
||||
}
|
||||
|
||||
const cleanupFns = [];
|
||||
|
||||
containers.forEach((container) => {
|
||||
if (!(container instanceof HTMLElement)) {return;}
|
||||
@@ -30,9 +36,10 @@ export function initPasswordHints() {
|
||||
const passwordSelector = container.dataset.passwordInput;
|
||||
const confirmSelector = container.dataset.confirmInput;
|
||||
const emailSelector = container.dataset.emailInput;
|
||||
const passwordInput = passwordSelector ? document.querySelector(passwordSelector) : null;
|
||||
const confirmInput = confirmSelector ? document.querySelector(confirmSelector) : null;
|
||||
const emailInput = emailSelector ? document.querySelector(emailSelector) : null;
|
||||
const scope = document;
|
||||
const passwordInput = passwordSelector ? scope.querySelector(passwordSelector) : null;
|
||||
const confirmInput = confirmSelector ? scope.querySelector(confirmSelector) : null;
|
||||
const emailInput = emailSelector ? scope.querySelector(emailSelector) : null;
|
||||
if (passwordSelector && !passwordInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing password input: ${passwordSelector}`, { module: 'password-hints' });
|
||||
}
|
||||
@@ -79,16 +86,28 @@ export function initPasswordHints() {
|
||||
});
|
||||
};
|
||||
|
||||
if (passwordInput) {passwordInput.addEventListener('input', evaluate);}
|
||||
if (confirmInput) {confirmInput.addEventListener('input', evaluate);}
|
||||
if (emailInput) {emailInput.addEventListener('input', evaluate);}
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('input', evaluate);
|
||||
cleanupFns.push(() => passwordInput.removeEventListener('input', evaluate));
|
||||
}
|
||||
if (confirmInput) {
|
||||
confirmInput.addEventListener('input', evaluate);
|
||||
cleanupFns.push(() => confirmInput.removeEventListener('input', evaluate));
|
||||
}
|
||||
if (emailInput) {
|
||||
emailInput.addEventListener('input', evaluate);
|
||||
cleanupFns.push(() => emailInput.removeEventListener('input', evaluate));
|
||||
}
|
||||
evaluate();
|
||||
container.dataset.passwordHintsBound = '1';
|
||||
cleanupFns.push(() => {
|
||||
delete container.dataset.passwordHintsBound;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPasswordHints);
|
||||
} else {
|
||||
initPasswordHints();
|
||||
const destroy = () => {
|
||||
cleanupFns.forEach((cleanup) => cleanup());
|
||||
};
|
||||
|
||||
return { destroy };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user