80011e6de5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
955 lines
29 KiB
Markdown
955 lines
29 KiB
Markdown
# LotusGuild Terminal Design System — v1.2
|
||
|
||
**Aesthetic:** Anduril × Hacker Terminal — dark military-tech, multi-accent neon, angular clip-path frames, glitch effects.
|
||
|
||
A single-file design system (`base.css` + `base.js`) used across all LotusGuild internal services. `base.html` is a living reference template that demonstrates every component and pattern.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Overview](#overview)
|
||
2. [Quick Start](#quick-start)
|
||
3. [Design Tokens](#design-tokens)
|
||
4. [Breakpoints](#breakpoints)
|
||
5. [Typography](#typography)
|
||
6. [Color Palette](#color-palette)
|
||
7. [Component Catalog](#component-catalog)
|
||
8. [JavaScript API](#javascript-api)
|
||
9. [Theming (Dark / Light)](#theming)
|
||
10. [Accessibility](#accessibility)
|
||
11. [File Structure](#file-structure)
|
||
12. [Changelog](#changelog)
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `base.css` | All styles — ~5,200 lines, 79 sections |
|
||
| `base.js` | All JS — vanilla IIFE, `window.lt` namespace, 55+ modules |
|
||
| `base.html` | Full component reference / demo page |
|
||
|
||
No build step. No npm. No framework. Drop in the two files and go.
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html lang="en" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<meta name="theme-color" content="#030508">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/base.css">
|
||
</head>
|
||
<body>
|
||
<!-- your app markup here -->
|
||
<script src="/base.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
lt.init({ bootName: 'MY APP' });
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
### `lt.init()` — master initializer
|
||
|
||
`lt.init(opts?)` runs all standard auto-init modules in one call. Safe to call multiple times — a guard prevents double-initialization.
|
||
|
||
```js
|
||
lt.init({
|
||
bootName: 'MY APP', // shown in CRT boot overlay (optional)
|
||
skipBoot: false, // set true to skip the boot sequence
|
||
csrf: 'token', // CSRF token string (optional; overrides meta-tag detection)
|
||
});
|
||
```
|
||
|
||
Modules initialized automatically: `tabs`, `accordion`, `tooltip`, `clipboard.initCopyButtons`, `alerts`, `sidebar`, `keys.initDefaults`, `mobileNav`.
|
||
|
||
---
|
||
|
||
## Design Tokens
|
||
|
||
All tokens are CSS custom properties on `:root`. Override any token in your app stylesheet **after** importing `base.css`.
|
||
|
||
### Backgrounds
|
||
|
||
| Token | Dark | Light |
|
||
|-------|------|-------|
|
||
| `--bg-primary` | `#030508` | `#edf0f5` |
|
||
| `--bg-secondary` | `#060c14` | `#e2e8f0` |
|
||
| `--bg-tertiary` | `#0d1520` | `#cdd5e0` |
|
||
| `--bg-card` | `#07101a` | `#f5f7fb` |
|
||
| `--bg-terminal` | `#010304` | `#d8dfe8` |
|
||
| `--bg-overlay` | `rgba(3,5,8,0.94)` | `rgba(237,240,245,0.96)` |
|
||
|
||
### Text
|
||
|
||
| Token | Dark | Light |
|
||
|-------|------|-------|
|
||
| `--text-primary` | `#e8edf5` | `#1a2233` |
|
||
| `--text-secondary` | `#8899b0` | `#3a4a60` |
|
||
| `--text-muted` | `#4a5a70` | `#6a7a90` |
|
||
| `--text-accent` | `#FF6B00` | `#c44e00` |
|
||
|
||
### Borders
|
||
|
||
| Token | Dark | Light |
|
||
|-------|------|-------|
|
||
| `--border-color` | `rgba(255,107,0,0.25)` | `rgba(130,140,160,0.5)` |
|
||
| `--border-dim` | `rgba(255,107,0,0.10)` | `rgba(130,140,160,0.25)` |
|
||
| `--border-bright` | `rgba(255,107,0,0.6)` | `rgba(196,78,0,0.5)` |
|
||
|
||
### Spacing
|
||
|
||
`--space-xs` (0.25rem) · `--space-sm` (0.5rem) · `--space-md` (1rem) · `--space-lg` (1.5rem) · `--space-xl` (2rem) · `--space-2xl` (3rem)
|
||
|
||
### Layout
|
||
|
||
| Token | Value |
|
||
|-------|-------|
|
||
| `--sidebar-width` | `240px` |
|
||
| `--header-height` | `56px` |
|
||
| `--container-max` | `1600px` |
|
||
|
||
### Transitions
|
||
|
||
| Token | Value |
|
||
|-------|-------|
|
||
| `--transition-fast` | `all 0.12s ease` |
|
||
| `--transition-default` | `all 0.25s ease` |
|
||
|
||
### Corner Cuts
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| `--corner-cut` | `8px` | Standard clip-path notch |
|
||
| `--corner-cut-sm` | `5px` | Small elements |
|
||
| `--corner-cut-lg` | `16px` | Large modals / hero cards |
|
||
|
||
### Z-Index Ladder
|
||
|
||
| Token | Value | Layer |
|
||
|-------|-------|-------|
|
||
| `--z-base` | `1` | Stacking context base |
|
||
| `--z-dropdown` | `100` | Dropdowns, autocomplete |
|
||
| `--z-sticky` | `200` | Sticky elements |
|
||
| `--z-fixed` | `300` | Fixed header/sidebar |
|
||
| `--z-overlay` | `9999` | CRT scanlines / boot overlay |
|
||
| `--z-modal-backdrop` | `10010` | Modal scrim |
|
||
| `--z-modal` | `10011` | Modal dialogs |
|
||
| `--z-popover` | `10012` | Popovers |
|
||
| `--z-tooltip` | `10013` | Tooltips |
|
||
| `--z-toast` | `10014` | Toast notifications |
|
||
| `--z-panel` | `10020` | Notification / dropdown panels |
|
||
|
||
---
|
||
|
||
## Breakpoints
|
||
|
||
Mobile-first. 8 tiers:
|
||
|
||
| Name | Range | CSS min-width |
|
||
|------|-------|---------------|
|
||
| `xs` | ≤ 479px | — (base) |
|
||
| `sm` | 480–767px | `480px` |
|
||
| `md` | 768–1023px | `768px` |
|
||
| `lg` | 1024–1279px | `1024px` |
|
||
| `xl` | 1280–1535px | `1280px` |
|
||
| `2xl` | 1536–1919px | `1536px` |
|
||
| `3xl` | 1920–2559px | `1920px` |
|
||
| `4k` | ≥ 2560px | `2560px` |
|
||
|
||
Check breakpoints in JavaScript:
|
||
|
||
```js
|
||
lt.viewport.bp() // → 'xs' | 'sm' | 'md' | ... | '4k'
|
||
lt.viewport.is('md') // → true if current bp >= md
|
||
lt.viewport.on(cb) // subscribe to resize/bp changes
|
||
lt.viewport.touch() // true if primary pointer is coarse
|
||
lt.viewport.landscape()
|
||
```
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
**Font:** JetBrains Mono (Google Fonts) — weights 400, 600, 700; italic 400.
|
||
|
||
Load via:
|
||
```html
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
||
```
|
||
|
||
| Token | Value |
|
||
|-------|-------|
|
||
| `--font-mono` | `'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace` |
|
||
| `--font-size-base` | `0.875rem` (14px) |
|
||
| `--line-height` | `1.6` |
|
||
|
||
Heading classes: `.lt-h1` through `.lt-h6`. Utility: `.lt-text-xs`, `.lt-text-sm`, `.lt-text-lg`, `.lt-text-xl`, `.lt-text-muted`, `.lt-text-accent`, `.lt-font-mono`, `.lt-font-bold`.
|
||
|
||
---
|
||
|
||
## Color Palette
|
||
|
||
### Dark Mode (default)
|
||
|
||
| Name | Hex | Variable |
|
||
|------|-----|----------|
|
||
| Orange (primary) | `#FF6B00` | `--accent-orange` |
|
||
| Cyan | `#00D4FF` | `--accent-cyan` |
|
||
| Green | `#00FF88` | `--accent-green` |
|
||
| Red | `#FF2D55` | `--accent-red` |
|
||
| Purple | `#BF5FFF` | `--accent-purple` |
|
||
| Amber | `#FFB300` | `--accent-amber` |
|
||
|
||
### Light Mode
|
||
|
||
All accents are desaturated for readability on light backgrounds:
|
||
|
||
| Name | Dark | Light |
|
||
|------|------|-------|
|
||
| Orange | `#FF6B00` | `#c44e00` |
|
||
| Cyan | `#00D4FF` | `#0062b8` |
|
||
| Green | `#00FF88` | `#006d35` |
|
||
| Red | `#FF2D55` | `#b8001f` |
|
||
| Purple | `#BF5FFF` | `#7c22cc` |
|
||
|
||
Glows in dark mode use `box-shadow` with color spread. Light mode replaces neon glows with drop-shadow rings (e.g., `0 0 0 1px rgba(196,78,0,0.25), 0 1px 6px rgba(196,78,0,0.18)`).
|
||
|
||
---
|
||
|
||
## Component Catalog
|
||
|
||
All components use the `.lt-` prefix.
|
||
|
||
### Layout
|
||
|
||
```html
|
||
<!-- App shell -->
|
||
<div class="lt-app-shell">
|
||
<header class="lt-header">…</header>
|
||
<aside class="lt-sidebar">…</aside>
|
||
<main class="lt-main lt-container">…</main>
|
||
<footer class="lt-footer">…</footer>
|
||
</div>
|
||
|
||
<!-- Grid system -->
|
||
<div class="lt-grid lt-grid-2">…</div> <!-- 2-col -->
|
||
<div class="lt-grid lt-grid-3">…</div> <!-- 3-col -->
|
||
<div class="lt-grid lt-grid-4">…</div> <!-- 4-col -->
|
||
<div class="lt-grid lt-grid-auto">…</div> <!-- auto-fit, min 280px -->
|
||
<div class="lt-stats-grid">…</div> <!-- auto-fit, min 200px -->
|
||
```
|
||
|
||
### ASCII Frame System
|
||
|
||
The signature angled clip-path card border:
|
||
|
||
```html
|
||
<div class="lt-frame">…</div>
|
||
<div class="lt-frame lt-frame--cyan">…</div>
|
||
<div class="lt-frame lt-frame--green">…</div>
|
||
|
||
<!-- With corner label -->
|
||
<div class="lt-frame">
|
||
<div class="lt-frame-label">SECTION HEADER</div>
|
||
…content…
|
||
</div>
|
||
```
|
||
|
||
### Cards
|
||
|
||
```html
|
||
<div class="lt-card">
|
||
<div class="lt-card-header">…</div>
|
||
<div class="lt-card-body">…</div>
|
||
<div class="lt-card-footer">…</div>
|
||
</div>
|
||
<div class="lt-card lt-card--orange">…</div>
|
||
<div class="lt-card lt-card--cyan">…</div>
|
||
```
|
||
|
||
### Buttons
|
||
|
||
```html
|
||
<button type="button" class="lt-btn lt-btn-primary">Action</button>
|
||
<button type="button" class="lt-btn lt-btn-secondary">Cancel</button>
|
||
<button type="button" class="lt-btn lt-btn-danger">Delete</button>
|
||
<button type="button" class="lt-btn lt-btn-ghost">Ghost</button>
|
||
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm">Small</button>
|
||
<button type="button" class="lt-btn lt-btn-primary lt-btn-lg">Large</button>
|
||
<button type="button" class="lt-btn lt-btn-primary" disabled>Disabled</button>
|
||
|
||
<!-- Icon button -->
|
||
<button type="button" class="lt-icon-btn" aria-label="Settings">⚙</button>
|
||
|
||
<!-- With spinner -->
|
||
<button type="button" class="lt-btn lt-btn-primary lt-btn-loading">
|
||
<span class="lt-spinner lt-spinner--sm"></span> Loading…
|
||
</button>
|
||
```
|
||
|
||
### Forms & Inputs
|
||
|
||
```html
|
||
<div class="lt-form-group">
|
||
<label class="lt-label" for="field">Field Label</label>
|
||
<input type="text" class="lt-input" id="field" autocomplete="off">
|
||
<span class="lt-field-hint">Helper text</span>
|
||
</div>
|
||
|
||
<select class="lt-select">…</select>
|
||
<textarea class="lt-textarea"></textarea>
|
||
|
||
<!-- Search -->
|
||
<div class="lt-search">
|
||
<input type="search" class="lt-input lt-search-input" autocomplete="off">
|
||
</div>
|
||
|
||
<!-- Toggle switch -->
|
||
<label class="lt-toggle">
|
||
<input type="checkbox" class="lt-toggle-input">
|
||
<span class="lt-toggle-slider"></span>
|
||
</label>
|
||
|
||
<!-- Range slider -->
|
||
<input type="range" class="lt-range">
|
||
|
||
<!-- File dropzone -->
|
||
<div class="lt-dropzone" id="my-dropzone">
|
||
<span>Drop files here or click to browse</span>
|
||
</div>
|
||
```
|
||
|
||
JS: `lt.dropzone.init(el, { onFiles(files){} })`
|
||
|
||
Form validation:
|
||
```js
|
||
lt.validate.init(formEl, ({ data, errors }) => {
|
||
// called on submit when valid
|
||
});
|
||
// Custom validator
|
||
lt.validate.custom['my-rule'] = (val) => val.length > 3 ? null : 'Too short';
|
||
```
|
||
|
||
### Status Badges
|
||
|
||
```html
|
||
<span class="lt-badge lt-badge-open">Open</span>
|
||
<span class="lt-badge lt-badge-closed">Closed</span>
|
||
<span class="lt-badge lt-badge-progress">In Progress</span>
|
||
<span class="lt-badge lt-badge-critical">Critical</span>
|
||
<span class="lt-badge lt-badge-high">High</span>
|
||
<span class="lt-badge lt-badge-medium">Medium</span>
|
||
<span class="lt-badge lt-badge-low">Low</span>
|
||
|
||
<!-- Notification dot -->
|
||
<span class="lt-notif-badge" data-count="3"></span>
|
||
```
|
||
|
||
JS: `lt.notif.set(el, n)` / `lt.notif.inc(el)` / `lt.notif.clear(el)`
|
||
|
||
### Tables
|
||
|
||
```html
|
||
<div class="lt-table-wrap">
|
||
<table class="lt-table" id="my-table">
|
||
<caption class="lt-sr-only">Accessible caption</caption>
|
||
<thead>
|
||
<tr>
|
||
<th><input type="checkbox" class="lt-checkbox lt-select-all"></th>
|
||
<th data-sort="name">Name ↕</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>…</tbody>
|
||
</table>
|
||
</div>
|
||
```
|
||
|
||
JS:
|
||
```js
|
||
lt.tableNav.init('my-table'); // keyboard nav (arrow keys, enter)
|
||
lt.sortTable.init('my-table'); // click-to-sort columns
|
||
lt.tableColumns.init('my-table'); // show/hide columns
|
||
```
|
||
|
||
Responsive: collapses to card layout below 767px.
|
||
|
||
### Modals
|
||
|
||
```html
|
||
<button type="button" class="lt-btn lt-btn-primary" data-modal-open="my-modal">Open</button>
|
||
|
||
<div class="lt-modal-backdrop" id="my-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="my-modal-title" aria-hidden="true">
|
||
<div class="lt-modal">
|
||
<div class="lt-modal-header">
|
||
<h2 class="lt-modal-title" id="my-modal-title">Dialog Title</h2>
|
||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||
</div>
|
||
<div class="lt-modal-body">…</div>
|
||
<div class="lt-modal-footer">
|
||
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
|
||
<button type="button" class="lt-btn lt-btn-primary">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
JS: `lt.modal.open('my-modal')` / `lt.modal.close('my-modal')` / `lt.modal.closeAll()`
|
||
|
||
### Toasts
|
||
|
||
```js
|
||
lt.toast.success('Saved!');
|
||
lt.toast.error('Something went wrong', 8000);
|
||
lt.toast.warning('Disk space low');
|
||
lt.toast.info('Deployment queued', 5000);
|
||
```
|
||
|
||
Toast queue is capped at 12 pending notifications. All types support an optional `duration` in ms (default: 4000).
|
||
|
||
### Drawers (Side Panels)
|
||
|
||
```html
|
||
<div class="lt-drawer" id="my-drawer" role="dialog" aria-hidden="true">
|
||
<div class="lt-drawer-header">
|
||
<h3>Drawer Title</h3>
|
||
<button type="button" class="lt-drawer-close" aria-label="Close">✕</button>
|
||
</div>
|
||
<div class="lt-drawer-body">…</div>
|
||
</div>
|
||
<div class="lt-overlay" id="my-overlay"></div>
|
||
```
|
||
|
||
### Tabs
|
||
|
||
```html
|
||
<div class="lt-tabs" role="tablist">
|
||
<button type="button" class="lt-tab active" data-tab="tab-one"
|
||
role="tab" aria-selected="true" aria-controls="tab-one">Tab 1</button>
|
||
<button type="button" class="lt-tab" data-tab="tab-two"
|
||
role="tab" aria-selected="false" aria-controls="tab-two">Tab 2</button>
|
||
</div>
|
||
<div class="lt-tab-panels">
|
||
<div id="tab-one" class="lt-tab-panel active" role="tabpanel">…</div>
|
||
<div id="tab-two" class="lt-tab-panel" role="tabpanel">…</div>
|
||
</div>
|
||
```
|
||
|
||
JS: `lt.tabs.init()` — auto-wires all `.lt-tabs` on page. `lt.tabs.switch('tab-two')` — programmatic switch.
|
||
|
||
### Accordion
|
||
|
||
```html
|
||
<div class="lt-accordion">
|
||
<button type="button" class="lt-accordion-trigger" aria-expanded="false">
|
||
Section Title
|
||
</button>
|
||
<div class="lt-accordion-content">…</div>
|
||
</div>
|
||
```
|
||
|
||
JS: `lt.accordion.init()` (called by `lt.init()`)
|
||
|
||
### Tooltips
|
||
|
||
```html
|
||
<span class="lt-tooltip-anchor" data-tooltip="Tooltip text">Hover me</span>
|
||
```
|
||
|
||
JS: `lt.tooltip.init()` (called by `lt.init()`). Custom placement via `data-tooltip-pos="top|bottom|left|right"`.
|
||
|
||
### Notifications Bell
|
||
|
||
```html
|
||
<div class="lt-notif-wrap" style="position:relative">
|
||
<button type="button" class="lt-notif-bell-btn" id="notif-bell"
|
||
aria-label="Notifications" aria-expanded="false" aria-haspopup="true">
|
||
🔔
|
||
<span class="lt-notif-badge" id="notif-count" data-count="3"></span>
|
||
</button>
|
||
<div class="lt-notif-panel" id="notif-panel" aria-hidden="true">
|
||
<div class="lt-notif-panel-header">
|
||
<span>Notifications</span>
|
||
<button type="button" class="lt-notif-panel-clear" id="notif-mark-all">Mark all read</button>
|
||
</div>
|
||
<ul class="lt-notif-panel-list" id="notif-list">
|
||
<li class="lt-notif-item lt-notif-item--unread" data-notif-id="1">…</li>
|
||
</ul>
|
||
<div class="lt-notif-panel-footer">
|
||
<a href="#" class="lt-link">View all notifications</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### Dropdown Widget
|
||
|
||
Generic dropdown attached to any trigger button:
|
||
|
||
```html
|
||
<div class="lt-dropdown-wrap" style="position:relative">
|
||
<button type="button" class="lt-btn lt-btn-secondary lt-dropdown-trigger"
|
||
aria-haspopup="true" aria-expanded="false">
|
||
Actions ▾
|
||
</button>
|
||
<div class="lt-dropdown-panel lt-dropdown-panel--right" aria-hidden="true">
|
||
<button type="button" class="lt-dropdown-item">Option 1</button>
|
||
<button type="button" class="lt-dropdown-item">Option 2</button>
|
||
<div class="lt-dropdown-divider"></div>
|
||
<button type="button" class="lt-dropdown-item lt-dropdown-item--danger">Delete</button>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
Closes on outside click and Escape (wired in DOMContentLoaded).
|
||
|
||
### Combobox (Searchable Select)
|
||
|
||
```html
|
||
<div class="lt-combobox" id="my-combobox">
|
||
<input type="text" class="lt-combobox-input" id="my-combobox-input"
|
||
placeholder="Search…" autocomplete="off" role="combobox"
|
||
aria-expanded="false" aria-autocomplete="list"
|
||
aria-controls="my-combobox-list">
|
||
<ul class="lt-combobox-list" id="my-combobox-list" role="listbox" aria-hidden="true"></ul>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
lt.combobox.init(inputEl, [
|
||
{ value: '1', label: 'Alice' },
|
||
{ value: '2', label: 'Bob' },
|
||
], {
|
||
onSelect(opt) { console.log(opt.value); },
|
||
});
|
||
```
|
||
|
||
Labels are HTML-escaped before highlight mark insertion (XSS-safe).
|
||
|
||
### Typeahead
|
||
|
||
```html
|
||
<input type="text" class="lt-input" id="my-typeahead"
|
||
placeholder="Type to search…" autocomplete="off">
|
||
```
|
||
|
||
```js
|
||
lt.typeahead.init(inputEl, async (query) => {
|
||
const res = await lt.api.get('/api/search?q=' + encodeURIComponent(query));
|
||
return res.items.map(i => ({ value: i.id, label: i.name }));
|
||
}, {
|
||
onSelect(item) { console.log(item); },
|
||
minChars: 2,
|
||
debounce: 200,
|
||
});
|
||
```
|
||
|
||
### Wizard / Stepper
|
||
|
||
```html
|
||
<div class="lt-wizard" id="my-wizard">
|
||
<div class="lt-wizard-steps">
|
||
<div class="lt-wizard-step active" data-step="0">Step 1</div>
|
||
<div class="lt-wizard-step" data-step="1">Step 2</div>
|
||
<div class="lt-wizard-step" data-step="2">Step 3</div>
|
||
</div>
|
||
<div class="lt-wizard-panels">
|
||
<div class="lt-wizard-panel active">…Panel 1…</div>
|
||
<div class="lt-wizard-panel">…Panel 2…</div>
|
||
<div class="lt-wizard-panel">…Panel 3…</div>
|
||
</div>
|
||
<div class="lt-wizard-nav">
|
||
<button type="button" class="lt-btn lt-btn-secondary lt-wizard-prev">Back</button>
|
||
<button type="button" class="lt-btn lt-btn-primary lt-wizard-next">Next</button>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
const wiz = lt.wizard.init(document.getElementById('my-wizard'), {
|
||
onComplete() { lt.toast.success('Done!'); },
|
||
});
|
||
wiz.goTo(1); wiz.next(); wiz.prev();
|
||
```
|
||
|
||
### Kanban / Sortable
|
||
|
||
```html
|
||
<div class="lt-grid lt-grid-4">
|
||
<div class="lt-card" id="kanban-col-open">
|
||
<div class="lt-card-header">Open</div>
|
||
<div class="lt-sortable-list" id="kanban-list-open">
|
||
<div class="lt-sortable-item">Card 1</div>
|
||
</div>
|
||
</div>
|
||
<!-- repeat for other columns -->
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
['open','progress','review','done'].forEach(col => {
|
||
lt.sortable.init(document.getElementById('kanban-list-' + col), {
|
||
group: 'kanban', // same group = cross-column drag allowed
|
||
handle: '.lt-drag-handle',
|
||
onSort(listEl) { /* save new order */ },
|
||
});
|
||
});
|
||
```
|
||
|
||
### Pagination
|
||
|
||
```html
|
||
<nav class="lt-pagination" id="demo-pagination" aria-label="Page navigation"></nav>
|
||
```
|
||
|
||
```js
|
||
lt.pagination.init(document.getElementById('demo-pagination'), {
|
||
total: 100,
|
||
pageSize: 10,
|
||
current: 1,
|
||
onChange(page) { loadPage(page); },
|
||
});
|
||
```
|
||
|
||
### Split Pane
|
||
|
||
```html
|
||
<div class="lt-split-pane" id="my-split">
|
||
<div class="lt-split-top">…top panel…</div>
|
||
<div class="lt-split-divider" aria-label="Resize"
|
||
role="separator" aria-orientation="horizontal"></div>
|
||
<div class="lt-split-bottom">…bottom panel…</div>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
lt.splitPane.init(document.getElementById('my-split'), { minTop: 80, minBottom: 80 });
|
||
```
|
||
|
||
### Lightbox
|
||
|
||
```html
|
||
<img src="thumb.jpg" data-lightbox="path/to/full.jpg"
|
||
data-lightbox-caption="Caption" alt="…">
|
||
```
|
||
|
||
```js
|
||
lt.lightbox.init('[data-lightbox]');
|
||
```
|
||
|
||
### Context Menu
|
||
|
||
```js
|
||
lt.contextMenu.register('[data-context-menu="my-menu"]', [
|
||
{ label: 'View Details', action: () => {} },
|
||
{ label: 'Copy ID', action: () => {} },
|
||
{ separator: true },
|
||
{ label: 'Delete', action: () => {}, danger: true },
|
||
]);
|
||
```
|
||
|
||
### Command Palette
|
||
|
||
```html
|
||
<div id="lt-cmd-overlay" class="lt-cmd-overlay" aria-hidden="true" role="dialog">
|
||
<div class="lt-cmd-box">
|
||
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
|
||
placeholder="Search commands…" autocomplete="off" spellcheck="false">
|
||
<ul class="lt-cmd-results" id="lt-cmd-results" role="listbox"></ul>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
lt.cmdPalette.init([
|
||
{ id: 'new-ticket', label: 'New Ticket', shortcut: 'N', action: () => {} },
|
||
{ id: 'go-settings', label: 'Go to Settings', action: () => {} },
|
||
]);
|
||
// Default binding: Ctrl+K opens the palette
|
||
```
|
||
|
||
### Progress Bars
|
||
|
||
```html
|
||
<div class="lt-progress" data-value="65" data-max="100">
|
||
<div class="lt-progress-bar"></div>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
lt.progress.set(el, 65);
|
||
lt.progress.animate(el, 0, 65, 800);
|
||
```
|
||
|
||
### WebSocket Status
|
||
|
||
```html
|
||
<div class="lt-ws-status" id="ws-status" aria-live="polite">
|
||
<span class="lt-ws-dot"></span>
|
||
<span class="lt-ws-label">Connecting…</span>
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
const ws = lt.ws.connect('wss://app.example.com/ws', {
|
||
statusEl: document.getElementById('ws-status'),
|
||
onMessage(data) { console.log(data); },
|
||
onOpen() { lt.toast.success('Connected'); },
|
||
onClose() { lt.toast.warning('Disconnected'); },
|
||
reconnect: true,
|
||
reconnectDelay: 2000,
|
||
});
|
||
ws.send({ type: 'ping' });
|
||
ws.close();
|
||
```
|
||
|
||
### Boot Sequence Overlay
|
||
|
||
```js
|
||
lt.boot.run('MY APP'); // CRT boot animation, auto-dismisses
|
||
lt.boot.run('MY APP', true); // force replay even if already shown this session
|
||
```
|
||
|
||
Shown once per session. Skipped automatically under `prefers-reduced-motion`.
|
||
|
||
### Timeline / Log Entries
|
||
|
||
```html
|
||
<div class="lt-timeline">
|
||
<div class="lt-timeline-entry">
|
||
<span class="lt-timeline-dot"></span>
|
||
<div class="lt-timeline-body">
|
||
<span class="lt-timeline-time">14:32</span>
|
||
<span class="lt-timeline-text">Build started</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### Skeleton Loaders
|
||
|
||
```html
|
||
<div class="lt-skeleton lt-skeleton-title"></div>
|
||
<div class="lt-skeleton lt-skeleton-text"></div>
|
||
<div class="lt-skeleton lt-skeleton-text" style="width:70%"></div>
|
||
<div class="lt-skeleton lt-skeleton-avatar"></div>
|
||
<div class="lt-skeleton lt-skeleton-btn"></div>
|
||
```
|
||
|
||
Shimmer animation is disabled under `prefers-reduced-motion` and on touch devices.
|
||
|
||
### Inline Alert Banners
|
||
|
||
```html
|
||
<div class="lt-alert lt-alert-warning" role="alert">
|
||
<span class="lt-alert-icon">⚠</span>
|
||
<span class="lt-alert-text">Maintenance window at 02:00 UTC</span>
|
||
<button type="button" class="lt-alert-dismiss" aria-label="Dismiss">✕</button>
|
||
</div>
|
||
```
|
||
|
||
Types: `lt-alert-info`, `lt-alert-success`, `lt-alert-warning`, `lt-alert-danger`.
|
||
|
||
JS: `lt.alerts.init()` wires dismiss buttons (called by `lt.init()`).
|
||
|
||
---
|
||
|
||
## JavaScript API
|
||
|
||
All modules live under `window.lt`. Full list:
|
||
|
||
| Module | Key Methods |
|
||
|--------|-------------|
|
||
| `lt.init` | `(opts?)` — master initializer |
|
||
| `lt.toast` | `success / error / warning / info (msg, ms?)` |
|
||
| `lt.modal` | `open / close / closeAll` |
|
||
| `lt.tabs` | `init / switch(panelId)` |
|
||
| `lt.accordion` | `init / open / close` |
|
||
| `lt.tooltip` | `init / show / hide` |
|
||
| `lt.sidebar` | `init` |
|
||
| `lt.mobileNav` | `open / close / toggle` |
|
||
| `lt.boot` | `run(name, force?)` |
|
||
| `lt.keys` | `on / off / initDefaults` |
|
||
| `lt.clipboard` | `copy(text) → Promise<bool> / initCopyButtons` |
|
||
| `lt.alerts` | `init / dismiss(el)` |
|
||
| `lt.progress` | `set / animate` |
|
||
| `lt.cmdPalette` | `init / open / close / register` |
|
||
| `lt.validate` | `field / form / showError / clearError / init / custom` |
|
||
| `lt.tableNav` | `init(tableId)` |
|
||
| `lt.sortTable` | `init(tableId)` |
|
||
| `lt.tableColumns` | `init / show / hide / toggle` |
|
||
| `lt.statsFilter` | `init` |
|
||
| `lt.autoRefresh` | `start(fn, ms) / stop / now` |
|
||
| `lt.dropzone` | `init(el, opts)` |
|
||
| `lt.combobox` | `init(input, options, opts)` |
|
||
| `lt.typeahead` | `init(input, source, opts)` |
|
||
| `lt.wizard` | `init(el, opts)` |
|
||
| `lt.sortable` | `init(listEl, opts)` |
|
||
| `lt.splitPane` | `init(el, opts)` |
|
||
| `lt.lightbox` | `init(selector, opts)` |
|
||
| `lt.contextMenu` | `register(selector, items)` |
|
||
| `lt.ws` | `connect(url, opts)` |
|
||
| `lt.pagination` | `init(navEl, opts)` |
|
||
| `lt.theme` | `toggle / set('light'\|'dark') / get` |
|
||
| `lt.notif` | `set / inc / clear` |
|
||
| `lt.viewport` | `bp / is / on / off / touch / landscape` |
|
||
| `lt.bus` | `on / off / emit / once` |
|
||
| `lt.store` | `set / get / remove / clear / session.*` |
|
||
| `lt.url` | `params / get / set / remove / setMultiple` |
|
||
| `lt.api` | `get / post / put / patch / delete(url, body?)` |
|
||
| `lt.csrf` | `headers()` |
|
||
| `lt.time` | `ago / uptime / format` |
|
||
| `lt.bytes` | `format(n)` |
|
||
| `lt.num` | `format / compact / percent / pad / clamp / lerp` |
|
||
| `lt.dom` | `el / qs / qsa / on / off / show / hide / toggle` |
|
||
| `lt.observe` | `lazy(selector) / sentinel(el, cb)` |
|
||
| `lt.poll` | `(fn, ms, opts?) → { stop }` |
|
||
| `lt.retry` | `(fn, opts?) → Promise` |
|
||
| `lt.debounce` | `(fn, wait)` |
|
||
| `lt.throttle` | `(fn, wait)` |
|
||
|
||
### API Helper
|
||
|
||
```js
|
||
// GET — no Content-Type header (correct per HTTP spec)
|
||
const data = await lt.api.get('/api/tickets');
|
||
|
||
// POST / PUT / PATCH / DELETE — JSON body + Content-Type
|
||
const result = await lt.api.post('/api/tickets', { title: 'Bug', priority: 'high' });
|
||
await lt.api.delete('/api/tickets/42');
|
||
```
|
||
|
||
CSRF token is read from `<meta name="csrf-token">` or set via `lt.init({ csrf: 'token' })`.
|
||
|
||
### Event Bus
|
||
|
||
```js
|
||
lt.bus.on('ticket:created', (ticket) => { refreshTable(); });
|
||
lt.bus.emit('ticket:created', { id: 99, title: 'New Bug' });
|
||
lt.bus.once('deploy:done', () => lt.toast.success('Deployed!'));
|
||
lt.bus.off('ticket:created');
|
||
```
|
||
|
||
### Local / Session Storage
|
||
|
||
```js
|
||
lt.store.set('key', { any: 'value' }); // JSON-serialized
|
||
lt.store.get('key'); // → parsed value or null
|
||
lt.store.remove('key');
|
||
lt.store.clear();
|
||
|
||
lt.store.session.set('key', value); // sessionStorage
|
||
lt.store.session.get('key');
|
||
```
|
||
|
||
---
|
||
|
||
## Theming
|
||
|
||
### Dark / Light Toggle
|
||
|
||
```html
|
||
<button type="button" class="lt-theme-toggle" id="theme-toggle"
|
||
aria-label="Toggle theme">☀</button>
|
||
```
|
||
|
||
```js
|
||
lt.theme.toggle(); // flip dark ↔ light
|
||
lt.theme.set('light'); // explicitly set
|
||
lt.theme.get(); // → 'dark' | 'light'
|
||
```
|
||
|
||
Theme is persisted to `localStorage` (`lt_theme` key). Changes sync across browser tabs instantly via the `storage` event. The native `color-scheme` CSS property is also updated so browser UI (scrollbars, form controls) matches the current theme.
|
||
|
||
### CSS Only
|
||
|
||
Set `data-theme="light"` on `<html>` directly. All component styles react through the Section 51 overrides.
|
||
|
||
---
|
||
|
||
## Accessibility
|
||
|
||
- All interactive elements have `role`, `aria-label`, `aria-expanded`, `aria-controls`, `aria-hidden` as appropriate.
|
||
- Focus rings: `outline: 2px solid var(--accent-orange)` with `outline-offset: 2px`.
|
||
- Screen-reader-only text: `<span class="lt-sr-only">…</span>`.
|
||
- Touch targets: all interactive elements are ≥ 44×44px on `pointer: coarse` devices.
|
||
- `prefers-reduced-motion`: skeleton shimmer and boot animations are disabled.
|
||
- `prefers-color-scheme`: theme is auto-detected on first load if no saved preference.
|
||
- Keyboard navigation: arrow keys / Enter in tables, Ctrl+K for command palette, Escape closes modals / drawers / dropdowns.
|
||
- Data tables require `<caption class="lt-sr-only">…</caption>`.
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
```
|
||
web_template/
|
||
├── base.css Design system styles (79 sections, ~5,200 lines)
|
||
├── base.js Design system JS (55+ modules, ~2,800 lines)
|
||
├── base.html Living reference template
|
||
├── README.md This file
|
||
└── (framework skeletons)
|
||
├── php/ PHP / Tinker Tickets
|
||
├── python/ Flask / Jinja2 / GANDALF
|
||
└── node/ Express / EJS / PULSE
|
||
```
|
||
|
||
---
|
||
|
||
## Changelog
|
||
|
||
### v1.2 (current)
|
||
|
||
**Design System**
|
||
- Font: Courier New → **JetBrains Mono** (400, 600, 700; italic)
|
||
- Palette: phosphor-green single-accent → **5-accent neon system** (Orange #FF6B00, Cyan #00D4FF, Green #00FF88, Red #FF2D55, Purple #BF5FFF)
|
||
- Background: green-tinted CRT → **Anduril dark military-tech** (`#030508`–`#0d1520`)
|
||
- **Light mode rebuilt from scratch** (~200 lines, Section 51): desaturated accent colors, drop-shadow rings instead of neon glows, WCAG-accessible contrast on all 80+ component classes
|
||
- Added `--corner-cut`, `--corner-cut-sm`, `--corner-cut-lg` CSS variables
|
||
- Added `--z-panel` (10020) to z-index ladder; replaced all hardcoded z-index values
|
||
- Added `will-change: transform` on `.lt-spinner`, `will-change: opacity` on `.lt-skeleton`
|
||
- Removed duplicate `.lt-hidden` definition
|
||
|
||
**New Components**
|
||
- Notification bell: unread badge, animated dropdown panel, mark-all-read, per-item click-to-read, outside-click close
|
||
- Generic dropdown widget: `.lt-dropdown-wrap` + `.lt-dropdown-panel` + `.lt-dropdown-trigger`
|
||
- Editable ticket detail drawer: form grid, status/priority/assignee selects, description textarea, comment box
|
||
- Advanced filter dropdown for ticket queue
|
||
- Bulk actions dropdown with toast feedback
|
||
- Footer landmark (`<footer class="lt-footer">`) with copyright
|
||
|
||
**CSS Fixes**
|
||
- Responsive table breakpoint: `640px` → `767px`
|
||
- Touch targets: 44px minimum on `pointer: coarse`
|
||
- Breakpoints expanded from 4 to 8 tiers (xs → 4k)
|
||
- Safe-area insets added: `env(safe-area-inset-*)` in header, nav, footer
|
||
- Kanban forced 2-column at sm/xs breakpoints
|
||
- Toast background: hardcoded rgba → `var(--bg-overlay)`
|
||
|
||
**JavaScript Fixes**
|
||
- **Critical:** `lt.clipboard.initCopyButtons()` call fixed (was `lt.clipboard.init()` — non-existent, crashed entire DOMContentLoaded handler)
|
||
- **Critical:** Removed duplicate `function showToast` declaration (JS hoisting caused infinite recursion → silent toasts for all types); progress bar inlined into `_displayToast`
|
||
- **`lt.init()`**: New master initializer with `_ltInitialized` double-init guard
|
||
- **`lt.pagination`** (Module 55): Paginator with ellipsis, prev/next, `onChange` callback
|
||
- **`lt.sortable`**: Shared module-level drag state enables cross-column Kanban drag-and-drop between same-group instances
|
||
- **`lt.theme`**: Sets `color-scheme` CSS property, updates `<meta name="theme-color">`, cross-tab sync via `storage` event
|
||
- **`lt.api`**: GET requests no longer send `Content-Type: application/json` (HTTP spec)
|
||
- **`lt.combobox` / `lt.typeahead`**: `escHtml()` applied before mark-tag insertion (XSS fix)
|
||
- **Toast queue**: Capped at 12 pending entries
|
||
|
||
### v1.0
|
||
|
||
Initial release — Courier New, phosphor-green `#00ff41`, 4-breakpoint responsive, single-accent palette.
|