Final modularization layout (user-chosen): every page is a self-contained folder under components/<id>/ (controllers + CSS + its html fragment), and all shared/framework code folds into core/: core/kernel (feature-registry, lifecycle, services, spa) core/boot (auth, system-loader/orchestrator, setup, loaders) core/lib (data-loader, router, helpers, the task kernel, shared modules) core/ui (topbar, modal, notifications, … + topbar.html) core/css (all shared stylesheets) core/icons Top level is now just: components/, core/, themes/, index.html (+ runtime data/). Every path reference rewritten (index.html, scripts arrays, fetch()/ loadFragment()/loadScript() literals, system-loader + config-manager controller paths, kernel manifest URL, feature.json, backend FEATURES_DIR). The /api/features/list endpoint NAME is unchanged (it now scans components/). Deleted 3 dead files (app-content.html, apps-content.html, html-cache.js). Verified: 0 stale prefixes, 0 double-rewrites, all JS/JSON valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
228 lines
8.8 KiB
JavaScript
Executable File
228 lines
8.8 KiB
JavaScript
Executable File
// Config Renderer - Handles all rendering logic
|
||
class ConfigRenderer {
|
||
constructor() {
|
||
this.fieldTypes = {
|
||
text: this.renderTextField.bind(this),
|
||
number: this.renderNumberField.bind(this),
|
||
email: this.renderEmailField.bind(this),
|
||
password: this.renderPasswordField.bind(this),
|
||
select: this.renderSelectField.bind(this),
|
||
checkbox: this.renderCheckboxField.bind(this),
|
||
textarea: this.renderTextareaField.bind(this)
|
||
};
|
||
}
|
||
|
||
renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription, isAdvanced = false) {
|
||
const isEnabled = masterKey.value === 'true';
|
||
const sectionId = `master-${masterKey.key}`;
|
||
|
||
let html = `
|
||
<div class="config-category">
|
||
<div class="domains-wrapper">
|
||
<div class="domains-header">
|
||
<div>
|
||
<h3>${displaySubcategory}</h3>
|
||
<p class="category-description">${subcategoryDescription}</p>
|
||
</div>
|
||
</div>
|
||
<div class="domains-divider"></div>
|
||
<div class="git-master-toggle">
|
||
<div class="field-group">
|
||
<label class="checkbox-label master-toggle">
|
||
<input type="checkbox" id="${masterKey.key.toLowerCase()}-toggle" name="${masterKey.key}" value="true" ${isEnabled ? 'checked' : ''} onchange="window.ConfigShared.toggleSection('section-content-${sectionId}', this.checked)">
|
||
<span class="checkbox-custom"></span>
|
||
<span class="checkbox-text">
|
||
${masterKey.title || 'Enable Advanced Configuration'}
|
||
<span class="tooltip" title="${masterKey.description || 'Enable advanced configuration options'}">ℹ️</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div id="section-content-${sectionId}" class="git-section-content ${isEnabled ? '' : 'hidden'}">
|
||
<div class="config-fields">
|
||
`;
|
||
|
||
// Add all other fields (excluding the master toggle)
|
||
configItems.filter(item => item.key !== masterKey.key).forEach(item => {
|
||
const fieldId = `config-${item.key}`;
|
||
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options) || '';
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="spacer spacer-lg"></div>
|
||
</div>
|
||
`;
|
||
|
||
return html;
|
||
}
|
||
|
||
renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription, config = {}) {
|
||
let html = `
|
||
<div class="config-category">
|
||
<div class="domains-wrapper">
|
||
<div class="domains-header">
|
||
<div>
|
||
<h3>${displaySubcategory}</h3>
|
||
<p class="category-description">${subcategoryDescription}</p>
|
||
</div>
|
||
</div>
|
||
<div class="domains-divider"></div>
|
||
<div class="config-fields">
|
||
`;
|
||
|
||
// Add all fields
|
||
configItems.forEach(item => {
|
||
const fieldId = `config-${item.key}`;
|
||
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
<div class="spacer spacer-lg"></div>
|
||
</div>
|
||
`;
|
||
|
||
return html;
|
||
}
|
||
|
||
renderRegularSubcategory(configItems, displaySubcategory, subcategoryDescription, config = {}) {
|
||
let html = `
|
||
<div class="config-category">
|
||
<div class="domains-wrapper">
|
||
<div class="domains-header">
|
||
<div>
|
||
<h3>${displaySubcategory}</h3>
|
||
<p class="category-description">${subcategoryDescription}</p>
|
||
</div>
|
||
</div>
|
||
<div class="domains-divider"></div>
|
||
<div class="config-fields">
|
||
`;
|
||
|
||
// Add all fields
|
||
configItems.forEach(item => {
|
||
const fieldId = `config-${item.key}`;
|
||
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
<div class="spacer spacer-lg"></div>
|
||
</div>
|
||
`;
|
||
|
||
return html;
|
||
}
|
||
|
||
// Field rendering methods
|
||
renderTextField(fieldId, key, value, title, description, options) {
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<input type="text" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderNumberField(fieldId, key, value, title, description, options) {
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<input type="number" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" min="${options?.min || ''}" max="${options?.max || ''}" step="${options?.step || '1'}">
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderEmailField(fieldId, key, value, title, description, options) {
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<input type="email" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderPasswordField(fieldId, key, value, title, description, options) {
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<div class="password-input-group">
|
||
<input type="password" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
|
||
<button type="button" class="password-toggle-btn" onclick="window.configRenderer.togglePasswordVisibility('${fieldId}')">
|
||
<span class="password-icon">👁️</span>
|
||
</button>
|
||
</div>
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderSelectField(fieldId, key, value, title, description, fieldOptions) {
|
||
const options = window.ConfigOptions?.getSelectOptions(key) || [];
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<select id="${fieldId}" name="${key}" class="form-control">
|
||
${options.map(opt => `<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>${opt.label}</option>`).join('')}
|
||
</select>
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderCheckboxField(fieldId, key, value, title, description, options) {
|
||
const isChecked = value === 'true' || value === true;
|
||
return `
|
||
<div class="field-group">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="${fieldId}" name="${key}" value="true" ${isChecked ? 'checked' : ''}>
|
||
<span class="checkbox-custom"></span>
|
||
<span class="checkbox-text">${title || this.cleanDescription(key)}</span>
|
||
</label>
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderTextareaField(fieldId, key, value, title, description, options) {
|
||
return `
|
||
<div class="field-group">
|
||
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
|
||
<textarea id="${fieldId}" name="${key}" class="form-control" rows="${options?.rows || '4'}" placeholder="${options?.placeholder || ''}">${value || ''}</textarea>
|
||
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
togglePasswordVisibility(fieldId) {
|
||
const input = document.getElementById(fieldId);
|
||
const button = document.querySelector(`button[onclick*="${fieldId}"]`);
|
||
|
||
if (input && button) {
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
button.innerHTML = '<span class="password-icon">🙈️</span>';
|
||
} else {
|
||
input.type = 'password';
|
||
button.innerHTML = '<span class="password-icon">👁️</span>';
|
||
}
|
||
}
|
||
}
|
||
|
||
cleanDescription(description) {
|
||
if (!description) return '';
|
||
return description.replace(/CFG_[A-Z_]+/g, '').replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
}
|
||
|
||
// Export for use in other modules
|
||
window.ConfigRenderer = ConfigRenderer;
|