v1.1: Complete remaining 8 feature modules + 7 CSS component sections

JS modules added:
- lt.sidebarSubmenus — nested nav groups with expand/collapse, auto-opens active
- lt.infiniteScroll  — IntersectionObserver-based (scroll fallback), loading indicator
- lt.wizard          — multi-step form with indicators, validation hook, getData()
- lt.sortable        — HTML5 drag-to-reorder lists with placeholder ghost + bus event
- lt.timer           — countdown (urgent threshold + onExpire) + stopwatch (pause/reset)
- lt.lightbox        — full-screen image viewer, prev/next, ESC, caption, loop
- lt.auth            — JWT token management: setToken, refresh (auto + manual),
                       401 retry, onExpire hook, patches lt.api with Bearer header
- lt.markdown        — micro-renderer (no deps); auto-delegates to window.marked /
                       markdownit if present; renders headings/bold/italic/code/
                       links/lists/blockquotes/tables/HR

CSS sections added (69–75):
- Infinite scroll sentinel + loading indicator
- Wizard step indicators (connectors, active/complete/error states, nav footer)
- Sortable item dragging + placeholder ghost
- Countdown/timer display + urgency blink animation
- Image lightbox overlay (close/prev/next controls, caption, counter)
- Sidebar submenu groups (chevron, expand/collapse, active sub-link)
- Markdown output styling (.lt-markdown — all block elements themed)

