'use strict'; /* admin → Cmd+K / Ctrl+K command palette (Phase 4). * Self-initialized on DOMContentLoaded. Not a section — it's a global widget. * Overrides the generic /js/search.js Ctrl+K handler on admin pages by binding * in capture phase and calling stopImmediatePropagation. */ (function () { 'use strict'; /* ── Hardcoded actions ────────────────────────────────────────────────── */ const ACTIONS = [ { id: 'award_coins', name: 'Выдать монеты', hint: 'shop', icon: 'coins', go: () => navigateTo('#shop') }, { id: 'award_xp', name: 'Выдать XP', hint: 'gam', icon: 'zap', go: () => navigateTo('#gam') }, { id: 'new_class', name: 'Создать класс', hint: 'classes', icon: 'plus-circle', go: () => { window.location.href = '/classes'; } }, { id: 'new_test', name: 'Создать тест', hint: 'tests', icon: 'file-plus', go: () => navigateTo('#tests') }, { id: 'view_users', name: 'Все пользователи', hint: 'users', icon: 'users', go: () => navigateTo('#users') }, { id: 'view_sessions', name: 'Все сессии', hint: 'sessions', icon: 'history', go: () => navigateTo('#sessions') }, { id: 'view_audit', name: 'Audit log', hint: 'sublog', icon: 'shield', go: () => navigateTo('#sublog') }, { id: 'view_overview', name: 'Главная', hint: 'overview', icon: 'layout-dashboard', go: () => navigateTo('#overview') }, ]; /* ── State ────────────────────────────────────────────────────────────── */ let _overlay = null; let _input = null; let _results = null; let _timer = null; let _items = []; // flat list of result items in display order let _activeIdx = 0; let _lastQuery = ''; let _reqSeq = 0; // race-guard for async fetches /* ── Helpers ──────────────────────────────────────────────────────────── */ function navigateTo(hash) { if (window.AdminRouter) AdminRouter.navigate(hash); else window.location.hash = hash; } function esc(s) { return (window.LS && LS.esc) ? LS.esc(s) : String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>'); } function isOpen() { return !!(_overlay && _overlay.classList.contains('open')); } /* ── Styles (lazy injection) ──────────────────────────────────────────── */ function ensureStyles() { if (document.getElementById('akp-style')) return; const s = document.createElement('style'); s.id = 'akp-style'; s.textContent = ` .akp-ov { position: fixed; inset: 0; z-index: 9500; display: flex; align-items: flex-start; justify-content: center; padding: 96px 20px 20px; background: rgba(15,23,42,0.55); backdrop-filter: blur(8px); opacity: 0; pointer-events: none; transition: opacity .15s ease; } .akp-ov.open { opacity: 1; pointer-events: auto; } .akp-box { width: 100%; max-width: 600px; background: var(--surface, #fff); border: 1px solid var(--border, rgba(15,23,42,.08)); border-radius: 16px; box-shadow: 0 24px 80px rgba(15,23,42,0.32); display: flex; flex-direction: column; max-height: calc(100vh - 140px); overflow: hidden; transform: translateY(-8px) scale(.98); transition: transform .18s ease; } .akp-ov.open .akp-box { transform: translateY(0) scale(1); } .akp-input-wrap { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border, rgba(15,23,42,.08)); } .akp-input-wrap svg.akp-search-icon { width: 18px; height: 18px; color: var(--text-3, #64748b); flex-shrink: 0; } .akp-input { flex: 1; border: none; outline: none; background: transparent; font-family: inherit; font-size: 1.05rem; color: var(--text, #0F172A); padding: 4px 0; } .akp-input::placeholder { color: var(--text-3, #94a3b8); } .akp-kbd { font-family: ui-monospace, monospace; font-size: .7rem; background: rgba(15,23,42,.06); color: var(--text-3, #64748b); padding: 2px 6px; border-radius: 5px; border: 1px solid var(--border, rgba(15,23,42,.08)); flex-shrink: 0; } .akp-results { flex: 1; overflow-y: auto; padding: 6px 0; } .akp-group-label { font-family: 'Unbounded', sans-serif; font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3, #64748b); padding: 10px 18px 6px; } .akp-item { display: flex; align-items: center; gap: 12px; padding: 9px 18px; cursor: pointer; user-select: none; border-left: 3px solid transparent; } .akp-item:hover, .akp-item.active { background: rgba(155,93,229,.08); border-left-color: var(--violet, #9B5DE5); } .akp-icon { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: rgba(155,93,229,.1); color: var(--violet, #9B5DE5); flex-shrink: 0; } .akp-icon svg { width: 16px; height: 16px; } .akp-body { flex: 1; min-width: 0; } .akp-title { font-size: .92rem; font-weight: 600; color: var(--text, #0F172A); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .akp-sub { font-size: .76rem; color: var(--text-3, #64748b); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; } .akp-badge { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-3, #64748b); padding: 2px 7px; border-radius: 5px; background: rgba(15,23,42,.06); border: 1px solid var(--border, rgba(15,23,42,.08)); flex-shrink: 0; } .akp-badge.role-admin { background: rgba(241,91,181,.1); color: var(--pink, #F15BB5); border-color: rgba(241,91,181,.25); } .akp-badge.role-teacher { background: rgba(6,214,224,.1); color: var(--cyan, #06D6E0); border-color: rgba(6,214,224,.25); } .akp-badge.role-student { background: rgba(155,93,229,.1); color: var(--violet,#9B5DE5); border-color: rgba(155,93,229,.25); } .akp-empty { padding: 22px 18px; text-align: center; color: var(--text-3, #64748b); font-size: .88rem; } .akp-footer { display: flex; align-items: center; gap: 14px; padding: 9px 18px; font-size: .72rem; color: var(--text-3, #64748b); border-top: 1px solid var(--border, rgba(15,23,42,.08)); background: rgba(15,23,42,.02); flex-shrink: 0; } .akp-footer span { display: inline-flex; align-items: center; gap: 5px; } .akp-footer kbd { font-family: ui-monospace, monospace; font-size: .7rem; padding: 1px 5px; border-radius: 4px; background: var(--surface, #fff); border: 1px solid var(--border, rgba(15,23,42,.12)); } @media (max-width: 540px) { .akp-ov { padding: 40px 12px 12px; } .akp-box { max-height: calc(100vh - 60px); } } `; document.head.appendChild(s); } /* ── Lucide icon helper (inline SVG fallback if Lucide missing) ───────── */ function iconHtml(name) { return ``; } /* ── Build DOM ────────────────────────────────────────────────────────── */ function build() { if (_overlay) return; ensureStyles(); _overlay = document.createElement('div'); _overlay.className = 'akp-ov'; _overlay.setAttribute('role', 'dialog'); _overlay.setAttribute('aria-modal', 'true'); _overlay.setAttribute('aria-label', 'Командная палитра'); _overlay.innerHTML = `