Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/ working directory, which had drifted across the entire V1.0 GA polish initiative (Phases 1-10) plus this session's topnav icon-order bugfix. - 52 existing files updated, 2 new pages added (about.html, health.html) - cgit/cezen-portal/ is now byte-identical to cezen-portal/ - Backend files (rag_ingest.py, cezen_license.py) already in sync; main.py intentionally left as-is (cgit is ahead there with workstation-category commercial-fit logic not yet backported to the live working copy) Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
This commit is contained in:
1 parent
7540da04a8
commit
b3eec34158
57 files changed
+2832
-1354
No files matched your search
+346
-16
@@ -66,7 +66,7 @@
|
||||
modal.innerHTML = [
|
||||
'<div id="cezen-upgrade-backdrop"></div>',
|
||||
'<div id="cezen-upgrade-box">',
|
||||
' <div id="cezen-upgrade-icon">🔒</div>',
|
||||
' <div id="cezen-upgrade-icon"><span class="icon-svg icon-2xl" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span></div>',
|
||||
' <h3 id="cezen-upgrade-title">Feature Locked</h3>',
|
||||
' <p id="cezen-upgrade-body"></p>',
|
||||
' <div id="cezen-upgrade-actions">',
|
||||
@@ -191,11 +191,11 @@
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'cezen-lock-badge';
|
||||
badge.setAttribute('title', 'Requires ' + TIER_NAMES[minTier] + ' tier');
|
||||
badge.textContent = '🔒';
|
||||
badge.innerHTML = '<span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>';
|
||||
link.appendChild(badge);
|
||||
|
||||
// Intercept click — show upgrade modal instead of navigating
|
||||
var featureName = (link.textContent || file).replace('🔒', '').trim();
|
||||
var featureName = (link.textContent || file).trim();
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
openModal(featureName, minTier);
|
||||
@@ -214,6 +214,7 @@
|
||||
var tierSlug = b.tier_slug || 'basic';
|
||||
|
||||
ensureApplianceNavLink();
|
||||
ensureAboutNavLink();
|
||||
normalizeApplianceLinks();
|
||||
applyServiceAvailability();
|
||||
|
||||
@@ -358,13 +359,13 @@
|
||||
/* Modal box */
|
||||
'#cezen-upgrade-box {',
|
||||
' position: relative;',
|
||||
' background: #fff;',
|
||||
' border-radius: 14px;',
|
||||
' background: var(--surface, #fff);',
|
||||
' border-radius: var(--radius-lg, 14px);',
|
||||
' padding: 40px 36px 32px;',
|
||||
' max-width: 420px;',
|
||||
' width: 90%;',
|
||||
' text-align: center;',
|
||||
' box-shadow: 0 24px 60px rgba(0,0,0,0.22);',
|
||||
' box-shadow: var(--shadow-lg, 0 24px 60px rgba(0,0,0,0.22));',
|
||||
' animation: cezen-modal-in 0.2s ease;',
|
||||
'}',
|
||||
'@keyframes cezen-modal-in {',
|
||||
@@ -372,18 +373,18 @@
|
||||
' to { opacity:1; transform: scale(1) translateY(0); }',
|
||||
'}',
|
||||
|
||||
'#cezen-upgrade-icon { font-size: 2.4rem; margin-bottom: 10px; }',
|
||||
'#cezen-upgrade-icon { color: var(--brand, #0D9488); margin-bottom: 10px; display: flex; justify-content: center; }',
|
||||
|
||||
'#cezen-upgrade-title {',
|
||||
' font-size: 1.15rem;',
|
||||
' font-weight: 700;',
|
||||
' color: #0f172a;',
|
||||
' color: var(--text-primary, #0f172a);',
|
||||
' margin: 0 0 10px;',
|
||||
'}',
|
||||
|
||||
'#cezen-upgrade-body {',
|
||||
' font-size: 0.9rem;',
|
||||
' color: #475569;',
|
||||
' color: var(--text-secondary, #475569);',
|
||||
' line-height: 1.55;',
|
||||
' margin: 0 0 24px;',
|
||||
'}',
|
||||
@@ -392,28 +393,28 @@
|
||||
|
||||
'#cezen-upgrade-cta {',
|
||||
' display: inline-block;',
|
||||
' background: #0D9488;',
|
||||
' background: var(--accent, #0D9488);',
|
||||
' color: #fff !important;',
|
||||
' padding: 9px 20px;',
|
||||
' border-radius: 7px;',
|
||||
' border-radius: var(--radius-md, 7px);',
|
||||
' font-size: 0.875rem;',
|
||||
' font-weight: 600;',
|
||||
' text-decoration: none;',
|
||||
' transition: background 0.18s;',
|
||||
'}',
|
||||
'#cezen-upgrade-cta:hover { background: #0b7a6f; }',
|
||||
'#cezen-upgrade-cta:hover { background: var(--accent-dark, #0b7a6f); }',
|
||||
|
||||
'#cezen-upgrade-close {',
|
||||
' background: #f1f5f9;',
|
||||
' background: var(--bg, #f1f5f9);',
|
||||
' border: none;',
|
||||
' border-radius: 7px;',
|
||||
' border-radius: var(--radius-md, 7px);',
|
||||
' padding: 9px 20px;',
|
||||
' font-size: 0.875rem;',
|
||||
' color: #334155;',
|
||||
' color: var(--text-secondary, #334155);',
|
||||
' cursor: pointer;',
|
||||
' transition: background 0.18s;',
|
||||
'}',
|
||||
'#cezen-upgrade-close:hover { background: #e2e8f0; }',
|
||||
'#cezen-upgrade-close:hover { background: var(--border, #e2e8f0); }',
|
||||
].join('\n');
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
@@ -443,6 +444,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Adds "About" to the end of the SYSTEM nav group on every page (except
|
||||
// about.html itself, which already has it hardcoded and marked active).
|
||||
function ensureAboutNavLink() {
|
||||
if (document.querySelector('a[href="about.html"]')) return;
|
||||
document.querySelectorAll('.nav-drop-cat').forEach(function (cat) {
|
||||
if ((cat.textContent || '').trim().toUpperCase() !== 'SYSTEM /') return;
|
||||
var sib = cat.nextElementSibling;
|
||||
while (sib && sib.tagName === 'A') { sib = sib.nextElementSibling; }
|
||||
var link = document.createElement('a');
|
||||
link.href = 'about.html';
|
||||
link.textContent = 'About';
|
||||
cat.parentNode.insertBefore(link, sib || null);
|
||||
});
|
||||
}
|
||||
|
||||
// ── License status labels (window.licenseStatusLabel) ──────────────────────
|
||||
// Raw codes from the backend's license evaluation (valid/missing/expired/
|
||||
// invalid_signature/not_yet_valid/machine_mismatch) aren't meant for display
|
||||
// — translate them the same way /api/health does, everywhere the portal
|
||||
// shows a license status to a user.
|
||||
var LICENSE_STATUS_LABELS = {
|
||||
valid: 'Valid',
|
||||
missing: 'No license installed (field-staging mode)',
|
||||
expired: 'Expired — contact support@cezentech.com',
|
||||
invalid_signature: 'Invalid signature — re-upload a valid signed license',
|
||||
not_yet_valid: 'Not yet in its valid date range',
|
||||
machine_mismatch: 'Bound to different hardware — contact support@cezentech.com',
|
||||
};
|
||||
window.licenseStatusLabel = function (status) {
|
||||
return LICENSE_STATUS_LABELS[status] || (status || 'Unknown');
|
||||
};
|
||||
|
||||
function shadeColor(hex, pct) {
|
||||
var num = parseInt(hex.replace('#', ''), 16);
|
||||
var r = Math.min(255, Math.max(0, (num >> 16) + Math.round(2.55 * pct)));
|
||||
@@ -451,6 +484,303 @@
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
// ── Shared toast notifications (window.showToast) ──────────────────────────
|
||||
// Single source of truth for transient messages — replaces the assorted
|
||||
// per-page toast divs and native alert() calls. See style.css for
|
||||
// #cezen-toast-stack / .cezen-toast styling.
|
||||
var TOAST_ICONS = {
|
||||
success: '<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>',
|
||||
error: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>',
|
||||
warn: '<svg viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
|
||||
info: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
||||
};
|
||||
|
||||
function ensureToastStack() {
|
||||
var stack = document.getElementById('cezen-toast-stack');
|
||||
if (!stack) {
|
||||
stack = document.createElement('div');
|
||||
stack.id = 'cezen-toast-stack';
|
||||
document.body.appendChild(stack);
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
window.showToast = function (message, type, opts) {
|
||||
type = TOAST_ICONS[type] ? type : 'info';
|
||||
opts = opts || {};
|
||||
var duration = opts.duration !== undefined ? opts.duration : (type === 'error' ? 6000 : 4000);
|
||||
var stack = ensureToastStack();
|
||||
var el = document.createElement('div');
|
||||
el.className = 'cezen-toast ' + type;
|
||||
el.innerHTML =
|
||||
'<span class="icon-svg icon-sm" aria-hidden="true">' + TOAST_ICONS[type] + '</span>' +
|
||||
'<span class="cezen-toast-body"></span>' +
|
||||
'<button class="cezen-toast-close" aria-label="Dismiss">×</button>';
|
||||
el.querySelector('.cezen-toast-body').textContent = message;
|
||||
function remove() {
|
||||
el.classList.add('leaving');
|
||||
setTimeout(function () { if (el.parentNode) el.remove(); }, 160);
|
||||
}
|
||||
el.querySelector('.cezen-toast-close').onclick = remove;
|
||||
stack.appendChild(el);
|
||||
if (duration > 0) setTimeout(remove, duration);
|
||||
return el;
|
||||
};
|
||||
|
||||
// ── Shared confirmation modal (window.showConfirm) ──────────────────────────
|
||||
// Replaces native confirm() with a styled, on-brand modal.
|
||||
// Usage: showConfirm('Delete user?', 'This cannot be undone.', {danger:true})
|
||||
// .then(function (ok) { if (ok) { ...proceed... } });
|
||||
function ensureConfirmModal() {
|
||||
var modal = document.getElementById('cezen-confirm-modal');
|
||||
if (modal) return modal;
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'cezen-confirm-modal';
|
||||
modal.innerHTML =
|
||||
'<div id="cezen-confirm-backdrop"></div>' +
|
||||
'<div id="cezen-confirm-box">' +
|
||||
' <div id="cezen-confirm-title"></div>' +
|
||||
' <div id="cezen-confirm-body"></div>' +
|
||||
' <div id="cezen-confirm-actions">' +
|
||||
' <button id="cezen-confirm-cancel" class="btn-secondary">Cancel</button>' +
|
||||
' <button id="cezen-confirm-ok" class="btn-primary">Confirm</button>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
window.showConfirm = function (title, body, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
var modal = ensureConfirmModal();
|
||||
modal.querySelector('#cezen-confirm-title').textContent = title || 'Are you sure?';
|
||||
modal.querySelector('#cezen-confirm-body').textContent = body || '';
|
||||
var okBtn = modal.querySelector('#cezen-confirm-ok');
|
||||
var cancelBtn = modal.querySelector('#cezen-confirm-cancel');
|
||||
okBtn.textContent = opts.confirmLabel || 'Confirm';
|
||||
cancelBtn.textContent = opts.cancelLabel || 'Cancel';
|
||||
okBtn.className = opts.danger ? 'btn-danger' : 'btn-primary';
|
||||
var backdrop = modal.querySelector('#cezen-confirm-backdrop');
|
||||
function close(result) {
|
||||
modal.classList.remove('cezen-modal-open');
|
||||
okBtn.onclick = null; cancelBtn.onclick = null; backdrop.onclick = null;
|
||||
resolve(result);
|
||||
}
|
||||
okBtn.onclick = function () { close(true); };
|
||||
cancelBtn.onclick = function () { close(false); };
|
||||
backdrop.onclick = function () { close(false); };
|
||||
modal.classList.add('cezen-modal-open');
|
||||
});
|
||||
};
|
||||
|
||||
// ── Global command palette (window search / Ctrl+K) ─────────────────────────
|
||||
// No global or cross-entity search existed anywhere in the portal — only a
|
||||
// handful of pages had a local filter box. This adds a lightweight, static
|
||||
// "jump to" index of every page in the product, reachable from any screen
|
||||
// via Ctrl/Cmd+K or the search icon in the top nav. It's navigation search,
|
||||
// not a live database query — no new API surface, matches the rest of
|
||||
// branding.js's pattern of injecting shared UI once per page load.
|
||||
var CMDK_PAGES = [
|
||||
{ label: 'Home', href: 'index.html', cat: 'General' },
|
||||
{ label: 'Quick Start', href: 'quickstart.html', cat: 'General' },
|
||||
{ label: 'Prompt Library', href: 'prompts.html', cat: 'General' },
|
||||
{ label: 'Use Cases', href: 'usecases.html', cat: 'General' },
|
||||
{ label: 'Model Library', href: 'models.html', cat: 'Learn' },
|
||||
{ label: 'Troubleshooting', href: 'troubleshooting.html', cat: 'Support' },
|
||||
{ label: 'FAQ', href: 'faq.html', cat: 'Support' },
|
||||
{ label: 'Glossary', href: 'glossary.html', cat: 'More' },
|
||||
{ label: "What's New", href: 'whats-new.html', cat: 'More' },
|
||||
{ label: 'Security & Privacy', href: 'security.html', cat: 'Docs' },
|
||||
{ label: 'Admin Guide', href: 'admin.html', cat: 'Docs' },
|
||||
{ label: 'Dashboard', href: 'dashboard.html', cat: 'Monitor' },
|
||||
{ label: 'Usage Analytics', href: 'analytics.html', cat: 'Monitor' },
|
||||
{ label: 'Governance & Audit', href: 'audit.html', cat: 'Monitor' },
|
||||
{ label: 'Health Center', href: 'health.html', cat: 'Monitor', keywords: 'status services gpu database' },
|
||||
{ label: 'Feedback & Ratings', href: 'feedback.html', cat: 'Monitor' },
|
||||
{ label: 'Users', href: 'users.html', cat: 'Manage', keywords: 'user management accounts' },
|
||||
{ label: 'Teams', href: 'teams.html', cat: 'Manage' },
|
||||
{ label: 'Model Catalog', href: 'models-admin.html', cat: 'Manage', keywords: 'models install pull upload' },
|
||||
{ label: 'Training', href: 'training.html', cat: 'Manage', keywords: 'fine-tune qlora' },
|
||||
{ label: 'Knowledge Base', href: 'knowledge.html', cat: 'Manage', keywords: 'rag documents collections kb' },
|
||||
{ label: 'API Keys', href: 'apikeys.html', cat: 'Tools' },
|
||||
{ label: 'Benchmarking', href: 'benchmark.html', cat: 'Tools', keywords: 'model benchmark speed tokens' },
|
||||
{ label: 'Model Compare', href: 'model-compare.html', cat: 'Tools' },
|
||||
{ label: 'API Playground', href: 'api-playground.html', cat: 'Tools' },
|
||||
{ label: 'Guardrails', href: 'guardrails.html', cat: 'Tools', keywords: 'content filters pii keyword' },
|
||||
{ label: 'RAG Quality', href: 'rag-quality.html', cat: 'Tools' },
|
||||
{ label: 'Model Router', href: 'router.html', cat: 'Tools' },
|
||||
{ label: 'MCP & Enterprise Connectors', href: 'connectors.html', cat: 'Tools', keywords: 'database folder connector integration' },
|
||||
{ label: 'Console', href: 'console.html', cat: 'System' },
|
||||
{ label: 'Settings', href: 'settings.html', cat: 'System' },
|
||||
{ label: 'Appliance Ops', href: 'appliance.html', cat: 'System' },
|
||||
{ label: 'Document Intelligence', href: 'documents.html', cat: 'AI Tools' },
|
||||
{ label: 'AI Workspace', href: 'chat-multi.html', cat: 'AI Tools', keywords: 'multimodal chat' },
|
||||
{ label: 'Prompt Studio', href: 'prompt-studio.html', cat: 'AI Tools' },
|
||||
{ label: 'Meeting Assistant', href: 'meeting.html', cat: 'AI Tools' },
|
||||
{ label: 'Agent Studio', href: 'agents.html', cat: 'AI Tools', keywords: 'agents automation' },
|
||||
{ label: 'Scheduled Jobs', href: 'schedules.html', cat: 'AI Tools', keywords: 'cron automation jobs' },
|
||||
{ label: 'Workflow Designer', href: 'workflows.html', cat: 'AI Tools', keywords: 'automation pipeline' },
|
||||
{ label: 'AI Eval Suite', href: 'evals.html', cat: 'AI Tools', keywords: 'evaluation test suite' },
|
||||
{ label: 'Chat Rooms', href: 'chatrooms.html', cat: 'AI Tools' },
|
||||
{ label: 'Notifications', href: 'notifications.html', cat: 'General' },
|
||||
];
|
||||
|
||||
function ensureCmdPalette() {
|
||||
var modal = document.getElementById('cezen-cmdk');
|
||||
if (modal) return modal;
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'cezen-cmdk';
|
||||
modal.innerHTML =
|
||||
'<div id="cezen-cmdk-backdrop"></div>' +
|
||||
'<div id="cezen-cmdk-box">' +
|
||||
' <div id="cezen-cmdk-input-wrap">' +
|
||||
' <span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>' +
|
||||
' <input id="cezen-cmdk-input" type="text" placeholder="Jump to a page…" autocomplete="off">' +
|
||||
' <span id="cezen-cmdk-hint">Esc</span>' +
|
||||
' </div>' +
|
||||
' <div id="cezen-cmdk-results"></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(modal);
|
||||
var input = modal.querySelector('#cezen-cmdk-input');
|
||||
modal.querySelector('#cezen-cmdk-backdrop').onclick = closeCmdPalette;
|
||||
input.addEventListener('input', function () { renderCmdResults(input.value); });
|
||||
input.addEventListener('keydown', function (e) {
|
||||
var results = modal.querySelectorAll('.cezen-cmdk-item');
|
||||
if (!results.length) return;
|
||||
var activeIdx = -1;
|
||||
results.forEach(function (r, i) { if (r.classList.contains('active')) activeIdx = i; });
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
activeIdx = (activeIdx + 1) % results.length;
|
||||
results.forEach(function (r, i) { r.classList.toggle('active', i === activeIdx); });
|
||||
results[activeIdx].scrollIntoView({ block: 'nearest' });
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
activeIdx = activeIdx <= 0 ? results.length - 1 : activeIdx - 1;
|
||||
results.forEach(function (r, i) { r.classList.toggle('active', i === activeIdx); });
|
||||
results[activeIdx].scrollIntoView({ block: 'nearest' });
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var target = modal.querySelector('.cezen-cmdk-item.active') || results[0];
|
||||
if (target) target.click();
|
||||
}
|
||||
});
|
||||
return modal;
|
||||
}
|
||||
|
||||
function renderCmdResults(query) {
|
||||
var modal = document.getElementById('cezen-cmdk');
|
||||
var resultsEl = modal.querySelector('#cezen-cmdk-results');
|
||||
var q = (query || '').trim().toLowerCase();
|
||||
var matches = CMDK_PAGES.filter(function (p) {
|
||||
if (!q) return true;
|
||||
var hay = (p.label + ' ' + p.cat + ' ' + (p.keywords || '')).toLowerCase();
|
||||
return hay.indexOf(q) !== -1;
|
||||
}).slice(0, 8);
|
||||
if (!matches.length) {
|
||||
resultsEl.innerHTML = '<div id="cezen-cmdk-empty">No pages match "' + escapeHtml(query) + '".</div>';
|
||||
return;
|
||||
}
|
||||
resultsEl.innerHTML = matches.map(function (p, i) {
|
||||
return '<a class="cezen-cmdk-item' + (i === 0 ? ' active' : '') + '" href="' + p.href + '">' +
|
||||
'<span class="cezen-cmdk-item-label">' + escapeHtml(p.label) + '</span>' +
|
||||
'<span class="cezen-cmdk-item-cat">' + escapeHtml(p.cat) + '</span></a>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return (s || '').replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function openCmdPalette() {
|
||||
var modal = ensureCmdPalette();
|
||||
modal.classList.add('cezen-cmdk-open');
|
||||
var input = modal.querySelector('#cezen-cmdk-input');
|
||||
input.value = '';
|
||||
renderCmdResults('');
|
||||
setTimeout(function () { input.focus(); }, 0);
|
||||
}
|
||||
|
||||
function closeCmdPalette() {
|
||||
var modal = document.getElementById('cezen-cmdk');
|
||||
if (modal) modal.classList.remove('cezen-cmdk-open');
|
||||
}
|
||||
|
||||
function injectCmdPaletteTrigger() {
|
||||
if (document.querySelector('.nav-search-btn')) return;
|
||||
var topnav = document.querySelector('.topnav');
|
||||
if (!topnav) return; // page has no shared topnav (e.g. login) — skip
|
||||
var navMenu = topnav.querySelector(':scope > nav');
|
||||
if (!navMenu) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'nav-icon-btn nav-search-btn';
|
||||
btn.type = 'button';
|
||||
btn.setAttribute('aria-label', 'Search (Ctrl+K)');
|
||||
btn.title = 'Search (Ctrl+K)';
|
||||
btn.innerHTML = '<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>';
|
||||
btn.onclick = openCmdPalette;
|
||||
topnav.insertBefore(btn, navMenu.nextSibling);
|
||||
}
|
||||
|
||||
// Two topnav markup variants exist across the portal: some pages wrap the
|
||||
// logo/tier-badge/notification-bell trio in a .nav-right div (in that DOM
|
||||
// order), others place the same three elements as flat topnav children in
|
||||
// the REVERSE order (bell, badge, logo). Relying on CSS `order` to reconcile
|
||||
// this is fragile because .nav-right is itself a nested flex container, so
|
||||
// `order` values only resolve within whichever flex context an element
|
||||
// actually sits in. Instead, physically move all four icon-cluster elements
|
||||
// into one canonical container in one canonical DOM order, so every page's
|
||||
// rendered markup is identical after this runs — no reliance on `order` math.
|
||||
function normalizeNavIconCluster() {
|
||||
var topnav = document.querySelector('.topnav');
|
||||
if (!topnav) return;
|
||||
var search = topnav.querySelector('.nav-search-btn');
|
||||
var bell = topnav.querySelector('a.nav-icon-btn[href="notifications.html"]');
|
||||
var badge = topnav.querySelector('[data-brand="tier"]');
|
||||
var logo = topnav.querySelector('#nav-org-logo');
|
||||
if (!bell && !badge && !logo) return; // nothing to normalize on this page
|
||||
|
||||
var cluster = topnav.querySelector('.nav-right');
|
||||
if (!cluster) {
|
||||
cluster = document.createElement('div');
|
||||
cluster.className = 'nav-right';
|
||||
topnav.appendChild(cluster);
|
||||
}
|
||||
// Canonical left-to-right order: search, bell, tier badge, org logo.
|
||||
// appendChild on an already-attached node MOVES it, so this deterministically
|
||||
// reorders in place regardless of the page's original markup pattern.
|
||||
[search, bell, badge, logo].forEach(function (el) {
|
||||
if (el) cluster.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
var isMeta = e.metaKey || e.ctrlKey;
|
||||
if (isMeta && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault();
|
||||
var modal = document.getElementById('cezen-cmdk');
|
||||
if (modal && modal.classList.contains('cezen-cmdk-open')) closeCmdPalette();
|
||||
else openCmdPalette();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
var m = document.getElementById('cezen-cmdk');
|
||||
if (m && m.classList.contains('cezen-cmdk-open')) closeCmdPalette();
|
||||
}
|
||||
});
|
||||
|
||||
function setupTopnavIcons() {
|
||||
injectCmdPaletteTrigger();
|
||||
normalizeNavIconCluster();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', setupTopnavIcons);
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
setupTopnavIcons();
|
||||
}
|
||||
|
||||
// ── Fetch branding and apply ────────────────────────────────────────────────
|
||||
fetch('/api/settings/branding', { credentials: 'include' })
|
||||
.then(function (r) { return r.ok ? r.json() : {}; })
|
||||
|
||||
Reference in new issue
Block a user