HTML demos for all 8 new components added and wired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 22:42:16 -04:00
parent 0eb91f1937
commit 6ad4cb2354
3 changed files with 1128 additions and 0 deletions
+227
View File
@@ -1256,6 +1256,191 @@
</div>
</div>
<!-- Wizard / Multi-Step Form -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// WIZARD / MULTI-STEP FORM</span>
</div>
<div class="lt-section-body">
<div id="demo-wizard">
<!-- Step indicators -->
<div class="lt-wizard-steps">
<div class="lt-wizard-step is-active" data-wizard-indicator>
<div class="lt-wizard-num">1</div>
<div class="lt-wizard-label">Details</div>
</div>
<div class="lt-wizard-step" data-wizard-indicator>
<div class="lt-wizard-num">2</div>
<div class="lt-wizard-label">Assign</div>
</div>
<div class="lt-wizard-step" data-wizard-indicator>
<div class="lt-wizard-num">3</div>
<div class="lt-wizard-label">Review</div>
</div>
</div>
<!-- Counter -->
<p class="lt-wizard-counter">Step <strong data-wizard-current>1</strong> of <strong data-wizard-total>3</strong></p>
<!-- Steps -->
<div data-wizard-step="1" class="is-active" aria-hidden="false">
<div class="lt-grid lt-grid-2" style="gap:1rem;margin-top:1rem">
<div class="lt-form-group"><label class="lt-label">Ticket Title</label><input type="text" name="title" class="lt-input lt-w-full" placeholder="Brief description…"></div>
<div class="lt-form-group"><label class="lt-label">Priority</label><select name="priority" class="lt-select lt-w-full"><option value="p1">P1 Critical</option><option value="p2">P2 High</option><option value="p3" selected>P3 Medium</option><option value="p4">P4 Low</option></select></div>
</div>
</div>
<div data-wizard-step="2" aria-hidden="true">
<div class="lt-form-group" style="margin-top:1rem"><label class="lt-label">Assign To</label><input type="text" name="assignee" class="lt-input lt-w-full" placeholder="Username…"></div>
<div class="lt-form-group"><label class="lt-label">Due Date</label><input type="date" name="due" class="lt-input"></div>
</div>
<div data-wizard-step="3" aria-hidden="true">
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon"></div><div class="lt-empty-state-title">Review &amp; Submit</div><div class="lt-empty-state-body">Check the details above and click Submit when ready.</div></div>
</div>
<!-- Nav -->
<div class="lt-wizard-nav">
<button class="lt-btn lt-btn-sm" data-wizard-prev disabled>← Back</button>
<button class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-next>Next →</button>
<button class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-done style="display:none">Submit ✓</button>
</div>
</div>
</div>
</div>
<!-- Sortable List -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// DRAG-TO-REORDER (SORTABLE)</span>
</div>
<div class="lt-section-body">
<div class="lt-grid lt-grid-2" style="gap:1.5rem">
<div>
<p style="font-size:0.72rem;color:var(--text-muted);margin-bottom:0.75rem">Drag items to reorder. Uses native HTML5 drag API with pointer-events fallback.</p>
<ul id="demo-sortable" style="list-style:none;padding:0;display:flex;flex-direction:column;gap:4px">
<li data-id="p1" style="padding:0.5rem 0.75rem;background:var(--bg-card);border:1px solid var(--border-dim);display:flex;align-items:center;gap:0.5rem"><span style="color:var(--text-muted);cursor:grab"></span> P1 — Storage link-down</li>
<li data-id="p2" style="padding:0.5rem 0.75rem;background:var(--bg-card);border:1px solid var(--border-dim);display:flex;align-items:center;gap:0.5rem"><span style="color:var(--text-muted);cursor:grab"></span> P2 — Switch port flapping</li>
<li data-id="p3" style="padding:0.5rem 0.75rem;background:var(--bg-card);border:1px solid var(--border-dim);display:flex;align-items:center;gap:0.5rem"><span style="color:var(--text-muted);cursor:grab"></span> P3 — SFP+ replacement</li>
<li data-id="p4" style="padding:0.5rem 0.75rem;background:var(--bg-card);border:1px solid var(--border-dim);display:flex;align-items:center;gap:0.5rem"><span style="color:var(--text-muted);cursor:grab"></span> P4 — SSL cert renewal</li>
</ul>
<p id="demo-sort-order" style="font-size:0.68rem;color:var(--text-muted);margin-top:0.5rem">Order: p1, p2, p3, p4</p>
</div>
<div>
<p style="font-size:0.72rem;color:var(--text-muted);margin-bottom:0.75rem">API:</p>
<pre class="lt-code-block" style="font-size:0.7rem">const s = lt.sortable.init(listEl, {
onSort: (items) => console.log(s.getOrder())
});</pre>
</div>
</div>
</div>
</div>
<!-- Countdown / Timer -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// COUNTDOWN &amp; TIMER</span>
</div>
<div class="lt-section-body">
<div class="lt-grid lt-grid-3" style="gap:1rem">
<div class="lt-stat-card">
<div class="lt-stat-label">SLA Countdown</div>
<div class="lt-countdown lt-num" id="demo-countdown">--:--:--</div>
<div style="font-size:0.68rem;color:var(--text-muted);margin-top:0.25rem">Goes urgent (red) at &lt;5 min</div>
</div>
<div class="lt-stat-card">
<div class="lt-stat-label">Stopwatch</div>
<div class="lt-countdown lt-num" id="demo-stopwatch">00:00:00</div>
<div class="lt-flex lt-gap-xs" style="margin-top:0.5rem">
<button class="lt-btn lt-btn-sm" id="sw-pause">Pause</button>
<button class="lt-btn lt-btn-sm" id="sw-reset">Reset</button>
</div>
</div>
<div class="lt-stat-card">
<div class="lt-stat-label">API</div>
<pre class="lt-code-block" style="font-size:0.62rem;margin:0">lt.timer.countdown(el, date, {
urgent: 300,
onExpire: () => {}
});
lt.timer.stopwatch(el);</pre>
</div>
</div>
</div>
</div>
<!-- Image Lightbox -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// IMAGE LIGHTBOX</span>
</div>
<div class="lt-section-body">
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:1rem">Click any image to open full-screen viewer. Keyboard: ←/→ navigate, Esc closes.</p>
<div class="lt-flex lt-gap-md lt-wrap">
<img class="lt-lightbox-demo" src="https://picsum.photos/seed/lt1/320/180" alt="Server rack overview" style="width:160px;height:90px;object-fit:cover;border:1px solid var(--border-dim)" loading="lazy">
<img class="lt-lightbox-demo" src="https://picsum.photos/seed/lt2/320/180" alt="Network switch closeup" style="width:160px;height:90px;object-fit:cover;border:1px solid var(--border-dim)" loading="lazy">
<img class="lt-lightbox-demo" src="https://picsum.photos/seed/lt3/320/180" alt="Datacenter floor view" style="width:160px;height:90px;object-fit:cover;border:1px solid var(--border-dim)" loading="lazy">
</div>
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.5rem"><code>lt.lightbox.init('.lt-lightbox-demo', { caption: 'alt', loop: true })</code></p>
</div>
</div>
<!-- Sidebar Submenus -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// SIDEBAR SUBMENUS</span>
</div>
<div class="lt-section-body">
<div style="width:220px;background:var(--bg-secondary);border:1px solid var(--border-dim);padding:0.5rem">
<a href="#" class="lt-sidebar-link active" style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.75rem;font-size:0.78rem;color:var(--accent-orange);text-decoration:none">⊞ Dashboard</a>
<a href="#" class="lt-sidebar-link" style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.75rem;font-size:0.78rem;color:var(--text-secondary);text-decoration:none">🎫 Tickets</a>
<div class="lt-sidebar-group is-open">
<div class="lt-sidebar-group-label">Admin <span class="chevron"></span></div>
<div class="lt-sidebar-submenu">
<a href="#" class="lt-sidebar-sub-link active" aria-current="page">⚙ Templates</a>
<a href="#" class="lt-sidebar-sub-link">🔀 Workflow</a>
<a href="#" class="lt-sidebar-sub-link">📋 Audit Log</a>
<a href="#" class="lt-sidebar-sub-link">🔑 API Keys</a>
</div>
</div>
<div class="lt-sidebar-group">
<div class="lt-sidebar-group-label">Reports <span class="chevron"></span></div>
<div class="lt-sidebar-submenu">
<a href="#" class="lt-sidebar-sub-link">📊 SLA Report</a>
<a href="#" class="lt-sidebar-sub-link">📈 Volume Trends</a>
</div>
</div>
</div>
</div>
</div>
<!-- Markdown Renderer -->
<div class="lt-section">
<div class="lt-section-header">
<span class="lt-section-title">// MARKDOWN RENDERER</span>
</div>
<div class="lt-section-body">
<div class="lt-grid lt-grid-2" style="gap:1.5rem">
<div>
<div class="lt-section-label" style="margin-bottom:0.5rem">Source</div>
<pre class="lt-code-block" style="font-size:0.7rem"># Incident Report
**Severity**: P1 Critical
## Summary
Storage array link-down on `compute-storage-01`.
- Affected: 3 production services
- Duration: 2h 33m
- Root cause: NIC hardware failure
> Resolved. Monitoring continues.
[View Ticket](#001)</pre>
</div>
<div>
<div class="lt-section-label" style="margin-bottom:0.5rem">Rendered</div>
<div id="demo-markdown" class="lt-markdown" style="background:var(--bg-card);border:1px solid var(--border-dim);padding:1rem" data-markdown="# Incident Report&#10;**Severity**: P1 Critical&#10;&#10;## Summary&#10;Storage array link-down on &#96;compute-storage-01&#96;.&#10;&#10;- Affected: 3 production services&#10;- Duration: 2h 33m&#10;- Root cause: NIC hardware failure&#10;&#10;&gt; Resolved. Monitoring continues.&#10;&#10;[View Ticket](#001)"></div>
</div>
</div>
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.75rem">Built-in micro-renderer (no deps). Drops in <code>window.marked</code> or <code>window.markdownit</code> automatically if present.</p>
</div>
</div>
</main><!-- /.lt-main -->
@@ -1490,6 +1675,48 @@
// Demo notification badge initial count
lt.notif.set('#lt-notif-bell', 3);
// Wizard demo
lt.wizard.init(document.getElementById('demo-wizard'), {
onComplete: data => lt.toast.success('Ticket submitted: ' + (data.title || 'untitled')),
validate: (step, data) => {
if (step === 1 && !data.title?.trim()) { lt.toast.error('Title is required'); return false; }
return true;
},
});
// Sortable demo
const sortableList = lt.sortable.init(document.getElementById('demo-sortable'), {
onSort: (items) => {
document.getElementById('demo-sort-order').textContent = 'Order: ' + items.map(el => el.dataset.id).join(', ');
},
});
// Countdown demo — SLA expires 2 hours from now
const slaTarget = new Date(Date.now() + 2 * 60 * 60 * 1000);
lt.timer.countdown(document.getElementById('demo-countdown'), slaTarget, {
urgent: 300,
urgentClass: 'lt-text-red lt-countdown-urgent',
onExpire: () => lt.toast.error('SLA BREACHED'),
});
// Stopwatch demo
const sw = lt.timer.stopwatch(document.getElementById('demo-stopwatch'));
let swRunning = true;
document.getElementById('sw-pause').addEventListener('click', function() {
if (swRunning) { sw.pause(); this.textContent = 'Resume'; } else { sw.resume(); this.textContent = 'Pause'; }
swRunning = !swRunning;
});
document.getElementById('sw-reset').addEventListener('click', () => { sw.reset(); swRunning = true; document.getElementById('sw-pause').textContent = 'Pause'; });
// Lightbox demo
lt.lightbox.init('.lt-lightbox-demo');
// Markdown demo
lt.markdown.init('#demo-markdown');
// Sidebar submenus (re-init for demo sidebar)
lt.sidebarSubmenus.init(document.querySelector('.lt-section-body'));
// Tab bar switching
document.querySelectorAll('.lt-tab-bar').forEach(bar => {
bar.addEventListener('click', e => {