style(cms): improve editor ui and implement collapsible item cards

Enhance the CMS administration interface by introducing a more refined design system for content editors. This update replaces generic Bootstrap utility classes with dedicated CMS styling to improve visual hierarchy and usability.
Key changes:
- Introduce `cms-editor-group` and `cms-item-card` classes for consistent grouping and item styling.
- Implement collapsible functionality for item cards to manage long lists of content more effectively.
- Standardize editor buttons, including new styles for drag handles, remove buttons, and add buttons.
- Update the fixed bottom action bar with improved backdrop filters, shadows, and spacing.
- Refactor editor scripts across Accreditation, Admissions, History, Partnerships, and Policies to utilize the new UI components and collapse logic.
- Add comprehensive CSS to `main.ejs` to support the new CMS design tokens and layout behaviors.
This commit is contained in:
Tống Thành Đạt
2026-04-20 20:10:27 +07:00
parent 7df3a6f6bf
commit 5615c494cf
10 changed files with 387 additions and 137 deletions
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}