fix: accessibility & quality audit pass 4+5
CSS: - Add :active/:focus-visible to .lt-modal-close, .lt-drawer-right-close, .lt-notif-panel-clear, .lt-file-item-remove - Add :focus-visible to .lt-accordion-header, .lt-tag-remove, .lt-combobox-tag-remove - Add .lt-cmd-input-wrap:focus-within focus indicator (outline:none compensation) - Add will-change: stroke-dashoffset to .lt-gauge-fill - Add range slider :focus-visible thumb ring - Fix .tok-cmt hardcoded #5c8c6a → var(--color-tok-cmt) w/ light-mode override - Add .lt-skip-link component (visible on focus) - Fix .lt-filter-group fieldset UA border reset JS: - Fix infinite scroll: store throttled handler ref so removeEventListener works - Fix right drawer: remove close-button listeners in _rdClose (were never removed) - Fix right drawer: add Tab focus trap (matches modal behaviour) - Fix _cmdPaletteClose: restore focus to element that opened the palette - Fix initSortTable: set aria-sort="ascending"/"descending"/"none" on th elements - Fix switchTab: set aria-selected="true"/"false" on .lt-tab[data-tab] buttons - Fix copy button timeout: guard with document.contains() before DOM mutation - Fix combobox: add role=combobox, aria-expanded, aria-controls, role=listbox; toggle aria-expanded on open/close HTML: - Add skip nav link + id="main-content" on <main> - Primary tab nav: add role=tablist, role=tab, aria-selected, aria-controls, id attrs; tab panels get role=tabpanel + aria-labelledby - Tab bar demo: same ARIA wiring + aria-controls + role/labelledby on panels - Sidebar filters: convert div+span to fieldset+legend for proper grouping - Table sort headers: add aria-sort="none" (JS updates on click) - Accordion: add aria-controls on headers, IDs on bodies - Wizard: add aria-current="step" on active step indicator - Table th: scope="col" on all column headers - Row checkboxes: aria-label per ticket ID - Worker metrics table: add <tbody> - Progress bars: role=progressbar + aria-valuenow/min/max + aria-label - Export + keyboard shortcuts modals: role=dialog, aria-modal, aria-labelledby Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,7 +175,16 @@
|
|||||||
--z-popover: 10012;
|
--z-popover: 10012;
|
||||||
--z-tooltip: 10013;
|
--z-tooltip: 10013;
|
||||||
--z-toast: 10014;
|
--z-toast: 10014;
|
||||||
|
--z-panel: 10020; /* notification / generic dropdown panels — above everything */
|
||||||
--z-overlay: 9999; /* scanlines / CRT effects */
|
--z-overlay: 9999; /* scanlines / CRT effects */
|
||||||
|
|
||||||
|
/* --- Corner cuts (clip-path polygon notches) --- */
|
||||||
|
--corner-cut: 8px;
|
||||||
|
--corner-cut-sm: 5px;
|
||||||
|
--corner-cut-lg: 16px;
|
||||||
|
|
||||||
|
/* --- Syntax highlight --- */
|
||||||
|
--color-tok-cmt: #5c8c6a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -763,6 +772,12 @@ hr {
|
|||||||
|
|
||||||
.lt-btn:active { transform: translateY(1px); }
|
.lt-btn:active { transform: translateY(1px); }
|
||||||
|
|
||||||
|
.lt-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: var(--box-glow-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
.lt-btn:disabled,
|
.lt-btn:disabled,
|
||||||
.lt-btn[disabled] {
|
.lt-btn[disabled] {
|
||||||
opacity: 0.30;
|
opacity: 0.30;
|
||||||
@@ -859,10 +874,27 @@ hr {
|
|||||||
|
|
||||||
.lt-input:focus,
|
.lt-input:focus,
|
||||||
.lt-select:focus,
|
.lt-select:focus,
|
||||||
.lt-textarea:focus {
|
.lt-textarea:focus,
|
||||||
|
.lt-input:focus-visible,
|
||||||
|
.lt-select:focus-visible,
|
||||||
|
.lt-textarea:focus-visible {
|
||||||
border-color: var(--accent-cyan);
|
border-color: var(--accent-cyan);
|
||||||
box-shadow: var(--box-glow-cyan);
|
box-shadow: var(--box-glow-cyan);
|
||||||
background: rgba(0,212,255,0.025);
|
background: rgba(0,212,255,0.025);
|
||||||
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-input:disabled,
|
||||||
|
.lt-select:disabled,
|
||||||
|
.lt-textarea:disabled,
|
||||||
|
.lt-input[readonly],
|
||||||
|
.lt-textarea[readonly] {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lt-input::placeholder,
|
.lt-input::placeholder,
|
||||||
@@ -949,6 +981,47 @@ select option:checked {
|
|||||||
text-shadow: var(--glow-cyan);
|
text-shadow: var(--glow-cyan);
|
||||||
}
|
}
|
||||||
.lt-checkbox:hover { border-color: var(--accent-cyan); }
|
.lt-checkbox:hover { border-color: var(--accent-cyan); }
|
||||||
|
.lt-checkbox:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.lt-checkbox:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radio (same visual language as checkbox) */
|
||||||
|
.lt-radio {
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1px solid var(--accent-cyan-border);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-terminal);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.lt-radio:checked {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background: var(--accent-cyan-dim);
|
||||||
|
}
|
||||||
|
.lt-radio:checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
box-shadow: var(--glow-cyan);
|
||||||
|
}
|
||||||
|
.lt-radio:hover { border-color: var(--accent-cyan); }
|
||||||
|
.lt-radio:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.lt-radio:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -1055,7 +1128,7 @@ select option:checked {
|
|||||||
.lt-status-closed { color: var(--status-closed); background: rgba(255,45,85,0.07); }
|
.lt-status-closed { color: var(--status-closed); background: rgba(255,45,85,0.07); }
|
||||||
.lt-status-online { color: var(--status-online); background: rgba(0,255,136,0.07); text-shadow: var(--glow-green); }
|
.lt-status-online { color: var(--status-online); background: rgba(0,255,136,0.07); text-shadow: var(--glow-green); }
|
||||||
.lt-status-offline { color: var(--status-offline); background: rgba(255,45,85,0.07); }
|
.lt-status-offline { color: var(--status-offline); background: rgba(255,45,85,0.07); }
|
||||||
.lt-status-running { color: var(--status-running); background: rgba(255,179,0,0.07); animation: pulse-amber 2s infinite; }
|
.lt-status-running { color: var(--status-running); background: rgba(255,179,0,0.07); animation: pulse-amber 2s infinite; will-change: color; }
|
||||||
.lt-status-completed { color: var(--status-completed); background: rgba(0,212,255,0.07); }
|
.lt-status-completed { color: var(--status-completed); background: rgba(0,212,255,0.07); }
|
||||||
.lt-status-failed { color: var(--status-failed); background: rgba(255,45,85,0.07); }
|
.lt-status-failed { color: var(--status-failed); background: rgba(255,45,85,0.07); }
|
||||||
|
|
||||||
@@ -1107,6 +1180,17 @@ select option:checked {
|
|||||||
.lt-badge-amber { color: var(--accent-amber); }
|
.lt-badge-amber { color: var(--accent-amber); }
|
||||||
.lt-badge-red { color: var(--accent-red); }
|
.lt-badge-red { color: var(--accent-red); }
|
||||||
|
|
||||||
|
/* Status + priority badge variants (dark-mode base) */
|
||||||
|
.lt-badge-open { color: var(--accent-green); background: rgba(0,255,136,0.08); border-color: rgba(0,255,136,0.35); text-shadow: var(--glow-green); }
|
||||||
|
.lt-badge-closed { color: var(--text-muted); background: rgba(74,90,112,0.10); border-color: rgba(74,90,112,0.35); }
|
||||||
|
.lt-badge-in-progress,
|
||||||
|
.lt-badge-progress { color: var(--accent-amber); background: rgba(255,179,0,0.08); border-color: rgba(255,179,0,0.35); }
|
||||||
|
.lt-badge-pending { color: var(--accent-purple); background: rgba(191,95,255,0.08); border-color: rgba(191,95,255,0.35); }
|
||||||
|
.lt-badge-p1 { color: var(--accent-red); background: rgba(255,45,85,0.09); border-color: rgba(255,45,85,0.40); text-shadow: var(--glow-red); }
|
||||||
|
.lt-badge-p2 { color: var(--accent-orange); background: rgba(255,107,0,0.09); border-color: rgba(255,107,0,0.38); }
|
||||||
|
.lt-badge-p3 { color: var(--accent-cyan); background: rgba(0,212,255,0.07); border-color: rgba(0,212,255,0.30); }
|
||||||
|
.lt-badge-p4 { color: var(--accent-green); background: rgba(0,255,136,0.07); border-color: rgba(0,255,136,0.30); }
|
||||||
|
|
||||||
/* Status dots */
|
/* Status dots */
|
||||||
.lt-dot {
|
.lt-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -1115,9 +1199,9 @@ select option:checked {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.lt-dot-up { background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green), 0 0 14px var(--accent-green); animation: pulse-green 2.2s ease-in-out infinite; }
|
.lt-dot-up { background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green), 0 0 14px var(--accent-green); animation: pulse-green 2.2s ease-in-out infinite; will-change: box-shadow; }
|
||||||
.lt-dot-down { background: var(--accent-red); box-shadow: 0 0 6px var(--accent-red), 0 0 12px var(--accent-red); }
|
.lt-dot-down { background: var(--accent-red); box-shadow: 0 0 6px var(--accent-red), 0 0 12px var(--accent-red); }
|
||||||
.lt-dot-warn { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber), 0 0 12px var(--accent-amber); animation: pulse-amber 2.2s ease-in-out infinite; }
|
.lt-dot-warn { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber), 0 0 12px var(--accent-amber); animation: pulse-amber 2.2s ease-in-out infinite; will-change: box-shadow; }
|
||||||
.lt-dot-idle { background: var(--text-muted); box-shadow: none; }
|
.lt-dot-idle { background: var(--text-muted); box-shadow: none; }
|
||||||
|
|
||||||
|
|
||||||
@@ -1193,7 +1277,9 @@ select option:checked {
|
|||||||
padding: 0.2rem 0.4rem;
|
padding: 0.2rem 0.4rem;
|
||||||
transition: var(--transition-fast);
|
transition: var(--transition-fast);
|
||||||
}
|
}
|
||||||
.lt-modal-close:hover { color: var(--accent-red); text-shadow: var(--glow-red); }
|
.lt-modal-close:hover { color: var(--accent-red); text-shadow: var(--glow-red); }
|
||||||
|
.lt-modal-close:active { color: var(--accent-red); opacity: 0.7; }
|
||||||
|
.lt-modal-close:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
|
||||||
|
|
||||||
.lt-modal-body {
|
.lt-modal-body {
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
@@ -1292,6 +1378,10 @@ select option:checked {
|
|||||||
border-color: var(--accent-orange-border);
|
border-color: var(--accent-orange-border);
|
||||||
border-bottom-color: var(--bg-card);
|
border-bottom-color: var(--bg-card);
|
||||||
}
|
}
|
||||||
|
.lt-tab:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-orange);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
.lt-tab-panel { display: none; }
|
.lt-tab-panel { display: none; }
|
||||||
.lt-tab-panel.active { display: block; }
|
.lt-tab-panel.active { display: block; }
|
||||||
@@ -1339,8 +1429,10 @@ select option:checked {
|
|||||||
|
|
||||||
.lt-filter-group {
|
.lt-filter-group {
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
padding-bottom: var(--space-md);
|
padding: 0 0 var(--space-md) 0;
|
||||||
|
border: none;
|
||||||
border-bottom: 1px solid var(--border-color-dim);
|
border-bottom: 1px solid var(--border-color-dim);
|
||||||
|
min-width: 0; /* fieldset UA reset */
|
||||||
}
|
}
|
||||||
.lt-filter-group:last-child { border-bottom: none; }
|
.lt-filter-group:last-child { border-bottom: none; }
|
||||||
|
|
||||||
@@ -2195,7 +2287,35 @@ select option:checked {
|
|||||||
.lt-text-upper { text-transform: uppercase; letter-spacing: 0.1em; }
|
.lt-text-upper { text-transform: uppercase; letter-spacing: 0.1em; }
|
||||||
|
|
||||||
.lt-hidden { display: none !important; }
|
.lt-hidden { display: none !important; }
|
||||||
.lt-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
|
||||||
|
/* Skip navigation link — visible only on focus */
|
||||||
|
.lt-skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: var(--space-sm);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
background: var(--bg-terminal);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
border: 1px solid var(--accent-cyan);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
z-index: var(--z-toast);
|
||||||
|
transition: top 0.1s;
|
||||||
|
}
|
||||||
|
.lt-skip-link:focus { top: var(--space-sm); }
|
||||||
|
|
||||||
|
.lt-sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%); /* replaces deprecated clip: rect(0,0,0,0) */
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Cursor blink */
|
/* Cursor blink */
|
||||||
.lt-cursor::after {
|
.lt-cursor::after {
|
||||||
@@ -2206,6 +2326,9 @@ select option:checked {
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.lt-cursor::after { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Glitch text effect */
|
/* Glitch text effect */
|
||||||
.lt-glitch { position: relative; }
|
.lt-glitch { position: relative; }
|
||||||
@@ -2221,11 +2344,13 @@ select option:checked {
|
|||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
animation: glitch-1 4s infinite;
|
animation: glitch-1 4s infinite;
|
||||||
|
will-change: clip-path, transform;
|
||||||
}
|
}
|
||||||
.lt-glitch::after {
|
.lt-glitch::after {
|
||||||
color: var(--accent-orange);
|
color: var(--accent-orange);
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
animation: glitch-2 4s 0.12s infinite;
|
animation: glitch-2 4s 0.12s infinite;
|
||||||
|
will-change: clip-path, transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2459,6 +2584,7 @@ select option:checked {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.lt-accordion-header:hover { background: var(--bg-tertiary); color: var(--accent-cyan); }
|
.lt-accordion-header:hover { background: var(--bg-tertiary); color: var(--accent-cyan); }
|
||||||
|
.lt-accordion-header:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; }
|
||||||
.lt-accordion-header[aria-expanded="true"] { color: var(--accent-orange); }
|
.lt-accordion-header[aria-expanded="true"] { color: var(--accent-orange); }
|
||||||
.lt-accordion-icon {
|
.lt-accordion-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
@@ -2573,9 +2699,17 @@ input[type="range"].lt-range {
|
|||||||
height: 4px;
|
height: 4px;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border-dim);
|
border: 1px solid var(--border-dim);
|
||||||
outline: none;
|
outline: none; /* reset; :focus-visible below provides keyboard ring */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
input[type="range"].lt-range:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-orange);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
input[type="range"].lt-range:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
input[type="range"].lt-range::-webkit-slider-thumb {
|
input[type="range"].lt-range::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 14px; height: 14px;
|
width: 14px; height: 14px;
|
||||||
@@ -2586,6 +2720,7 @@ input[type="range"].lt-range::-webkit-slider-thumb {
|
|||||||
transition: transform 0.15s;
|
transition: transform 0.15s;
|
||||||
}
|
}
|
||||||
input[type="range"].lt-range::-webkit-slider-thumb:hover { transform: scale(1.3); }
|
input[type="range"].lt-range::-webkit-slider-thumb:hover { transform: scale(1.3); }
|
||||||
|
input[type="range"].lt-range:focus-visible::-webkit-slider-thumb { transform: scale(1.3); box-shadow: 0 0 0 3px var(--bg-primary), 0 0 0 5px var(--accent-cyan); }
|
||||||
input[type="range"].lt-range::-moz-range-thumb {
|
input[type="range"].lt-range::-moz-range-thumb {
|
||||||
width: 14px; height: 14px;
|
width: 14px; height: 14px;
|
||||||
background: var(--accent-orange);
|
background: var(--accent-orange);
|
||||||
@@ -2638,6 +2773,8 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-file-item-name { color: var(--text-secondary); }
|
.lt-file-item-name { color: var(--text-secondary); }
|
||||||
.lt-file-item-size { color: var(--text-dim); }
|
.lt-file-item-size { color: var(--text-dim); }
|
||||||
.lt-file-item-remove { background: none; border: none; color: var(--accent-red); cursor: pointer; padding: 0; font-size: 0.9rem; }
|
.lt-file-item-remove { background: none; border: none; color: var(--accent-red); cursor: pointer; padding: 0; font-size: 0.9rem; }
|
||||||
|
.lt-file-item-remove:hover { opacity: 0.75; text-shadow: var(--glow-red); }
|
||||||
|
.lt-file-item-remove:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -2681,6 +2818,7 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
caret-color: var(--accent-orange);
|
caret-color: var(--accent-orange);
|
||||||
}
|
}
|
||||||
|
.lt-cmd-input-wrap:focus-within { border-bottom-color: var(--accent-cyan); }
|
||||||
.lt-cmd-results { max-height: 320px; overflow-y: auto; }
|
.lt-cmd-results { max-height: 320px; overflow-y: auto; }
|
||||||
.lt-cmd-group-label {
|
.lt-cmd-group-label {
|
||||||
padding: var(--space-xs) var(--space-md);
|
padding: var(--space-xs) var(--space-md);
|
||||||
@@ -2788,7 +2926,7 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.tok-kw { color: var(--accent-cyan); }
|
.tok-kw { color: var(--accent-cyan); }
|
||||||
.tok-str { color: var(--accent-green); }
|
.tok-str { color: var(--accent-green); }
|
||||||
.tok-num { color: var(--accent-orange); }
|
.tok-num { color: var(--accent-orange); }
|
||||||
.tok-cmt { color: #5c8c6a; font-style: italic; }
|
.tok-cmt { color: var(--color-tok-cmt); font-style: italic; }
|
||||||
.tok-fn { color: var(--accent-purple); }
|
.tok-fn { color: var(--accent-purple); }
|
||||||
|
|
||||||
|
|
||||||
@@ -2827,6 +2965,7 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.lt-tag-remove:hover { opacity: 1; }
|
.lt-tag-remove:hover { opacity: 1; }
|
||||||
|
.lt-tag-remove:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; border-radius: 2px; opacity: 1; }
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -3113,7 +3252,8 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-gauge svg { overflow: visible; }
|
.lt-gauge svg { overflow: visible; }
|
||||||
.lt-gauge-track { fill: none; stroke: var(--bg-tertiary); stroke-width: 8; }
|
.lt-gauge-track { fill: none; stroke: var(--bg-tertiary); stroke-width: 8; }
|
||||||
.lt-gauge-fill { fill: none; stroke: var(--accent-orange); stroke-width: 8; stroke-linecap: butt;
|
.lt-gauge-fill { fill: none; stroke: var(--accent-orange); stroke-width: 8; stroke-linecap: butt;
|
||||||
transition: stroke-dashoffset 0.6s cubic-bezier(0.4,0,0.2,1); }
|
transition: stroke-dashoffset 0.6s cubic-bezier(0.4,0,0.2,1);
|
||||||
|
will-change: stroke-dashoffset; }
|
||||||
.lt-gauge-label {
|
.lt-gauge-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0; left: 50%;
|
bottom: 0; left: 50%;
|
||||||
@@ -3138,6 +3278,7 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
border-top-color: var(--accent-orange);
|
border-top-color: var(--accent-orange);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: lt-spin 0.7s linear infinite;
|
animation: lt-spin 0.7s linear infinite;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
.lt-spinner--cyan { border-top-color: var(--accent-cyan); }
|
.lt-spinner--cyan { border-top-color: var(--accent-cyan); }
|
||||||
.lt-spinner--green { border-top-color: var(--accent-green); }
|
.lt-spinner--green { border-top-color: var(--accent-green); }
|
||||||
@@ -3247,7 +3388,6 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-border-red { border-color: var(--accent-red) !important; }
|
.lt-border-red { border-color: var(--accent-red) !important; }
|
||||||
|
|
||||||
/* Display */
|
/* Display */
|
||||||
.lt-hidden { display: none !important; }
|
|
||||||
.lt-visible { display: block !important; }
|
.lt-visible { display: block !important; }
|
||||||
.lt-flex { display: flex; }
|
.lt-flex { display: flex; }
|
||||||
.lt-grid { display: grid; }
|
.lt-grid { display: grid; }
|
||||||
@@ -3404,6 +3544,9 @@ html[data-theme="light"] {
|
|||||||
--box-glow-red: 0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12);
|
--box-glow-red: 0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12);
|
||||||
--box-glow-amber: 0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12);
|
--box-glow-amber: 0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12);
|
||||||
|
|
||||||
|
/* — Syntax highlight — */
|
||||||
|
--color-tok-cmt: #2e6540;
|
||||||
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3504,13 +3647,16 @@ html[data-theme="light"] .lt-table-wrap { border-color: var(--border-color);
|
|||||||
|
|
||||||
/* — Badges & status — */
|
/* — Badges & status — */
|
||||||
html[data-theme="light"] .lt-badge { background: var(--bg-tertiary); color: var(--text-secondary); }
|
html[data-theme="light"] .lt-badge { background: var(--bg-tertiary); color: var(--text-secondary); }
|
||||||
html[data-theme="light"] .lt-badge-open { background: rgba(0,109,53,0.12); color: #006d35; }
|
html[data-theme="light"] .lt-badge-open { background: rgba(0,109,53,0.12); color: #006d35; }
|
||||||
html[data-theme="light"] .lt-badge-closed { background: rgba(90,110,150,0.12); color: #5a6e8c; }
|
html[data-theme="light"] .lt-badge-closed { background: rgba(90,110,150,0.12); color: #5a6e8c; }
|
||||||
html[data-theme="light"] .lt-badge-p1 { background: rgba(181,0,31,0.12); color: #b5001f; }
|
html[data-theme="light"] .lt-badge-in-progress,
|
||||||
html[data-theme="light"] .lt-badge-p2 { background: rgba(196,78,0,0.12); color: #c44e00; }
|
html[data-theme="light"] .lt-badge-progress { background: rgba(138,90,0,0.12); color: #8a5a00; text-shadow: none; }
|
||||||
html[data-theme="light"] .lt-badge-p3 { background: rgba(138,90,0,0.12); color: #8a5a00; }
|
html[data-theme="light"] .lt-badge-pending { background: rgba(107,47,184,0.10); color: #7c22cc; }
|
||||||
html[data-theme="light"] .lt-badge-p4 { background: rgba(50,80,130,0.10); color: #2d3d56; }
|
html[data-theme="light"] .lt-badge-p1 { background: rgba(181,0,31,0.12); color: #b5001f; text-shadow: none; }
|
||||||
html[data-theme="light"] .lt-badge-admin { background: rgba(107,47,184,0.12); color: #6b2fb8; }
|
html[data-theme="light"] .lt-badge-p2 { background: rgba(196,78,0,0.12); color: #c44e00; }
|
||||||
|
html[data-theme="light"] .lt-badge-p3 { background: rgba(138,90,0,0.12); color: #8a5a00; }
|
||||||
|
html[data-theme="light"] .lt-badge-p4 { background: rgba(50,80,130,0.10); color: #2d3d56; }
|
||||||
|
html[data-theme="light"] .lt-badge-admin { background: rgba(107,47,184,0.12); color: #6b2fb8; }
|
||||||
|
|
||||||
/* — Toast — */
|
/* — Toast — */
|
||||||
html[data-theme="light"] .lt-toast {
|
html[data-theme="light"] .lt-toast {
|
||||||
@@ -3614,8 +3760,8 @@ html[data-theme="light"] .lt-divider-label { color: var(--text-dim); }
|
|||||||
html[data-theme="light"] .lt-tag { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-secondary); }
|
html[data-theme="light"] .lt-tag { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-secondary); }
|
||||||
|
|
||||||
/* — Tooltips — */
|
/* — Tooltips — */
|
||||||
html[data-theme="light"] [data-tooltip]::before { background: #1a2035; color: #fff; }
|
html[data-theme="light"] [data-tooltip]::before { background: var(--bg-terminal); color: var(--text-primary); }
|
||||||
html[data-theme="light"] [data-tooltip]::after { border-top-color: #1a2035; }
|
html[data-theme="light"] [data-tooltip]::after { border-top-color: var(--bg-terminal); }
|
||||||
|
|
||||||
/* — Pagination — */
|
/* — Pagination — */
|
||||||
html[data-theme="light"] .lt-page-btn { background: var(--bg-card); border-color: var(--border-color); color: var(--text-secondary); }
|
html[data-theme="light"] .lt-page-btn { background: var(--bg-card); border-color: var(--border-color); color: var(--text-secondary); }
|
||||||
@@ -3766,6 +3912,7 @@ html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--acc
|
|||||||
animation: lt-shimmer 1.6s ease-in-out infinite;
|
animation: lt-shimmer 1.6s ease-in-out infinite;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
display: block;
|
display: block;
|
||||||
|
will-change: opacity;
|
||||||
}
|
}
|
||||||
@keyframes lt-shimmer {
|
@keyframes lt-shimmer {
|
||||||
0% { background-position: 200% 0; }
|
0% { background-position: 200% 0; }
|
||||||
@@ -3941,7 +4088,8 @@ html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--acc
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: var(--transition-fast);
|
transition: var(--transition-fast);
|
||||||
}
|
}
|
||||||
.lt-drawer-right-close:hover { color: var(--accent-red); border-color: var(--accent-red); }
|
.lt-drawer-right-close:hover { color: var(--accent-red); border-color: var(--accent-red); }
|
||||||
|
.lt-drawer-right-close:active { color: var(--accent-red); opacity: 0.7; }
|
||||||
.lt-drawer-right-close:focus-visible { outline: 1px solid var(--accent-cyan); outline-offset: 2px; }
|
.lt-drawer-right-close:focus-visible { outline: 1px solid var(--accent-cyan); outline-offset: 2px; }
|
||||||
.lt-drawer-right-body {
|
.lt-drawer-right-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -4056,6 +4204,7 @@ html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--acc
|
|||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
}
|
}
|
||||||
.lt-combobox-tag-remove:hover { color: var(--accent-red); }
|
.lt-combobox-tag-remove:hover { color: var(--accent-red); }
|
||||||
|
.lt-combobox-tag-remove:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; border-radius: 2px; }
|
||||||
/* Dropdown list */
|
/* Dropdown list */
|
||||||
.lt-combobox-dropdown {
|
.lt-combobox-dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -4908,7 +5057,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
|
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
|
||||||
z-index: 10020;
|
z-index: var(--z-panel);
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||||
transform-origin: top right;
|
transform-origin: top right;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
@@ -4946,7 +5095,9 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.lt-notif-panel-clear:hover { text-decoration: underline; }
|
.lt-notif-panel-clear:hover { text-decoration: underline; }
|
||||||
|
.lt-notif-panel-clear:active { opacity: 0.7; }
|
||||||
|
.lt-notif-panel-clear:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
|
||||||
|
|
||||||
.lt-notif-panel-list { max-height: min(280px, calc(80vh - 110px)); overflow-y: auto; }
|
.lt-notif-panel-list { max-height: min(280px, calc(80vh - 110px)); overflow-y: auto; }
|
||||||
|
|
||||||
@@ -5015,7 +5166,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
|
||||||
z-index: 10020;
|
z-index: var(--z-panel);
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
@@ -5250,3 +5401,24 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
|||||||
.lt-stats-grid { grid-template-columns: repeat(8, 1fr); max-width: 100%; }
|
.lt-stats-grid { grid-template-columns: repeat(8, 1fr); max-width: 100%; }
|
||||||
.lt-modal { max-width: 800px; }
|
.lt-modal { max-width: 800px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
79. FOOTER
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
.lt-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
border-top: 1px solid var(--border-dim);
|
||||||
|
margin-top: auto;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.lt-footer { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<!--
|
<!--
|
||||||
LOTUSGUILD TERMINAL DESIGN SYSTEM v2.0 — base.html
|
LOTUSGUILD TERMINAL DESIGN SYSTEM v1.2 — base.html
|
||||||
Reference template showing every component and layout pattern.
|
Reference template showing every component and layout pattern.
|
||||||
|
|
||||||
This file is a STATIC DEMO. Framework-specific wiring is in:
|
This file is a STATIC DEMO. Framework-specific wiring is in:
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>MY APP — LotusGuild</title>
|
<title>MY APP — LotusGuild</title>
|
||||||
<meta name="description" content="LotusGuild infrastructure application">
|
<meta name="description" content="LotusGuild infrastructure application">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
<!-- =========================================================
|
<!-- =========================================================
|
||||||
Security headers are set server-side. CSP nonce is injected
|
Security headers are set server-side. CSP nonce is injected
|
||||||
@@ -44,6 +45,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
|
||||||
|
|
||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
BOOT SEQUENCE OVERLAY
|
BOOT SEQUENCE OVERLAY
|
||||||
Displays once per session. Remove if not desired.
|
Displays once per session. Remove if not desired.
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
MOBILE NAV DRAWER (hidden on desktop, slides in on mobile)
|
MOBILE NAV DRAWER (hidden on desktop, slides in on mobile)
|
||||||
=========================================================== -->
|
=========================================================== -->
|
||||||
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-label="Navigation menu">
|
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
|
||||||
<div class="lt-nav-drawer-header">
|
<div class="lt-nav-drawer-header">
|
||||||
<span class="lt-brand-title">MY APP</span>
|
<span class="lt-brand-title">MY APP</span>
|
||||||
<button class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close menu">✕</button>
|
<button class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close menu">✕</button>
|
||||||
@@ -168,7 +171,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Right-side detail drawer -->
|
<!-- Right-side detail drawer -->
|
||||||
<div id="lt-detail-drawer" class="lt-drawer-right" aria-hidden="true" role="dialog" aria-label="Detail panel" data-overlay="lt-detail-overlay">
|
<div id="lt-detail-drawer" class="lt-drawer-right" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Detail panel" data-overlay="lt-detail-overlay">
|
||||||
<div class="lt-drawer-right-header">
|
<div class="lt-drawer-right-header">
|
||||||
<span class="lt-drawer-right-title">// TICKET DETAIL</span>
|
<span class="lt-drawer-right-title">// TICKET DETAIL</span>
|
||||||
<button class="lt-drawer-right-close" data-drawer-close aria-label="Close detail panel">✕</button>
|
<button class="lt-drawer-right-close" data-drawer-close aria-label="Close detail panel">✕</button>
|
||||||
@@ -224,7 +227,7 @@
|
|||||||
<div class="lt-form-group" style="margin-bottom:0.5rem">
|
<div class="lt-form-group" style="margin-bottom:0.5rem">
|
||||||
<textarea id="td-comment" class="lt-input lt-textarea" rows="2" placeholder="Leave a comment…" style="resize:vertical"></textarea>
|
<textarea id="td-comment" class="lt-input lt-textarea" rows="2" placeholder="Leave a comment…" style="resize:vertical"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="
|
<button type="button" class="lt-btn lt-btn-sm" onclick="
|
||||||
const c=document.getElementById('td-comment');
|
const c=document.getElementById('td-comment');
|
||||||
if(c.value.trim()){lt.toast.success('Comment posted');c.value='';}
|
if(c.value.trim()){lt.toast.success('Comment posted');c.value='';}
|
||||||
else lt.toast.warning('Comment is empty');
|
else lt.toast.warning('Comment is empty');
|
||||||
@@ -248,8 +251,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-drawer-right-footer">
|
<div class="lt-drawer-right-footer">
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.rightDrawer.close('lt-detail-drawer')">Cancel</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.rightDrawer.close('lt-detail-drawer')">Cancel</button>
|
||||||
<button class="lt-btn lt-btn-primary lt-btn-sm" onclick="lt.toast.success('Ticket #123456789 updated')">Save Changes</button>
|
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" onclick="lt.toast.success('Ticket #123456789 updated')">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="lt-detail-overlay" class="lt-drawer-right-overlay"></div>
|
<div id="lt-detail-overlay" class="lt-drawer-right-overlay"></div>
|
||||||
@@ -257,7 +260,7 @@
|
|||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
MAIN CONTENT AREA
|
MAIN CONTENT AREA
|
||||||
=========================================================== -->
|
=========================================================== -->
|
||||||
<main class="lt-main lt-container">
|
<main class="lt-main lt-container" id="main-content">
|
||||||
|
|
||||||
<!-- Page title bar -->
|
<!-- Page title bar -->
|
||||||
<div class="lt-page-header">
|
<div class="lt-page-header">
|
||||||
@@ -306,10 +309,10 @@
|
|||||||
<!-- ==========================================================
|
<!-- ==========================================================
|
||||||
TAB NAVIGATION
|
TAB NAVIGATION
|
||||||
========================================================== -->
|
========================================================== -->
|
||||||
<div class="lt-tabs">
|
<div class="lt-tabs" role="tablist" aria-label="Main views">
|
||||||
<button class="lt-tab active" data-tab="tab-table">Table View</button>
|
<button class="lt-tab active" role="tab" data-tab="tab-table" aria-selected="true" aria-controls="tab-table" id="tab-btn-table">Table View</button>
|
||||||
<button class="lt-tab" data-tab="tab-kanban">Kanban</button>
|
<button class="lt-tab" role="tab" data-tab="tab-kanban" aria-selected="false" aria-controls="tab-kanban" id="tab-btn-kanban">Kanban</button>
|
||||||
<button class="lt-tab" data-tab="tab-workers">Workers</button>
|
<button class="lt-tab" role="tab" data-tab="tab-workers" aria-selected="false" aria-controls="tab-workers" id="tab-btn-workers">Workers</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==========================================================
|
<!-- ==========================================================
|
||||||
@@ -326,8 +329,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="lt-sidebar-body">
|
<div class="lt-sidebar-body">
|
||||||
|
|
||||||
<div class="lt-filter-group">
|
<fieldset class="lt-filter-group">
|
||||||
<span class="lt-filter-label">Status</span>
|
<legend class="lt-filter-label">Status</legend>
|
||||||
<label class="lt-filter-option">
|
<label class="lt-filter-option">
|
||||||
<input type="checkbox" class="lt-checkbox" checked> Open
|
<input type="checkbox" class="lt-checkbox" checked> Open
|
||||||
</label>
|
</label>
|
||||||
@@ -340,10 +343,10 @@
|
|||||||
<label class="lt-filter-option">
|
<label class="lt-filter-option">
|
||||||
<input type="checkbox" class="lt-checkbox"> Closed
|
<input type="checkbox" class="lt-checkbox"> Closed
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<div class="lt-filter-group">
|
<fieldset class="lt-filter-group">
|
||||||
<span class="lt-filter-label">Priority</span>
|
<legend class="lt-filter-label">Priority</legend>
|
||||||
<label class="lt-filter-option">
|
<label class="lt-filter-option">
|
||||||
<input type="checkbox" class="lt-checkbox"> P1 Critical
|
<input type="checkbox" class="lt-checkbox"> P1 Critical
|
||||||
</label>
|
</label>
|
||||||
@@ -353,7 +356,7 @@
|
|||||||
<label class="lt-filter-option">
|
<label class="lt-filter-option">
|
||||||
<input type="checkbox" class="lt-checkbox"> P3 Medium
|
<input type="checkbox" class="lt-checkbox"> P3 Medium
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<div class="lt-filter-group">
|
<div class="lt-filter-group">
|
||||||
<span class="lt-filter-label">Assigned To</span>
|
<span class="lt-filter-label">Assigned To</span>
|
||||||
@@ -381,7 +384,7 @@
|
|||||||
<div class="lt-toolbar-left">
|
<div class="lt-toolbar-left">
|
||||||
<div class="lt-search">
|
<div class="lt-search">
|
||||||
<input type="search" class="lt-input lt-search-input" id="ticket-search"
|
<input type="search" class="lt-input lt-search-input" id="ticket-search"
|
||||||
placeholder="Search tickets..." aria-label="Search">
|
placeholder="Search tickets..." aria-label="Search" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<!-- Advanced filter dropdown -->
|
<!-- Advanced filter dropdown -->
|
||||||
<div class="lt-dropdown-wrap" id="adv-filter-wrap">
|
<div class="lt-dropdown-wrap" id="adv-filter-wrap">
|
||||||
@@ -419,8 +422,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:0.5rem;margin-top:0.25rem">
|
<div style="display:flex;gap:0.5rem;margin-top:0.25rem">
|
||||||
<button class="lt-btn lt-btn-sm lt-btn-primary" style="flex:1" onclick="lt.toast.info('Filters applied');document.getElementById('adv-filter-panel').setAttribute('aria-hidden','true');document.getElementById('adv-filter-btn').setAttribute('aria-expanded','false')">Apply</button>
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-primary" style="flex:1" onclick="lt.toast.info('Filters applied');document.getElementById('adv-filter-panel').setAttribute('aria-hidden','true');document.getElementById('adv-filter-btn').setAttribute('aria-expanded','false')">Apply</button>
|
||||||
<button class="lt-btn lt-btn-sm lt-btn-ghost" onclick="lt.toast.info('Filters cleared')">Reset</button>
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" onclick="lt.toast.info('Filters cleared')">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,11 +435,11 @@
|
|||||||
<div class="lt-dropdown-wrap" id="bulk-action-wrap">
|
<div class="lt-dropdown-wrap" id="bulk-action-wrap">
|
||||||
<button class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger" id="bulk-action-btn" aria-expanded="false" aria-haspopup="true">Bulk Actions ▾</button>
|
<button class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger" id="bulk-action-btn" aria-expanded="false" aria-haspopup="true">Bulk Actions ▾</button>
|
||||||
<div class="lt-dropdown-panel lt-dropdown-panel--right" id="bulk-action-panel" aria-hidden="true">
|
<div class="lt-dropdown-panel lt-dropdown-panel--right" id="bulk-action-panel" aria-hidden="true">
|
||||||
<button class="lt-dropdown-item" onclick="lt.toast.success('Closed selected tickets');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">✓ Close Selected</button>
|
<button type="button" class="lt-dropdown-item" onclick="lt.toast.success('Closed selected tickets');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">✓ Close Selected</button>
|
||||||
<button class="lt-dropdown-item" onclick="lt.toast.info('Reassign dialog…');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">↩ Reassign…</button>
|
<button type="button" class="lt-dropdown-item" onclick="lt.toast.info('Reassign dialog…');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">↩ Reassign…</button>
|
||||||
<button class="lt-dropdown-item" onclick="lt.toast.info('Exporting selected…');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">⤓ Export Selected</button>
|
<button type="button" class="lt-dropdown-item" onclick="lt.toast.info('Exporting selected…');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">⤓ Export Selected</button>
|
||||||
<div class="lt-dropdown-divider"></div>
|
<div class="lt-dropdown-divider"></div>
|
||||||
<button class="lt-dropdown-item lt-dropdown-item--danger" onclick="lt.toast.error('Deleted selected');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">🗑 Delete Selected</button>
|
<button type="button" class="lt-dropdown-item lt-dropdown-item--danger" onclick="lt.toast.error('Deleted selected');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">🗑 Delete Selected</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,7 +448,7 @@
|
|||||||
<!-- ==================================================
|
<!-- ==================================================
|
||||||
TAB PANEL: TABLE VIEW
|
TAB PANEL: TABLE VIEW
|
||||||
================================================== -->
|
================================================== -->
|
||||||
<div id="tab-table" class="lt-tab-panel active">
|
<div id="tab-table" class="lt-tab-panel active" role="tabpanel" aria-labelledby="tab-btn-table">
|
||||||
|
|
||||||
<!-- Outer ASCII frame wrapping the table -->
|
<!-- Outer ASCII frame wrapping the table -->
|
||||||
<div class="lt-frame">
|
<div class="lt-frame">
|
||||||
@@ -459,21 +462,21 @@
|
|||||||
<caption class="lt-sr-only">Ticket queue — sorted by priority</caption>
|
<caption class="lt-sr-only">Ticket queue — sorted by priority</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" class="lt-checkbox" aria-label="Select all"></th>
|
<th scope="col"><input type="checkbox" class="lt-checkbox" aria-label="Select all"></th>
|
||||||
<th data-sort-key="id">ID</th>
|
<th scope="col" data-sort-key="id" aria-sort="none">ID</th>
|
||||||
<th data-sort-key="priority">Priority</th>
|
<th scope="col" data-sort-key="priority" aria-sort="none">Priority</th>
|
||||||
<th data-sort-key="title">Title</th>
|
<th scope="col" data-sort-key="title" aria-sort="none">Title</th>
|
||||||
<th data-sort-key="status">Status</th>
|
<th scope="col" data-sort-key="status" aria-sort="none">Status</th>
|
||||||
<th data-sort-key="assignee">Assignee</th>
|
<th scope="col" data-sort-key="assignee" aria-sort="none">Assignee</th>
|
||||||
<th data-sort-key="created">Created</th>
|
<th scope="col" data-sort-key="created" aria-sort="none">Created</th>
|
||||||
<th>Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<!-- P1 Critical row -->
|
<!-- P1 Critical row -->
|
||||||
<tr class="lt-row-p1 lt-row-critical">
|
<tr class="lt-row-p1 lt-row-critical">
|
||||||
<td data-label="Select"><input type="checkbox" class="lt-checkbox"></td>
|
<td data-label="Select"><input type="checkbox" class="lt-checkbox" aria-label="Select ticket #123456789"></td>
|
||||||
<td data-label="ID"><a href="/ticket/123456789">#123456789</a></td>
|
<td data-label="ID"><a href="/ticket/123456789">#123456789</a></td>
|
||||||
<td data-label="Priority"><span class="lt-p1">P1 Critical</span></td>
|
<td data-label="Priority"><span class="lt-p1">P1 Critical</span></td>
|
||||||
<td data-label="Title">Storage array link-down on compute-storage-01</td>
|
<td data-label="Title">Storage array link-down on compute-storage-01</td>
|
||||||
@@ -490,7 +493,7 @@
|
|||||||
|
|
||||||
<!-- P2 High row -->
|
<!-- P2 High row -->
|
||||||
<tr class="lt-row-p2 lt-row-warning">
|
<tr class="lt-row-p2 lt-row-warning">
|
||||||
<td data-label="Select"><input type="checkbox" class="lt-checkbox"></td>
|
<td data-label="Select"><input type="checkbox" class="lt-checkbox" aria-label="Select ticket #987654321"></td>
|
||||||
<td data-label="ID"><a href="/ticket/987654321">#987654321</a></td>
|
<td data-label="ID"><a href="/ticket/987654321">#987654321</a></td>
|
||||||
<td data-label="Priority"><span class="lt-p2">P2 High</span></td>
|
<td data-label="Priority"><span class="lt-p2">P2 High</span></td>
|
||||||
<td data-label="Title">Switch port flapping on USW-Pro-24</td>
|
<td data-label="Title">Switch port flapping on USW-Pro-24</td>
|
||||||
@@ -506,7 +509,7 @@
|
|||||||
|
|
||||||
<!-- P3 Medium row -->
|
<!-- P3 Medium row -->
|
||||||
<tr class="lt-row-p3">
|
<tr class="lt-row-p3">
|
||||||
<td data-label="Select"><input type="checkbox" class="lt-checkbox"></td>
|
<td data-label="Select"><input type="checkbox" class="lt-checkbox" aria-label="Select ticket #111222333"></td>
|
||||||
<td data-label="ID"><a href="/ticket/111222333">#111222333</a></td>
|
<td data-label="ID"><a href="/ticket/111222333">#111222333</a></td>
|
||||||
<td data-label="Priority"><span class="lt-p3">P3 Med</span></td>
|
<td data-label="Priority"><span class="lt-p3">P3 Med</span></td>
|
||||||
<td data-label="Title">Scheduled maintenance: replace SFP+ on large1</td>
|
<td data-label="Title">Scheduled maintenance: replace SFP+ on large1</td>
|
||||||
@@ -522,7 +525,7 @@
|
|||||||
|
|
||||||
<!-- P4 closed -->
|
<!-- P4 closed -->
|
||||||
<tr class="lt-row-p4">
|
<tr class="lt-row-p4">
|
||||||
<td data-label="Select"><input type="checkbox" class="lt-checkbox"></td>
|
<td data-label="Select"><input type="checkbox" class="lt-checkbox" aria-label="Select ticket #444555666"></td>
|
||||||
<td data-label="ID"><a href="/ticket/444555666">#444555666</a></td>
|
<td data-label="ID"><a href="/ticket/444555666">#444555666</a></td>
|
||||||
<td data-label="Priority"><span class="lt-p4">P4 Low</span></td>
|
<td data-label="Priority"><span class="lt-p4">P4 Low</span></td>
|
||||||
<td data-label="Title">Update SSL cert on wiki.lotusguild.org</td>
|
<td data-label="Title">Update SSL cert on wiki.lotusguild.org</td>
|
||||||
@@ -546,7 +549,7 @@
|
|||||||
<!-- ==================================================
|
<!-- ==================================================
|
||||||
TAB PANEL: KANBAN
|
TAB PANEL: KANBAN
|
||||||
================================================== -->
|
================================================== -->
|
||||||
<div id="tab-kanban" class="lt-tab-panel">
|
<div id="tab-kanban" class="lt-tab-panel" role="tabpanel" aria-labelledby="tab-btn-kanban">
|
||||||
<div class="lt-grid-4">
|
<div class="lt-grid-4">
|
||||||
|
|
||||||
<!-- Kanban column: Open -->
|
<!-- Kanban column: Open -->
|
||||||
@@ -634,7 +637,7 @@
|
|||||||
<!-- ==================================================
|
<!-- ==================================================
|
||||||
TAB PANEL: WORKERS
|
TAB PANEL: WORKERS
|
||||||
================================================== -->
|
================================================== -->
|
||||||
<div id="tab-workers" class="lt-tab-panel">
|
<div id="tab-workers" class="lt-tab-panel" role="tabpanel" aria-labelledby="tab-btn-workers">
|
||||||
<div class="lt-grid-3">
|
<div class="lt-grid-3">
|
||||||
|
|
||||||
<div class="lt-card">
|
<div class="lt-card">
|
||||||
@@ -644,11 +647,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="lt-data-table-wrapper">
|
<div class="lt-data-table-wrapper">
|
||||||
<table class="lt-data-table">
|
<table class="lt-data-table">
|
||||||
<tr><td class="lt-text-muted">CPU</td> <td>12%</td></tr>
|
<tbody>
|
||||||
<tr><td class="lt-text-muted">Memory</td> <td>2.1 GB / 8 GB</td></tr>
|
<tr><td class="lt-text-muted">CPU</td> <td>12%</td></tr>
|
||||||
<tr><td class="lt-text-muted">Load</td> <td>0.42 / 0.51 / 0.48</td></tr>
|
<tr><td class="lt-text-muted">Memory</td> <td>2.1 GB / 8 GB</td></tr>
|
||||||
<tr><td class="lt-text-muted">Uptime</td> <td>14d 6h</td></tr>
|
<tr><td class="lt-text-muted">Load</td> <td>0.42 / 0.51 / 0.48</td></tr>
|
||||||
<tr><td class="lt-text-muted">Tasks</td> <td>2 / 5</td></tr>
|
<tr><td class="lt-text-muted">Uptime</td> <td>14d 6h</td></tr>
|
||||||
|
<tr><td class="lt-text-muted">Tasks</td> <td>2 / 5</td></tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -783,7 +788,7 @@
|
|||||||
<div class="lt-search lt-form-group">
|
<div class="lt-search lt-form-group">
|
||||||
<label class="lt-label" for="eg-search">Search</label>
|
<label class="lt-label" for="eg-search">Search</label>
|
||||||
<input id="eg-search" type="search" class="lt-input lt-search-input"
|
<input id="eg-search" type="search" class="lt-input lt-search-input"
|
||||||
placeholder="Ctrl+K to focus">
|
placeholder="Ctrl+K to focus" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-form-group">
|
<div class="lt-form-group">
|
||||||
<label class="lt-label">Loading state (skeleton)</label>
|
<label class="lt-label">Loading state (skeleton)</label>
|
||||||
@@ -834,19 +839,19 @@
|
|||||||
<div style="display:flex;flex-direction:column;gap:var(--space-md)">
|
<div style="display:flex;flex-direction:column;gap:var(--space-md)">
|
||||||
<div>
|
<div>
|
||||||
<div class="lt-progress-label"><span>CPU LOAD</span><span>72%</span></div>
|
<div class="lt-progress-label"><span>CPU LOAD</span><span>72%</span></div>
|
||||||
<div class="lt-progress"><div class="lt-progress-bar" style="width:72%" data-width="72%"></div></div>
|
<div class="lt-progress" role="progressbar" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100" aria-label="CPU Load"><div class="lt-progress-bar" style="width:72%" data-width="72%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="lt-progress-label"><span>MEMORY</span><span>45%</span></div>
|
<div class="lt-progress-label"><span>MEMORY</span><span>45%</span></div>
|
||||||
<div class="lt-progress lt-progress--cyan"><div class="lt-progress-bar" style="width:45%" data-width="45%"></div></div>
|
<div class="lt-progress lt-progress--cyan" role="progressbar" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" aria-label="Memory"><div class="lt-progress-bar" style="width:45%" data-width="45%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="lt-progress-label"><span>DISK I/O</span><span>89%</span></div>
|
<div class="lt-progress-label"><span>DISK I/O</span><span>89%</span></div>
|
||||||
<div class="lt-progress lt-progress--red lt-progress--lg"><div class="lt-progress-bar" style="width:89%" data-width="89%"></div></div>
|
<div class="lt-progress lt-progress--red lt-progress--lg" role="progressbar" aria-valuenow="89" aria-valuemin="0" aria-valuemax="100" aria-label="Disk I/O"><div class="lt-progress-bar" style="width:89%" data-width="89%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="lt-progress-label"><span>UPTIME</span><span>100%</span></div>
|
<div class="lt-progress-label"><span>UPTIME</span><span>100%</span></div>
|
||||||
<div class="lt-progress lt-progress--green lt-progress--striped"><div class="lt-progress-bar" style="width:100%" data-width="100%"></div></div>
|
<div class="lt-progress lt-progress--green lt-progress--striped" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" aria-label="Uptime"><div class="lt-progress-bar" style="width:100%" data-width="100%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -871,25 +876,25 @@
|
|||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<div>
|
<div>
|
||||||
<div class="lt-accordion">
|
<div class="lt-accordion">
|
||||||
<button class="lt-accordion-header" aria-expanded="false" data-accordion>
|
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-1" data-accordion>
|
||||||
SYSTEM OVERVIEW
|
SYSTEM OVERVIEW
|
||||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="lt-accordion-body"><div class="lt-accordion-content">Node running at 72% CPU. 12 active processes. Last restart: 3d ago.</div></div>
|
<div class="lt-accordion-body" id="acc-body-1"><div class="lt-accordion-content">Node running at 72% CPU. 12 active processes. Last restart: 3d ago.</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-accordion">
|
<div class="lt-accordion">
|
||||||
<button class="lt-accordion-header" aria-expanded="false" data-accordion>
|
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-2" data-accordion>
|
||||||
NETWORK CONFIG
|
NETWORK CONFIG
|
||||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="lt-accordion-body"><div class="lt-accordion-content">eth0: 10.0.0.7 — MTU 1500 — RX 4.2 GB — TX 1.1 GB</div></div>
|
<div class="lt-accordion-body" id="acc-body-2"><div class="lt-accordion-content">eth0: 10.0.0.7 — MTU 1500 — RX 4.2 GB — TX 1.1 GB</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-accordion">
|
<div class="lt-accordion">
|
||||||
<button class="lt-accordion-header" aria-expanded="false" data-accordion>
|
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-3" data-accordion>
|
||||||
FIREWALL RULES
|
FIREWALL RULES
|
||||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="lt-accordion-body"><div class="lt-accordion-content">22/tcp ALLOW — 80/tcp ALLOW — 443/tcp ALLOW — */* DENY</div></div>
|
<div class="lt-accordion-body" id="acc-body-3"><div class="lt-accordion-content">22/tcp ALLOW — 80/tcp ALLOW — 443/tcp ALLOW — */* DENY</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1059,20 +1064,20 @@
|
|||||||
<div class="lt-section-header">Tab Bar</div>
|
<div class="lt-section-header">Tab Bar</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<div class="lt-tab-bar" role="tablist">
|
<div class="lt-tab-bar" role="tablist">
|
||||||
<button class="lt-tab active" role="tab" data-tab-target="tab-overview" aria-selected="true">Overview</button>
|
<button class="lt-tab active" role="tab" id="tab2-btn-overview" data-tab-target="tab-overview" aria-selected="true" aria-controls="tab-overview">Overview</button>
|
||||||
<button class="lt-tab" role="tab" data-tab-target="tab-logs" aria-selected="false">Logs</button>
|
<button class="lt-tab" role="tab" id="tab2-btn-logs" data-tab-target="tab-logs" aria-selected="false" aria-controls="tab-logs">Logs</button>
|
||||||
<button class="lt-tab" role="tab" data-tab-target="tab-metrics" aria-selected="false">Metrics</button>
|
<button class="lt-tab" role="tab" id="tab2-btn-metrics" data-tab-target="tab-metrics" aria-selected="false" aria-controls="tab-metrics">Metrics</button>
|
||||||
<button class="lt-tab" role="tab" data-tab-target="tab-config" aria-selected="false">Config</button>
|
<button class="lt-tab" role="tab" id="tab2-btn-config" data-tab-target="tab-config" aria-selected="false" aria-controls="tab-config">Config</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-tab-panels" style="padding:var(--space-md) 0">
|
<div class="lt-tab-panels" style="padding:var(--space-md) 0">
|
||||||
<div id="tab-overview" class="lt-tab-panel active">System overview: all nodes nominal. 14 active sessions.</div>
|
<div id="tab-overview" class="lt-tab-panel active" role="tabpanel" aria-labelledby="tab2-btn-overview">System overview: all nodes nominal. 14 active sessions.</div>
|
||||||
<div id="tab-logs" class="lt-tab-panel">
|
<div id="tab-logs" class="lt-tab-panel" role="tabpanel" aria-labelledby="tab2-btn-logs">
|
||||||
<pre style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-secondary);margin:0">[03:41:22] nginx: 200 GET /api/nodes (12ms)
|
<pre style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-secondary);margin:0">[03:41:22] nginx: 200 GET /api/nodes (12ms)
|
||||||
[03:41:23] cron: heartbeat OK
|
[03:41:23] cron: heartbeat OK
|
||||||
[03:41:24] alert: disk 87% on node-03</pre>
|
[03:41:24] alert: disk 87% on node-03</pre>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-metrics" class="lt-tab-panel">CPU avg: 34% — Mem avg: 61% — Net TX: 4.2 Mbps</div>
|
<div id="tab-metrics" class="lt-tab-panel" role="tabpanel" aria-labelledby="tab2-btn-metrics">CPU avg: 34% — Mem avg: 61% — Net TX: 4.2 Mbps</div>
|
||||||
<div id="tab-config" class="lt-tab-panel">Config last updated: 2026-03-20 by admin@lotusguild.io</div>
|
<div id="tab-config" class="lt-tab-panel" role="tabpanel" aria-labelledby="tab2-btn-config">Config last updated: 2026-03-20 by admin@lotusguild.io</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1107,7 +1112,7 @@
|
|||||||
<p style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-secondary);margin:0 0 var(--space-sm)">
|
<p style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-secondary);margin:0 0 var(--space-sm)">
|
||||||
Press <kbd style="background:var(--bg-tertiary);border:1px solid var(--border-dim);padding:2px 6px;font-family:var(--font-mono)">Ctrl+K</kbd> or click the button below to open the command palette.
|
Press <kbd style="background:var(--bg-tertiary);border:1px solid var(--border-dim);padding:2px 6px;font-family:var(--font-mono)">Ctrl+K</kbd> or click the button below to open the command palette.
|
||||||
</p>
|
</p>
|
||||||
<button class="lt-btn lt-btn-ghost" onclick="lt.cmdPalette.open()">⌘ Open Command Palette</button>
|
<button type="button" class="lt-btn lt-btn-ghost" onclick="lt.cmdPalette.open()">⌘ Open Command Palette</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /.lt-frame-inner (v1.2 additions) -->
|
</div><!-- /.lt-frame-inner (v1.2 additions) -->
|
||||||
@@ -1126,9 +1131,9 @@
|
|||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:1rem">Dark/light mode with OS preference detection and localStorage persistence.</p>
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:1rem">Dark/light mode with OS preference detection and localStorage persistence.</p>
|
||||||
<div class="lt-flex lt-gap-sm lt-align-center lt-wrap">
|
<div class="lt-flex lt-gap-sm lt-align-center lt-wrap">
|
||||||
<button class="lt-btn lt-btn-primary" onclick="lt.theme.toggle()">Toggle Theme</button>
|
<button type="button" class="lt-btn lt-btn-primary" onclick="lt.theme.toggle()">Toggle Theme</button>
|
||||||
<button class="lt-btn" onclick="lt.theme.set('dark')">Force Dark</button>
|
<button type="button" class="lt-btn" onclick="lt.theme.set('dark')">Force Dark</button>
|
||||||
<button class="lt-btn" onclick="lt.theme.set('light')">Force Light</button>
|
<button type="button" class="lt-btn" onclick="lt.theme.set('light')">Force Light</button>
|
||||||
<code style="font-size:0.72rem;color:var(--accent-cyan)">lt.theme.toggle() | .set('light') | .get()</code>
|
<code style="font-size:0.72rem;color:var(--accent-cyan)">lt.theme.toggle() | .set('light') | .get()</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1171,7 +1176,7 @@
|
|||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<div class="lt-grid lt-grid-3" style="gap:1rem">
|
<div class="lt-grid lt-grid-3" style="gap:1rem">
|
||||||
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">📭</div><div class="lt-empty-state-title">No Tickets Found</div><div class="lt-empty-state-body">No tickets match your current filters.</div><button class="lt-btn lt-btn-sm lt-btn-primary">Clear Filters</button></div></div>
|
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">📭</div><div class="lt-empty-state-title">No Tickets Found</div><div class="lt-empty-state-body">No tickets match your current filters.</div><button class="lt-btn lt-btn-sm lt-btn-primary">Clear Filters</button></div></div>
|
||||||
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">🔌</div><div class="lt-empty-state-title">No Workers Online</div><div class="lt-empty-state-body">All workers are offline or unreachable.</div><button class="lt-btn lt-btn-sm" onclick="lt.toast.warning('Checking workers…')">Retry</button></div></div>
|
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">🔌</div><div class="lt-empty-state-title">No Workers Online</div><div class="lt-empty-state-body">All workers are offline or unreachable.</div><button type="button" class="lt-btn lt-btn-sm" onclick="lt.toast.warning('Checking workers…')">Retry</button></div></div>
|
||||||
<div class="lt-card"><div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon">🗂</div><div class="lt-empty-state-title">No Results</div><div class="lt-empty-state-body">Try a different search term.</div></div></div>
|
<div class="lt-card"><div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon">🗂</div><div class="lt-empty-state-title">No Results</div><div class="lt-empty-state-body">Try a different search term.</div></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1200,9 +1205,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="lt-flex lt-gap-md lt-align-center lt-wrap">
|
<div class="lt-flex lt-gap-md lt-align-center lt-wrap">
|
||||||
<span class="lt-notif-wrap" id="demo-notif-btn"><button class="lt-btn lt-btn-sm">🔔 Alerts</button></span>
|
<span class="lt-notif-wrap" id="demo-notif-btn"><button class="lt-btn lt-btn-sm">🔔 Alerts</button></span>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#demo-notif-btn')">+1 Badge</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#demo-notif-btn')">+1 Badge</button>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.notif.clear('#demo-notif-btn')">Clear</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.clear('#demo-notif-btn')">Clear</button>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#lt-notif-bell')">+1 Header Bell</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#lt-notif-bell')">+1 Header Bell</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1380,8 +1385,8 @@
|
|||||||
<div class="lt-ws-status" data-state="disconnected"><span class="lt-dot"></span><span>Disconnected</span></div>
|
<div class="lt-ws-status" data-state="disconnected"><span class="lt-dot"></span><span>Disconnected</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-flex lt-gap-sm lt-wrap">
|
<div class="lt-flex lt-gap-sm lt-wrap">
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.offline.isOnline() ? lt.toast.success('Online ✓') : lt.toast.error('Offline ✗')">Check Online Status</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.offline.isOnline() ? lt.toast.success('Online ✓') : lt.toast.error('Offline ✗')">Check Online Status</button>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.toast.info('lt.ws.connect(url, { reconnect:true, onMessage: fn })')">WS API Hint</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.toast.info('lt.ws.connect(url, { reconnect:true, onMessage: fn })')">WS API Hint</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.75rem">Offline banner + body class auto-applied on <code>navigator.onLine</code> change. WS manager has exponential backoff, event emitter, status indicator binding.</p>
|
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.75rem">Offline banner + body class auto-applied on <code>navigator.onLine</code> change. WS manager has exponential backoff, event emitter, status indicator binding.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1397,7 +1402,7 @@
|
|||||||
<div id="demo-wizard">
|
<div id="demo-wizard">
|
||||||
<!-- Step indicators -->
|
<!-- Step indicators -->
|
||||||
<div class="lt-wizard-steps">
|
<div class="lt-wizard-steps">
|
||||||
<div class="lt-wizard-step is-active" data-wizard-indicator>
|
<div class="lt-wizard-step is-active" data-wizard-indicator aria-current="step">
|
||||||
<div class="lt-wizard-num">1</div>
|
<div class="lt-wizard-num">1</div>
|
||||||
<div class="lt-wizard-label">Details</div>
|
<div class="lt-wizard-label">Details</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1574,24 +1579,36 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
|
|
||||||
</main><!-- /.lt-main -->
|
</main><!-- /.lt-main -->
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
FOOTER LANDMARK
|
||||||
|
================================================================ -->
|
||||||
|
<footer class="lt-footer" role="contentinfo" aria-label="Application footer">
|
||||||
|
<span class="lt-text-muted lt-font-mono lt-text-xs">
|
||||||
|
LotusGuild Terminal Design System v1.2 — Internal Use Only
|
||||||
|
</span>
|
||||||
|
<span class="lt-text-muted lt-font-mono lt-text-xs">
|
||||||
|
© <span id="footer-year"></span> LotusGuild. All rights reserved.
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
TOAST DEMO BUTTONS (remove in production)
|
TOAST DEMO BUTTONS (remove in production)
|
||||||
=========================================================== -->
|
=========================================================== -->
|
||||||
<div style="position:fixed;bottom:1rem;left:1rem;display:flex;flex-direction:column;gap:0.5rem;z-index:900">
|
<div style="position:fixed;bottom:1rem;left:1rem;display:flex;flex-direction:column;gap:0.5rem;z-index:900">
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.toast.success('Ticket saved successfully')">✓ Toast</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.toast.success('Ticket saved successfully')">✓ Toast</button>
|
||||||
<button class="lt-btn lt-btn-sm lt-btn-danger" onclick="lt.toast.error('Network error — retry in 5s', 5000)">✗ Error</button>
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger" onclick="lt.toast.error('Network error — retry in 5s', 5000)">✗ Error</button>
|
||||||
<button class="lt-btn lt-btn-sm" onclick="lt.toast.warning('Rate limit 80% used')">! Warn</button>
|
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.toast.warning('Rate limit 80% used')">! Warn</button>
|
||||||
<button class="lt-btn lt-btn-sm lt-btn-ghost" onclick="lt.toast.info('Auto-refresh triggered')">i Info</button>
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" onclick="lt.toast.info('Auto-refresh triggered')">i Info</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
MODAL EXAMPLE: Export
|
MODAL EXAMPLE: Export
|
||||||
=========================================================== -->
|
=========================================================== -->
|
||||||
<div id="export-modal" class="lt-modal-overlay" aria-hidden="true">
|
<div id="export-modal" class="lt-modal-overlay" aria-hidden="true">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="export-modal-title">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Export Tickets</span>
|
<span class="lt-modal-title" id="export-modal-title">Export Tickets</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
@@ -1610,8 +1627,8 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
<div class="lt-msg lt-msg-info">Exports include all visible columns.</div>
|
<div class="lt-msg lt-msg-info">Exports include all visible columns.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
|
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
|
||||||
<button class="lt-btn lt-btn-primary" onclick="lt.toast.success('Export started'); lt.modal.close('export-modal')">Export</button>
|
<button type="button" class="lt-btn lt-btn-primary" onclick="lt.toast.success('Export started'); lt.modal.close('export-modal')">Export</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1620,9 +1637,9 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
MODAL EXAMPLE: Keyboard shortcuts help
|
MODAL EXAMPLE: Keyboard shortcuts help
|
||||||
=========================================================== -->
|
=========================================================== -->
|
||||||
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
|
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="keys-help-title">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Keyboard Shortcuts</span>
|
<span class="lt-modal-title" id="keys-help-title">Keyboard Shortcuts</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
@@ -1932,6 +1949,10 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Footer year
|
||||||
|
const footerYear = document.getElementById('footer-year');
|
||||||
|
if (footerYear) footerYear.textContent = new Date().getFullYear();
|
||||||
|
|
||||||
// Tab bar switching
|
// Tab bar switching
|
||||||
document.querySelectorAll('.lt-tab-bar').forEach(bar => {
|
document.querySelectorAll('.lt-tab-bar').forEach(bar => {
|
||||||
bar.addEventListener('click', e => {
|
bar.addEventListener('click', e => {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
function showToast(message, type, duration) {
|
function showToast(message, type, duration) {
|
||||||
type = type || 'info';
|
type = type || 'info';
|
||||||
duration = duration || 3500;
|
duration = duration || 3500;
|
||||||
if (_toastActive) { _toastQueue.push({ message, type, duration }); return; }
|
if (_toastActive) { if (_toastQueue.length < 12) _toastQueue.push({ message, type, duration }); return; }
|
||||||
_displayToast(message, type, duration);
|
_displayToast(message, type, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
_modalTriggers.set(el, document.activeElement);
|
_modalTriggers.set(el, document.activeElement);
|
||||||
}
|
}
|
||||||
el.classList.add('is-open');
|
el.classList.add('is-open');
|
||||||
el.setAttribute('aria-hidden', 'false');
|
el.removeAttribute('aria-hidden'); /* removing is correct; setting 'false' is an anti-pattern */
|
||||||
_lockScroll();
|
_lockScroll();
|
||||||
// Focus first focusable element
|
// Focus first focusable element
|
||||||
const first = el.querySelector(_FOCUSABLE);
|
const first = el.querySelector(_FOCUSABLE);
|
||||||
@@ -263,16 +263,21 @@
|
|||||||
lt.tabs.switch('panel-id')
|
lt.tabs.switch('panel-id')
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
function switchTab(panelId) {
|
function switchTab(panelId) {
|
||||||
document.querySelectorAll('.lt-tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.lt-tab[data-tab]').forEach(t => {
|
||||||
|
t.classList.remove('active');
|
||||||
|
t.setAttribute('aria-selected', 'false');
|
||||||
|
});
|
||||||
document.querySelectorAll('.lt-tab-panel').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.lt-tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
const btn = document.querySelector('.lt-tab[data-tab="' + panelId + '"]');
|
const btn = document.querySelector('.lt-tab[data-tab="' + panelId + '"]');
|
||||||
const panel = document.getElementById(panelId);
|
const panel = document.getElementById(panelId);
|
||||||
if (btn) btn.classList.add('active');
|
if (btn) { btn.classList.add('active'); btn.setAttribute('aria-selected', 'true'); }
|
||||||
if (panel) panel.classList.add('active');
|
if (panel) panel.classList.add('active');
|
||||||
try { localStorage.setItem('lt_activeTab_' + location.pathname, panelId); } catch (_) {}
|
try { localStorage.setItem('lt_activeTab_' + location.pathname, panelId); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _tabsInitialized = false;
|
||||||
function initTabs() {
|
function initTabs() {
|
||||||
|
if (_tabsInitialized) return; _tabsInitialized = true;
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('lt_activeTab_' + location.pathname);
|
const saved = localStorage.getItem('lt_activeTab_' + location.pathname);
|
||||||
if (saved && document.getElementById(saved)) { switchTab(saved); }
|
if (saved && document.getElementById(saved)) { switchTab(saved); }
|
||||||
@@ -392,7 +397,9 @@
|
|||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
lt.sidebar.init()
|
lt.sidebar.init()
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
|
let _sidebarInitialized = false;
|
||||||
function initSidebar() {
|
function initSidebar() {
|
||||||
|
if (_sidebarInitialized) return; _sidebarInitialized = true;
|
||||||
document.querySelectorAll('[data-sidebar-toggle]').forEach(btn => {
|
document.querySelectorAll('[data-sidebar-toggle]').forEach(btn => {
|
||||||
const sidebar = document.getElementById(btn.dataset.sidebarToggle);
|
const sidebar = document.getElementById(btn.dataset.sidebarToggle);
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
@@ -429,8 +436,11 @@
|
|||||||
lt.api.put / patch / delete
|
lt.api.put / patch / delete
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
async function apiFetch(method, url, body) {
|
async function apiFetch(method, url, body) {
|
||||||
const opts = { method, headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()) };
|
const hasBody = body !== undefined;
|
||||||
if (body !== undefined) opts.body = JSON.stringify(body);
|
const headers = Object.assign({}, csrfHeaders());
|
||||||
|
if (hasBody) headers['Content-Type'] = 'application/json'; // Only set on requests with a body
|
||||||
|
const opts = { method, headers };
|
||||||
|
if (hasBody) opts.body = JSON.stringify(body);
|
||||||
let resp;
|
let resp;
|
||||||
try { resp = await fetch(url, opts); } catch (err) { throw new Error('Network error: ' + err.message); }
|
try { resp = await fetch(url, opts); } catch (err) { throw new Error('Network error: ' + err.message); }
|
||||||
let data;
|
let data;
|
||||||
@@ -536,9 +546,11 @@
|
|||||||
const ths = Array.from(table.querySelectorAll('th[data-sort-key]'));
|
const ths = Array.from(table.querySelectorAll('th[data-sort-key]'));
|
||||||
ths.forEach((th, colIdx) => {
|
ths.forEach((th, colIdx) => {
|
||||||
let dir = 'asc';
|
let dir = 'asc';
|
||||||
|
th.setAttribute('aria-sort', 'none');
|
||||||
th.addEventListener('click', () => {
|
th.addEventListener('click', () => {
|
||||||
ths.forEach(h => h.removeAttribute('data-sort'));
|
ths.forEach(h => { h.removeAttribute('data-sort'); h.setAttribute('aria-sort', 'none'); });
|
||||||
th.setAttribute('data-sort', dir);
|
th.setAttribute('data-sort', dir);
|
||||||
|
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
|
||||||
const tbody = table.querySelector('tbody');
|
const tbody = table.querySelector('tbody');
|
||||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
rows.sort((a, b) => {
|
rows.sort((a, b) => {
|
||||||
@@ -625,7 +637,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _accordionInitialized = false;
|
||||||
function initAccordion() {
|
function initAccordion() {
|
||||||
|
if (_accordionInitialized) return; _accordionInitialized = true;
|
||||||
// Support both data-accordion attribute (HTML) and .lt-accordion-trigger class
|
// Support both data-accordion attribute (HTML) and .lt-accordion-trigger class
|
||||||
document.querySelectorAll('[data-accordion], .lt-accordion-trigger').forEach(trigger => {
|
document.querySelectorAll('[data-accordion], .lt-accordion-trigger').forEach(trigger => {
|
||||||
if (trigger.getAttribute('aria-expanded') === 'true') {
|
if (trigger.getAttribute('aria-expanded') === 'true') {
|
||||||
@@ -680,7 +694,11 @@
|
|||||||
case 'right': top = r.top + sy + r.height / 2 - tr.height / 2; left = r.right + sx + 8; break;
|
case 'right': top = r.top + sy + r.height / 2 - tr.height / 2; left = r.right + sx + 8; break;
|
||||||
default: top = r.top + sy - tr.height - 8; left = r.left + sx + r.width / 2 - tr.width / 2;
|
default: top = r.top + sy - tr.height - 8; left = r.left + sx + r.width / 2 - tr.width / 2;
|
||||||
}
|
}
|
||||||
tip.style.cssText = 'position:absolute;top:' + Math.max(4, top) + 'px;left:' + Math.max(4, left) + 'px;z-index:9000';
|
const maxLeft = (global.scrollX || 0) + global.innerWidth - tr.width - 4;
|
||||||
|
const maxTop = (global.scrollY || 0) + global.innerHeight - tr.height - 4;
|
||||||
|
left = Math.max(4 + (global.scrollX || 0), Math.min(maxLeft, left));
|
||||||
|
top = Math.max(4 + (global.scrollY || 0), Math.min(maxTop, top));
|
||||||
|
tip.style.cssText = 'position:absolute;top:' + top + 'px;left:' + left + 'px';
|
||||||
requestAnimationFrame(() => tip.classList.add('is-visible'));
|
requestAnimationFrame(() => tip.classList.add('is-visible'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +706,10 @@
|
|||||||
if (_tooltipEl) { _tooltipEl.remove(); _tooltipEl = null; }
|
if (_tooltipEl) { _tooltipEl.remove(); _tooltipEl = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _tooltipInitialized = false;
|
||||||
function initTooltips() {
|
function initTooltips() {
|
||||||
|
if (_tooltipInitialized) return;
|
||||||
|
_tooltipInitialized = true;
|
||||||
document.addEventListener('mouseover', e => { const a = e.target.closest('[data-tooltip]'); if (a) _tooltipShow(a); });
|
document.addEventListener('mouseover', e => { const a = e.target.closest('[data-tooltip]'); if (a) _tooltipShow(a); });
|
||||||
document.addEventListener('mouseout', e => { if (!e.target.closest('[data-tooltip]')) return; if (!e.relatedTarget || !e.relatedTarget.closest('[data-tooltip]')) _tooltipHide(); });
|
document.addEventListener('mouseout', e => { if (!e.target.closest('[data-tooltip]')) return; if (!e.relatedTarget || !e.relatedTarget.closest('[data-tooltip]')) _tooltipHide(); });
|
||||||
document.addEventListener('focusin', e => { const a = e.target.closest('[data-tooltip]'); if (a) _tooltipShow(a); });
|
document.addEventListener('focusin', e => { const a = e.target.closest('[data-tooltip]'); if (a) _tooltipShow(a); });
|
||||||
@@ -718,7 +739,9 @@
|
|||||||
} catch (_) { return false; }
|
} catch (_) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _copyInitialized = false;
|
||||||
function initCopyButtons() {
|
function initCopyButtons() {
|
||||||
|
if (_copyInitialized) return; _copyInitialized = true;
|
||||||
document.addEventListener('click', async function (e) {
|
document.addEventListener('click', async function (e) {
|
||||||
const btn = e.target.closest('[data-copy]'); if (!btn) return;
|
const btn = e.target.closest('[data-copy]'); if (!btn) return;
|
||||||
const orig = btn.textContent;
|
const orig = btn.textContent;
|
||||||
@@ -726,7 +749,7 @@
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
btn.textContent = 'COPIED ✓'; btn.disabled = true;
|
btn.textContent = 'COPIED ✓'; btn.disabled = true;
|
||||||
if (btn.hasAttribute('data-copy-toast')) toast.success('Copied to clipboard');
|
if (btn.hasAttribute('data-copy-toast')) toast.success('Copied to clipboard');
|
||||||
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 1500);
|
setTimeout(() => { if (document.contains(btn)) { btn.textContent = orig; btn.disabled = false; } }, 1500);
|
||||||
} else { toast.error('Copy failed'); }
|
} else { toast.error('Copy failed'); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -750,9 +773,11 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _alertsInitialized = false;
|
||||||
function initAlerts() {
|
function initAlerts() {
|
||||||
|
if (_alertsInitialized) return; _alertsInitialized = true;
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
const btn = e.target.closest('.lt-alert-dismiss'); if (!btn) return;
|
const btn = e.target.closest('.lt-alert-close, .lt-alert-dismiss'); if (!btn) return;
|
||||||
const al = btn.closest('.lt-alert'); if (al) dismissAlert(al);
|
const al = btn.closest('.lt-alert'); if (al) dismissAlert(al);
|
||||||
});
|
});
|
||||||
document.querySelectorAll('.lt-alert[data-alert-auto-dismiss]').forEach(el => {
|
document.querySelectorAll('.lt-alert[data-alert-auto-dismiss]').forEach(el => {
|
||||||
@@ -805,12 +830,13 @@
|
|||||||
|
|
||||||
Command: { id, label, icon?, description?, kbd?, group?, tags?, action }
|
Command: { id, label, icon?, description?, kbd?, group?, tags?, action }
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
let _cpCommands = [], _cpSelected = 0;
|
let _cpCommands = [], _cpSelected = 0, _cpTrigger = null;
|
||||||
const _cpRecentKey = 'lt_cmd_recent';
|
const _cpRecentKey = 'lt_cmd_recent';
|
||||||
|
|
||||||
function _cmdPaletteOpen() {
|
function _cmdPaletteOpen() {
|
||||||
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
|
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
|
||||||
if (_mnOpen) _mnSetOpen(false);
|
if (_mnOpen) _mnSetOpen(false);
|
||||||
|
_cpTrigger = (document.activeElement && document.activeElement !== document.body) ? document.activeElement : null;
|
||||||
ov.classList.add('is-open');
|
ov.classList.add('is-open');
|
||||||
_lockScroll();
|
_lockScroll();
|
||||||
const palette = document.getElementById('lt-cmd-palette');
|
const palette = document.getElementById('lt-cmd-palette');
|
||||||
@@ -823,6 +849,7 @@
|
|||||||
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
|
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
|
||||||
ov.classList.remove('is-open');
|
ov.classList.remove('is-open');
|
||||||
_unlockScroll();
|
_unlockScroll();
|
||||||
|
if (_cpTrigger) { _cpTrigger.focus(); _cpTrigger = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cpHighlight(text, q) {
|
function _cpHighlight(text, q) {
|
||||||
@@ -960,12 +987,22 @@
|
|||||||
function _showError(el, msg) {
|
function _showError(el, msg) {
|
||||||
el.classList.add('is-invalid'); el.classList.remove('is-valid');
|
el.classList.add('is-invalid'); el.classList.remove('is-valid');
|
||||||
let err = el.parentElement && el.parentElement.querySelector('.lt-field-error');
|
let err = el.parentElement && el.parentElement.querySelector('.lt-field-error');
|
||||||
if (!err) { err = document.createElement('span'); err.className = 'lt-field-error'; if (el.parentElement) el.parentElement.appendChild(err); }
|
if (!err) {
|
||||||
|
err = document.createElement('span');
|
||||||
|
err.className = 'lt-field-error';
|
||||||
|
err.id = (el.id || ('lt-field-' + Math.random().toString(36).slice(2))) + '-err';
|
||||||
|
if (el.parentElement) el.parentElement.appendChild(err);
|
||||||
|
}
|
||||||
err.textContent = msg;
|
err.textContent = msg;
|
||||||
|
err.setAttribute('role', 'alert');
|
||||||
|
el.setAttribute('aria-describedby', err.id);
|
||||||
|
el.setAttribute('aria-invalid', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
function _clearError(el) {
|
function _clearError(el) {
|
||||||
el.classList.remove('is-invalid'); el.classList.add('is-valid');
|
el.classList.remove('is-invalid'); el.classList.add('is-valid');
|
||||||
|
el.removeAttribute('aria-invalid');
|
||||||
|
el.removeAttribute('aria-describedby');
|
||||||
const err = el.parentElement && el.parentElement.querySelector('.lt-field-error');
|
const err = el.parentElement && el.parentElement.querySelector('.lt-field-error');
|
||||||
if (err) err.remove();
|
if (err) err.remove();
|
||||||
}
|
}
|
||||||
@@ -990,7 +1027,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const r = _validateForm(formEl);
|
const r = _validateForm(formEl);
|
||||||
if (r.valid && typeof onSubmit === 'function') onSubmit(formEl, e);
|
if (r.valid && typeof onSubmit === 'function') onSubmit(formEl, e);
|
||||||
else if (!r.valid) r.errors[0].el.focus();
|
else if (!r.valid && r.errors.length) r.errors[0].el.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1550,9 +1587,11 @@
|
|||||||
if (triggerEl) _modalTriggers.set(drawer, triggerEl);
|
if (triggerEl) _modalTriggers.set(drawer, triggerEl);
|
||||||
const first = drawer.querySelector(_FOCUSABLE);
|
const first = drawer.querySelector(_FOCUSABLE);
|
||||||
if (first) setTimeout(() => first.focus(), 50);
|
if (first) setTimeout(() => first.focus(), 50);
|
||||||
// ESC to close
|
// ESC to close + Tab trap
|
||||||
drawer._rdKeyHandler = e => { if (e.key === 'Escape') _rdClose(drawer); };
|
drawer._rdKeyHandler = e => { if (e.key === 'Escape') _rdClose(drawer); };
|
||||||
document.addEventListener('keydown', drawer._rdKeyHandler);
|
document.addEventListener('keydown', drawer._rdKeyHandler);
|
||||||
|
drawer._rdTrapHandler = e => { if (e.key === 'Tab') _trapFocus(drawer, e); };
|
||||||
|
drawer.addEventListener('keydown', drawer._rdTrapHandler);
|
||||||
// Overlay click
|
// Overlay click
|
||||||
if (ov) ov._rdClick = () => _rdClose(drawer);
|
if (ov) ov._rdClick = () => _rdClose(drawer);
|
||||||
if (ov) ov.addEventListener('click', ov._rdClick);
|
if (ov) ov.addEventListener('click', ov._rdClick);
|
||||||
@@ -1570,8 +1609,12 @@
|
|||||||
drawer.classList.remove('is-open');
|
drawer.classList.remove('is-open');
|
||||||
drawer.setAttribute('aria-hidden', 'true');
|
drawer.setAttribute('aria-hidden', 'true');
|
||||||
if (ov) { ov.classList.remove('is-open'); if (ov._rdClick) { ov.removeEventListener('click', ov._rdClick); delete ov._rdClick; } }
|
if (ov) { ov.classList.remove('is-open'); if (ov._rdClick) { ov.removeEventListener('click', ov._rdClick); delete ov._rdClick; } }
|
||||||
|
drawer.querySelectorAll('[data-drawer-close]').forEach(btn => {
|
||||||
|
if (btn._rdHandler) { btn.removeEventListener('click', btn._rdHandler); delete btn._rdHandler; }
|
||||||
|
});
|
||||||
_unlockScroll();
|
_unlockScroll();
|
||||||
if (drawer._rdKeyHandler) { document.removeEventListener('keydown', drawer._rdKeyHandler); delete drawer._rdKeyHandler; }
|
if (drawer._rdKeyHandler) { document.removeEventListener('keydown', drawer._rdKeyHandler); delete drawer._rdKeyHandler; }
|
||||||
|
if (drawer._rdTrapHandler) { drawer.removeEventListener('keydown', drawer._rdTrapHandler); delete drawer._rdTrapHandler; }
|
||||||
const trigger = _modalTriggers.get(drawer);
|
const trigger = _modalTriggers.get(drawer);
|
||||||
if (trigger) { trigger.focus(); _modalTriggers.delete(drawer); }
|
if (trigger) { trigger.focus(); _modalTriggers.delete(drawer); }
|
||||||
}
|
}
|
||||||
@@ -1791,6 +1834,15 @@
|
|||||||
let focusedIdx = -1;
|
let focusedIdx = -1;
|
||||||
let filtered = [...options];
|
let filtered = [...options];
|
||||||
|
|
||||||
|
// ARIA combobox wiring
|
||||||
|
const dropId = dropdown.id || ('lt-cb-drop-' + Math.random().toString(36).slice(2));
|
||||||
|
dropdown.id = dropId;
|
||||||
|
dropdown.setAttribute('role', 'listbox');
|
||||||
|
inputEl.setAttribute('role', 'combobox');
|
||||||
|
inputEl.setAttribute('aria-expanded', 'false');
|
||||||
|
inputEl.setAttribute('aria-controls', dropId);
|
||||||
|
inputEl.setAttribute('aria-autocomplete', 'list');
|
||||||
|
|
||||||
function _renderTags() {
|
function _renderTags() {
|
||||||
wrap.querySelectorAll('.lt-combobox-tag').forEach(t => t.remove());
|
wrap.querySelectorAll('.lt-combobox-tag').forEach(t => t.remove());
|
||||||
selected.forEach(v => {
|
selected.forEach(v => {
|
||||||
@@ -1816,7 +1868,8 @@
|
|||||||
el.className = 'lt-combobox-option' + (selected.includes(opt.value) ? ' is-selected' : '');
|
el.className = 'lt-combobox-option' + (selected.includes(opt.value) ? ' is-selected' : '');
|
||||||
el.setAttribute('role', 'option');
|
el.setAttribute('role', 'option');
|
||||||
el.setAttribute('data-value', opt.value);
|
el.setAttribute('data-value', opt.value);
|
||||||
const hl = q ? opt.label.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'), '<mark>$1</mark>') : escHtml(opt.label);
|
const safeLabel = escHtml(opt.label);
|
||||||
|
const hl = q ? safeLabel.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'), '<mark>$1</mark>') : safeLabel;
|
||||||
el.innerHTML = `${opt.icon ? `<span class="icon">${escHtml(opt.icon)}</span>` : ''}<span>${hl}</span>`;
|
el.innerHTML = `${opt.icon ? `<span class="icon">${escHtml(opt.icon)}</span>` : ''}<span>${hl}</span>`;
|
||||||
el.addEventListener('mousedown', e => { e.preventDefault(); _toggle(opt.value); });
|
el.addEventListener('mousedown', e => { e.preventDefault(); _toggle(opt.value); });
|
||||||
dropdown.appendChild(el);
|
dropdown.appendChild(el);
|
||||||
@@ -1843,13 +1896,17 @@
|
|||||||
items[focusedIdx].scrollIntoView({ block: 'nearest' });
|
items[focusedIdx].scrollIntoView({ block: 'nearest' });
|
||||||
}
|
}
|
||||||
|
|
||||||
inputEl.addEventListener('input', () => { dropdown.classList.add('is-open'); _renderDropdown(inputEl.value); });
|
function _setOpen(open) {
|
||||||
inputEl.addEventListener('focus', () => { dropdown.classList.add('is-open'); _renderDropdown(inputEl.value); });
|
dropdown.classList.toggle('is-open', open);
|
||||||
|
inputEl.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
inputEl.addEventListener('input', () => { _setOpen(true); _renderDropdown(inputEl.value); });
|
||||||
|
inputEl.addEventListener('focus', () => { _setOpen(true); _renderDropdown(inputEl.value); });
|
||||||
inputEl.addEventListener('keydown', e => {
|
inputEl.addEventListener('keydown', e => {
|
||||||
if (e.key === 'ArrowDown') { e.preventDefault(); _moveFocus(1); }
|
if (e.key === 'ArrowDown') { e.preventDefault(); _moveFocus(1); }
|
||||||
if (e.key === 'ArrowUp') { e.preventDefault(); _moveFocus(-1); }
|
if (e.key === 'ArrowUp') { e.preventDefault(); _moveFocus(-1); }
|
||||||
if (e.key === 'Enter') { e.preventDefault(); if (focusedIdx >= 0 && filtered[focusedIdx]) _toggle(filtered[focusedIdx].value); }
|
if (e.key === 'Enter') { e.preventDefault(); if (focusedIdx >= 0 && filtered[focusedIdx]) _toggle(filtered[focusedIdx].value); }
|
||||||
if (e.key === 'Escape') { dropdown.classList.remove('is-open'); }
|
if (e.key === 'Escape') { _setOpen(false); }
|
||||||
if (e.key === 'Backspace' && !inputEl.value && selected.length) { _toggle(selected[selected.length - 1]); }
|
if (e.key === 'Backspace' && !inputEl.value && selected.length) { _toggle(selected[selected.length - 1]); }
|
||||||
});
|
});
|
||||||
inputWrap.addEventListener('mousedown', e => {
|
inputWrap.addEventListener('mousedown', e => {
|
||||||
@@ -1857,7 +1914,7 @@
|
|||||||
if (rmBtn) { e.preventDefault(); _toggle(rmBtn.dataset.value); return; }
|
if (rmBtn) { e.preventDefault(); _toggle(rmBtn.dataset.value); return; }
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
});
|
});
|
||||||
document.addEventListener('click', e => { if (!wrap.contains(e.target)) dropdown.classList.remove('is-open'); });
|
document.addEventListener('click', e => { if (!wrap.contains(e.target)) _setOpen(false); });
|
||||||
|
|
||||||
_renderTags();
|
_renderTags();
|
||||||
_renderDropdown('');
|
_renderDropdown('');
|
||||||
@@ -1899,7 +1956,8 @@
|
|||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'lt-typeahead-item';
|
el.className = 'lt-typeahead-item';
|
||||||
el.setAttribute('role', 'option');
|
el.setAttribute('role', 'option');
|
||||||
const hl = item.label.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'), '<mark>$1</mark>');
|
const safeItemLabel = escHtml(item.label);
|
||||||
|
const hl = safeItemLabel.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'), '<mark>$1</mark>');
|
||||||
el.innerHTML = `${item.icon ? `<span class="icon">${escHtml(item.icon)}</span>` : ''}<span>${hl}</span>${item.meta ? `<span style="margin-left:auto;color:var(--text-muted);font-size:0.68rem">${escHtml(item.meta)}</span>` : ''}`;
|
el.innerHTML = `${item.icon ? `<span class="icon">${escHtml(item.icon)}</span>` : ''}<span>${hl}</span>${item.meta ? `<span style="margin-left:auto;color:var(--text-muted);font-size:0.68rem">${escHtml(item.meta)}</span>` : ''}`;
|
||||||
el.addEventListener('mousedown', e => { e.preventDefault(); _select(item); });
|
el.addEventListener('mousedown', e => { e.preventDefault(); _select(item); });
|
||||||
dropdown.appendChild(el);
|
dropdown.appendChild(el);
|
||||||
@@ -2119,8 +2177,9 @@
|
|||||||
const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
|
const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||||
if (dist < threshold) _load();
|
if (dist < threshold) _load();
|
||||||
}
|
}
|
||||||
scrollRoot.addEventListener('scroll', throttle(_onScroll, 150), { passive: true });
|
const _onScrollThrottled = throttle(_onScroll, 150);
|
||||||
return { reset() { _done = false; _loading = false; }, stop() { scrollRoot.removeEventListener('scroll', _onScroll); } };
|
scrollRoot.addEventListener('scroll', _onScrollThrottled, { passive: true });
|
||||||
|
return { reset() { _done = false; _loading = false; }, stop() { scrollRoot.removeEventListener('scroll', _onScrollThrottled); } };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -2175,16 +2234,23 @@
|
|||||||
if (first) setTimeout(() => first.focus(), 60);
|
if (first) setTimeout(() => first.focus(), 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _wizBusy = false;
|
||||||
async function _next() {
|
async function _next() {
|
||||||
if (validate) {
|
if (_wizBusy) return;
|
||||||
const ok = await validate(current + 1, _getStepData(current));
|
_wizBusy = true;
|
||||||
if (!ok) {
|
try {
|
||||||
container.querySelectorAll('[data-wizard-indicator]')[current]?.classList.add('is-error');
|
if (validate) {
|
||||||
return;
|
const ok = await validate(current + 1, _getStepData(current));
|
||||||
|
if (!ok) {
|
||||||
|
container.querySelectorAll('[data-wizard-indicator]')[current]?.classList.add('is-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Object.assign(formData, _getStepData(current));
|
||||||
|
if (current < total - 1) { current++; _show(current); }
|
||||||
|
} finally {
|
||||||
|
_wizBusy = false;
|
||||||
}
|
}
|
||||||
Object.assign(formData, _getStepData(current));
|
|
||||||
if (current < total - 1) { current++; _show(current); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _prev() {
|
function _prev() {
|
||||||
@@ -2671,7 +2737,10 @@
|
|||||||
alerts: bool, clipboard: bool, sidebar: bool, submenus: bool }
|
alerts: bool, clipboard: bool, sidebar: bool, submenus: bool }
|
||||||
Individual modules can still be called manually.
|
Individual modules can still be called manually.
|
||||||
================================================================ */
|
================================================================ */
|
||||||
|
let _ltInitialized = false;
|
||||||
function ltInit(opts) {
|
function ltInit(opts) {
|
||||||
|
if (_ltInitialized) return; // Guard: safe to call multiple times
|
||||||
|
_ltInitialized = true;
|
||||||
const o = Object.assign({
|
const o = Object.assign({
|
||||||
boot: true,
|
boot: true,
|
||||||
bootName: null,
|
bootName: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user