2026-03-27 14:32:58 -04:00
# LotusGuild Terminal Design System — v1.2
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
**Aesthetic: ** Anduril × Hacker Terminal — dark military-tech, multi-accent neon, angular clip-path frames, glitch effects.
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
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.
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## 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 )
2026-03-28 21:04:57 -04:00
12. [LDAP Avatar Integration ](#ldap-avatar-integration )
13. [Changelog ](#changelog )
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
---
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
## Overview
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| 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 |
2026-03-14 21:27:31 -04:00
2026-03-27 14:32:58 -04:00
No build step. No npm. No framework. Drop in the two files and go.
2026-03-14 21:27:31 -04:00
2026-03-27 14:32:58 -04:00
---
## Quick Start
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
<!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.
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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)
} ) ;
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Modules initialized automatically: `tabs` , `accordion` , `tooltip` , `clipboard.initCopyButtons` , `alerts` , `sidebar` , `keys.initDefaults` , `mobileNav` .
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## Design Tokens
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
All tokens are CSS custom properties on `:root` . Override any token in your app stylesheet **after ** importing `base.css` .
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Backgrounds
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| 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)` |
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Text
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| Token | Dark | Light |
|-------|------|-------|
| `--text-primary` | `#e8edf5` | `#1a2233` |
| `--text-secondary` | `#8899b0` | `#3a4a60` |
| `--text-muted` | `#4a5a70` | `#6a7a90` |
| `--text-accent` | `#FF6B00` | `#c44e00` |
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Borders
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| 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)` |
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Spacing
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
`--space-xs` (0.25rem) · `--space-sm` (0.5rem) · `--space-md` (1rem) · `--space-lg` (1.5rem) · `--space-xl` (2rem) · `--space-2xl` (3rem)
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Layout
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| Token | Value |
|-------|-------|
| `--sidebar-width` | `240px` |
| `--header-height` | `56px` |
| `--container-max` | `1600px` |
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Transitions
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| Token | Value |
|-------|-------|
| `--transition-fast` | `all 0.12s ease` |
| `--transition-default` | `all 0.25s ease` |
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### 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 |
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## Breakpoints
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
Mobile-first. 8 tiers:
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
| 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 ( )
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
---
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
## Typography
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
**Font: ** JetBrains Mono (Google Fonts) — weights 400, 600, 700; italic 400.
Load via:
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< link href = "https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel = "stylesheet" >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
| 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)` ).
---
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
## Component Catalog
All components use the `.lt-` prefix.
### Layout
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
<!-- 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 -->
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### ASCII Frame System
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
The signature angled clip-path card border:
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### Cards
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### Buttons
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### Forms & Inputs
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
```
2026-03-27 14:32:58 -04:00
JS: `lt.dropzone.init(el, { onFiles(files){} })`
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
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
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
<!-- Notification dot -->
< span class = "lt-notif-badge" data-count = "3" > < / span >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
JS: `lt.notif.set(el, n)` / `lt.notif.inc(el)` / `lt.notif.clear(el)`
2026-03-14 21:08:57 -04:00
### Tables
``` html
< div class = "lt-table-wrap" >
< table class = "lt-table" id = "my-table" >
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / tr >
2026-03-27 14:32:58 -04:00
< / thead >
< tbody > …< / tbody >
2026-03-14 21:08:57 -04:00
< / table >
< / div >
2026-03-27 14:32:58 -04:00
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
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
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Responsive: collapses to card layout below 767px.
### Modals
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
JS: `lt.modal.open('my-modal')` / `lt.modal.close('my-modal')` / `lt.modal.closeAll()`
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### 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)
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
2026-03-27 14:32:58 -04:00
< div class = "lt-overlay" id = "my-overlay" > < / div >
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Tabs
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 >
2026-03-14 21:08:57 -04:00
< / div >
2026-03-27 14:32:58 -04:00
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
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 >
2026-03-14 21:08:57 -04:00
< / div >
```
2026-03-27 14:32:58 -04:00
JS: `lt.accordion.init()` (called by `lt.init()` )
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Tooltips
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< span class = "lt-tooltip-anchor" data-tooltip = "Tooltip text" > Hover me< / span >
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
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 >
2026-03-14 21:08:57 -04:00
< / div >
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
< / div >
< / div >
```
2026-03-27 14:32:58 -04:00
### Dropdown Widget
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
Generic dropdown attached to any trigger button:
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
```
2026-03-27 14:32:58 -04:00
Closes on outside click and Escape (wired in DOMContentLoaded).
### Combobox (Searchable Select)
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 >
```
2026-03-14 21:08:57 -04:00
``` js
2026-03-27 14:32:58 -04:00
lt . combobox . init ( inputEl , [
{ value : '1' , label : 'Alice' } ,
{ value : '2' , label : 'Bob' } ,
] , {
onSelect ( opt ) { console . log ( opt . value ) ; } ,
} ) ;
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Labels are HTML-escaped before highlight mark insertion (XSS-safe).
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Typeahead
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< input type = "text" class = "lt-input" id = "my-typeahead"
placeholder = "Type to search…" autocomplete = "off" >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
``` 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 ,
} ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Wizard / Stepper
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
< / div >
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
``` js
const wiz = lt . wizard . init ( document . getElementById ( 'my-wizard' ) , {
onComplete ( ) { lt . toast . success ( 'Done!' ) ; } ,
} ) ;
wiz . goTo ( 1 ) ; wiz . next ( ) ; wiz . prev ( ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Kanban / Sortable
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 -->
2026-03-14 21:08:57 -04:00
< / div >
```
2026-03-27 14:32:58 -04:00
``` 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 */ } ,
} ) ;
} ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Pagination
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` html
< nav class = "lt-pagination" id = "demo-pagination" aria-label = "Page navigation" > < / nav >
```
2026-03-14 21:08:57 -04:00
``` js
2026-03-27 14:32:58 -04:00
lt . pagination . init ( document . getElementById ( 'demo-pagination' ) , {
total : 100 ,
pageSize : 10 ,
current : 1 ,
onChange ( page ) { loadPage ( page ) ; } ,
} ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Split Pane
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 >
```
2026-03-14 21:27:31 -04:00
2026-03-27 14:32:58 -04:00
``` js
lt . splitPane . init ( document . getElementById ( 'my-split' ) , { minTop : 80 , minBottom : 80 } ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Lightbox
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` html
< img src = "thumb.jpg" data-lightbox = "path/to/full.jpg"
data-lightbox-caption = "Caption" alt = "…" >
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` js
lt . lightbox . init ( '[data-lightbox]' ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Context Menu
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` js
lt . contextMenu . register ( '[data-context-menu="my-menu"]' , [
{ label : 'View Details' , action : ( ) => { } } ,
{ label : 'Copy ID' , action : ( ) => { } } ,
{ separator : true } ,
{ label : 'Delete' , action : ( ) => { } , danger : true } ,
] ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Command Palette
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 >
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Progress Bars
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` html
< div class = "lt-progress" data-value = "65" data-max = "100" >
< div class = "lt-progress-bar" > < / div >
< / div >
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` js
lt . progress . set ( el , 65 ) ;
lt . progress . animate ( el , 0 , 65 , 800 ) ;
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### WebSocket Status
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 ( ) ;
```
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Boot Sequence Overlay
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Shown once per session. Skipped automatically under `prefers-reduced-motion` .
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Timeline / Log Entries
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
### Skeleton Loaders
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Shimmer animation is disabled under `prefers-reduced-motion` and on touch devices.
### Inline Alert Banners
2026-03-14 21:08:57 -04:00
``` html
2026-03-27 14:32:58 -04:00
< 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 >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
Types: `lt-alert-info` , `lt-alert-success` , `lt-alert-warning` , `lt-alert-danger` .
JS: `lt.alerts.init()` wires dismiss buttons (called by `lt.init()` ).
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## 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
2026-03-14 21:08:57 -04:00
``` js
2026-03-27 14:32:58 -04:00
// 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' ) ;
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
CSRF token is read from `<meta name="csrf-token">` or set via `lt.init({ csrf: 'token' })` .
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### Event Bus
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` 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' ) ;
```
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## Theming
### Dark / Light Toggle
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
``` html
< button type = "button" class = "lt-theme-toggle" id = "theme-toggle"
aria-label = "Toggle theme" > ☀< / button >
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
``` js
lt . theme . toggle ( ) ; // flip dark ↔ light
lt . theme . set ( 'light' ) ; // explicitly set
lt . theme . get ( ) ; // → 'dark' | 'light'
2026-03-14 21:08:57 -04:00
```
2026-03-27 14:32:58 -04:00
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.
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
### CSS Only
2026-03-14 21:08:57 -04:00
2026-03-27 14:32:58 -04:00
Set `data-theme="light"` on `<html>` directly. All component styles react through the Section 51 overrides.
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## 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>` .
2026-03-14 21:08:57 -04:00
---
2026-03-27 14:32:58 -04:00
## 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
```
---
2026-03-14 21:08:57 -04:00
2026-03-28 21:04:57 -04:00
## LDAP Avatar Integration
`lt-avatar` components support real profile photos pulled from **lldap ** (the LotusGuild LDAP server). Photos overlay the initials fallback — if a user has no photo the initials show instead; no broken images.
### Infrastructure (one-time, per app)
**1. Create a service account in lldap **
Log into lldap at `http://10.10.10.39:17170` , or use the GraphQL API:
``` bash
TOKEN = $( curl -s -X POST http://10.10.10.39:17170/auth/simple/login \
-H 'Content-Type: application/json' \
-d '{"username":"<admin>","password":"<pw>"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" )
# Create service account
curl -s -X POST http://10.10.10.39:17170/api/graphql \
-H "Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d '{"query":"mutation { createUser(user: { id: \"my-app\", email: \"my-app@lotusguild.org\", displayName: \"My App Service\" }) { id } }"}'
# Add to lldap_strict_readonly (id=3) for directory read access
curl -s -X POST http://10.10.10.39:17170/api/graphql \
-H "Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d '{"query":"mutation { addUserToGroup(userId: \"my-app\", groupId: 3) { ok } }"}'
```
Then set the password via `ldappasswd` :
``` bash
ldappasswd -H ldap://10.10.10.39:3890 \
-D "uid=<admin>,ou=people,dc=example,dc=com" \
-w '<admin-pw>' \
-s '<service-account-pw>' \
"uid=my-app,ou=people,dc=example,dc=com"
```
**2. Install php-ldap and create the avatar cache directory **
``` bash
apt-get install -y php8.2-ldap
mkdir -p /var/www/html/myapp/uploads/avatars
chown -R www-data:www-data /var/www/html/myapp/uploads/avatars
```
**3. Add LDAP config to `.env` **
``` ini
LDAP_ENABLED = true
LDAP_HOST = 10.10.10.39
LDAP_PORT = 3890
LDAP_BIND_DN = "uid=my-app,ou=people,dc=example,dc=com"
LDAP_BIND_PW = "<service-account-pw>"
LDAP_BASE_DN = "dc=example,dc=com"
LDAP_USER_BASE = "ou=people,dc=example,dc=com"
AVATAR_CACHE_TTL = 3600
```
> **Note:** lldap is currently configured with `dc=example,dc=com` as the base DN across all services (Authelia, etc.). Do not change this per-app — it requires a coordinated infrastructure migration.
### Avatar Endpoint (`/api/user_avatar.php`)
Copy the reference implementation from `tinker_tickets/api/user_avatar.php` . It:
1. Requires a valid session (returns 401 otherwise)
2. Accepts `?user_id=N` and looks up the user's `username` from the app's DB
3. Binds to lldap and searches `ou=people,dc=example,dc=com` with filter `(uid={username})`
4. Fetches the `avatar` attribute (raw binary JPEG, returned as-is by `ldap_get_entries()` )
5. Validates JPEG magic bytes (`\xFF\xD8\xFF` ) and writes to `uploads/avatars/user_{id}.jpg`
6. Writes a `.none` sentinel file for users with no avatar so lldap is not queried again until TTL expires
7. Serves the cached file with `Content-Type: image/jpeg`
### CSS — Photo-over-Initials Pattern
`base.css` (Section 62 — Avatar) provides `.lt-avatar-img` and `.lt-avatar-initials` :
``` css
. lt-avatar { position : relative ; }
. lt-avatar-initials { position : absolute ; inset : 0 ; display : flex ; align-items : center ; justify-content : center ; }
. lt-avatar-img { position : absolute ; inset : 0 ; width : 100 % ; height : 100 % ; object-fit : cover ; border-radius : inherit ; z-index : 1 ; }
```
The photo sits above the initials. The `onerror` handler hides the image if the endpoint returns 404 (no avatar), letting the initials show through.
### HTML Pattern
``` php
< ? php
$words = array_filter ( explode ( ' ' , $displayName ));
$initials = strtoupper ( implode ( '' , array_map ( fn ( $w ) => $w [ 0 ], array_slice ( $words , 0 , 2 ))));
$colors = [ 'lt-avatar--orange' , 'lt-avatar--green' , 'lt-avatar--purple' , '' ];
$color = $colors [ abs ( crc32 ( $displayName )) % count ( $colors )];
?>
<div class="lt-avatar lt-avatar--sm <?= $color ?>" aria-hidden="true">
<?php if ($userId > 0): ?>
<img src="/api/user_avatar.php?user_id=<?= $userId ?>"
alt=""
class="lt-avatar-img"
onerror="this.style.display='none'">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
</div>
```
Key points:
- Always render the initials — they are the fallback, never a broken state
- The `onerror` on the `<img>` hides it when the endpoint returns 404 (no photo set)
- Color is deterministically derived from the display name so it's consistent across page loads
- `aria-hidden="true"` because the avatar is purely decorative; the user's name appears in adjacent text
---
2026-03-31 19:43:49 -04:00
## Known Patterns & Gotchas
### Disabled / read-only form elements in display-only contexts
`base.css` applies aggressive visibility reduction to `:disabled` and `[readonly]` form elements:
``` css
. lt-input : disabled , . lt-select : disabled , . lt-textarea : disabled ,
. lt-input [ readonly ] , . lt-textarea [ readonly ] {
opacity : 0.45 ;
color : var ( - - text - muted ) ; /* #3e607a — ~3.2:1 contrast, fails WCAG AA on dark backgrounds */
}
. lt-checkbox : disabled { opacity : 0.4 ; }
```
This is intentional for * genuinely * disabled controls (submission forms, locked fields). However, if you use `disabled` purely to make a field **non-interactive for display ** , the result is nearly unreadable on dark/OLED screens.
**Pattern: display-only selects / inputs (edit-mode toggle) **
A common pattern is disabling fields in view-mode and enabling them in edit-mode. Apply a scoping class and override in your app CSS:
``` css
/* In your app's CSS — override base.css fading for display-only fields */
. your-display-field : disabled ,
. your-display-field [ disabled ] {
opacity : 1 ;
color : var ( - - text - secondary ) ; /* #7fa3bf — full legibility */
cursor : default ;
pointer-events : none ;
}
```
**Pattern: copy-to-clipboard inputs (readonly) **
`[readonly]` triggers the same `opacity: 0.45` rule. For API key / token display fields where the user must read and copy the value, restore opacity inline or via class:
``` html
< input type = "text" readonly class = "lt-input" style = "opacity:1;cursor:text" >
```
**Pattern: description / content display areas **
Avoid rendering multi-line content in a `disabled` textarea — use a styled `<div>` instead. Apply `white-space: pre-wrap` on that div to preserve newlines and multiple spaces (required for ASCII art / diagrams to align correctly, since the body font is already monospace):
``` css
. your-description-view {
white-space : pre-wrap ;
word-break : break-word ;
color : var ( - - text - primary ) ;
}
```
Set `innerHTML = escHtml(rawText)` — no `<br>` replacement needed when `white-space: pre-wrap` is active.
---
2026-03-27 14:32:58 -04:00
## 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.