From 6ee9760168552e20cd9052a4947763124923744e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 25 Mar 2026 23:36:29 -0400 Subject: [PATCH] fix: toast crash, notification dropdown, ticket detail editable, toolbar dropdowns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix duplicate function showToast declaration causing infinite recursion (showToast declared twice → function hoisting → _origShowToast === self) Progress bar now inlined directly into _displayToast; Module 47 removed - Notification bell now opens a proper dropdown panel with unread items, per-item click-to-read, "Mark all read", close on outside click/Esc - Ticket detail drawer now has real editable fields (title, status, priority, assignee, description textarea, comment box) instead of read-only KV pairs; Save Changes and Post Comment buttons functional - Advanced ▾ filter dropdown: status/priority/assignee selects + Apply/Reset - Bulk Actions dropdown: Close/Reassign/Export/Delete with toast feedback - Generic .lt-dropdown-trigger toggle system (works for any future dropdown) - Add CSS sections 76 (notification panel) and 77 (dropdown widget + .lt-textarea) Co-Authored-By: Claude Sonnet 4.6 --- base.css | 175 +++++++++++++++++++++++++++++++++++++ base.html | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++---- base.js | 31 ++----- 3 files changed, 416 insertions(+), 44 deletions(-) diff --git a/base.css b/base.css index 90a667a..2d09fb0 100644 --- a/base.css +++ b/base.css @@ -4524,3 +4524,178 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas .lt-code-block { white-space: pre-wrap; word-break: break-all; } .lt-page-header { border-bottom: 2px solid #333; padding-bottom: 0.5rem; margin-bottom: 1rem; } } + +/* ---------------------------------------------------------------- + 76. NOTIFICATION DROPDOWN PANEL + ---------------------------------------------------------------- */ +.lt-notif-dropdown-wrap { + position: relative; + display: inline-flex; + align-items: center; +} + +.lt-notif-panel { + position: absolute; + top: calc(100% + 6px); + right: 0; + width: 300px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); + z-index: 10020; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + transform-origin: top right; + transform: scale(0.95); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease; +} +.lt-notif-panel[aria-hidden="false"] { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +.lt-notif-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--border-dim); + font-size: 0.72rem; + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); +} +.lt-notif-panel-clear { + background: none; + border: none; + color: var(--accent-cyan); + font-size: 0.68rem; + font-family: var(--font-mono); + cursor: pointer; + padding: 0; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.lt-notif-panel-clear:hover { text-decoration: underline; } + +.lt-notif-panel-list { max-height: 280px; overflow-y: auto; } + +.lt-notif-item { + display: flex; + align-items: flex-start; + gap: 0.6rem; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--border-dim); + cursor: pointer; + transition: background 0.1s; +} +.lt-notif-item:hover { background: var(--bg-tertiary); } +.lt-notif-item--unread { background: rgba(0, 212, 255, 0.04); } + +.lt-notif-dot { + flex-shrink: 0; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent-cyan); + margin-top: 4px; + box-shadow: 0 0 6px var(--accent-cyan); +} +.lt-notif-dot--read { + background: var(--border-color); + box-shadow: none; +} + +.lt-notif-item-body { flex: 1; min-width: 0; } +.lt-notif-item-title { + font-size: 0.76rem; + color: var(--text-primary); + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.lt-notif-item--unread .lt-notif-item-title { color: var(--text-primary); font-weight: 600; } +.lt-notif-item-time { + font-size: 0.64rem; + color: var(--text-muted); + margin-top: 2px; + font-family: var(--font-mono); +} + +.lt-notif-panel-footer { + padding: 0.5rem 0.75rem; + border-top: 1px solid var(--border-dim); +} + +/* ---------------------------------------------------------------- + 77. GENERIC DROPDOWN WIDGET + ---------------------------------------------------------------- */ +.lt-dropdown-wrap { + position: relative; + display: inline-block; +} + +.lt-dropdown-panel { + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 160px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%); + z-index: 10020; + box-shadow: 0 8px 24px rgba(0,0,0,0.4); + transform-origin: top left; + transform: scale(0.95); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease; +} +.lt-dropdown-panel--right { + left: auto; + right: 0; + transform-origin: top right; +} +.lt-dropdown-panel[aria-hidden="false"] { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +.lt-dropdown-item { + display: block; + width: 100%; + padding: 0.5rem 0.85rem; + background: none; + border: none; + text-align: left; + font-size: 0.76rem; + font-family: var(--font-mono); + color: var(--text-secondary); + cursor: pointer; + transition: background 0.1s, color 0.1s; + white-space: nowrap; +} +.lt-dropdown-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} +.lt-dropdown-item--danger { color: var(--accent-red); } +.lt-dropdown-item--danger:hover { background: rgba(255,45,85,0.1); color: var(--accent-red); } + +.lt-dropdown-divider { + height: 1px; + background: var(--border-dim); + margin: 0.25rem 0; +} + +/* textarea utility */ +.lt-textarea { + min-height: 60px; + resize: vertical; + line-height: 1.5; +} diff --git a/base.html b/base.html index be00f61..8051cec 100644 --- a/base.html +++ b/base.html @@ -117,10 +117,49 @@ - - - - + +
+ + +
operator @@ -135,16 +174,64 @@
-
+ +
ID#123456789
-
StatusOpen
-
PriorityP1 Critical
-
AssigneeJDjdoe
-
Created2026-03-10
+
Created2026-03-10 09:14 UTC
-
Description
-

Storage array link-down on compute-storage-01. Affects prod write path. Investigate RAID controller firmware.

-
Activity
+ + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + +
Add Comment
+
+ +
+ + + +
Activity
jdoe assigned ticket2h ago
@@ -161,8 +248,8 @@
@@ -293,14 +380,65 @@
- + +
+ + +
- 42 results - + 42 results + +
+ + +
@@ -1721,6 +1859,86 @@ Storage array link-down on `compute-storage-01`. // Sidebar submenus (re-init for demo sidebar) lt.sidebarSubmenus.init(document.querySelector('.lt-section-body')); + // ----- Notification bell dropdown ----- + (function() { + const btn = document.getElementById('lt-notif-bell-btn'); + const panel = document.getElementById('lt-notif-panel'); + if (!btn || !panel) return; + + function open() { + panel.setAttribute('aria-hidden', 'false'); + btn.setAttribute('aria-expanded', 'true'); + // Mark all as read visually + } + function close() { + panel.setAttribute('aria-hidden', 'true'); + btn.setAttribute('aria-expanded', 'false'); + } + + btn.addEventListener('click', e => { + e.stopPropagation(); + panel.getAttribute('aria-hidden') === 'false' ? close() : open(); + }); + + // "Mark all read" button + const clearBtn = document.getElementById('lt-notif-clear-all'); + if (clearBtn) clearBtn.addEventListener('click', () => { + panel.querySelectorAll('.lt-notif-item--unread').forEach(el => el.classList.remove('lt-notif-item--unread')); + panel.querySelectorAll('.lt-notif-dot').forEach(el => { el.classList.remove('lt-notif-dot'); el.classList.add('lt-notif-dot', 'lt-notif-dot--read'); }); + lt.notif.clear('#lt-notif-bell'); + lt.toast.info('All notifications marked as read'); + }); + + // Individual item click + panel.querySelectorAll('.lt-notif-item').forEach(item => { + item.addEventListener('click', () => { + item.classList.remove('lt-notif-item--unread'); + const dot = item.querySelector('.lt-notif-dot'); + if (dot) { dot.classList.add('lt-notif-dot--read'); } + close(); + }); + }); + + // Close on outside click + document.addEventListener('click', e => { + if (!document.getElementById('lt-notif-bell').contains(e.target)) close(); + }); + + // Esc close + document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); + }()); + + // ----- Generic dropdown toggle (Advanced filter + Bulk Actions) ----- + document.querySelectorAll('.lt-dropdown-trigger').forEach(btn => { + const wrap = btn.closest('.lt-dropdown-wrap'); + const panel = wrap && wrap.querySelector('.lt-dropdown-panel'); + if (!panel) return; + + btn.addEventListener('click', e => { + e.stopPropagation(); + const isOpen = panel.getAttribute('aria-hidden') === 'false'; + // Close all other dropdowns first + document.querySelectorAll('.lt-dropdown-panel[aria-hidden="false"]').forEach(p => { + p.setAttribute('aria-hidden', 'true'); + const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger'); + if (t) t.setAttribute('aria-expanded', 'false'); + }); + if (!isOpen) { + panel.setAttribute('aria-hidden', 'false'); + btn.setAttribute('aria-expanded', 'true'); + } + }); + }); + + // Close dropdowns on outside click / Esc + document.addEventListener('click', () => { + document.querySelectorAll('.lt-dropdown-panel[aria-hidden="false"]').forEach(p => { + p.setAttribute('aria-hidden', 'true'); + const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger'); + if (t) t.setAttribute('aria-expanded', 'false'); + }); + }); + // Tab bar switching document.querySelectorAll('.lt-tab-bar').forEach(bar => { bar.addEventListener('click', e => { diff --git a/base.js b/base.js index 77006fd..69342d7 100644 --- a/base.js +++ b/base.js @@ -105,9 +105,14 @@ closeEl.setAttribute('aria-label', 'Dismiss'); closeEl.addEventListener('click', () => _dismissToast(toast)); + const progressEl = document.createElement('div'); + progressEl.className = 'lt-toast-progress'; + progressEl.style.animationDuration = duration + 'ms'; + toast.appendChild(iconEl); toast.appendChild(msgEl); toast.appendChild(closeEl); + toast.appendChild(progressEl); container.appendChild(toast); const timer = setTimeout(() => _dismissToast(toast), duration); @@ -2027,32 +2032,6 @@ }, }; - /* ================================================================ - MODULE 47 — TOAST ENHANCEMENTS - Adds a drain progress bar to each toast. - The original showToast already handles queuing (section 2). - ================================================================ */ - // Patch showToast to inject a progress bar after each toast is created - const _origShowToast = showToast; - function showToast(message, type, duration) { - duration = duration || 4000; - const result = _origShowToast(message, type, duration); - // Inject drain bar into the most recently added toast - requestAnimationFrame(() => { - const container = document.getElementById('lt-toast-container'); - if (!container) return; - const toasts = container.querySelectorAll('.lt-toast'); - const last = toasts[toasts.length - 1]; - if (last && !last.querySelector('.lt-toast-progress')) { - const bar = document.createElement('div'); - bar.className = 'lt-toast-progress'; - bar.style.animationDuration = duration + 'ms'; - last.appendChild(bar); - } - }); - return result; - } - /* ================================================================ MODULE 48 — SIDEBAR SUBMENUS Auto-inits .lt-sidebar-group elements.