# LotusGuild Terminal Design System — v1.2 [![Lint](https://code.lotusguild.org/LotusGuild/web_template/actions/workflows/lint.yml/badge.svg)](https://code.lotusguild.org/LotusGuild/web_template/actions?workflow=lint.yml) **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. [LDAP Avatar Integration](#ldap-avatar-integration) 13. [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 ``` ### `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 ``` | 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
``` ### ASCII Frame System The signature angled clip-path card border: ```html
SECTION HEADER
…content…
``` ### Cards ```html
``` ### Buttons ```html ``` ### Forms & Inputs ```html
Helper text
Drop files here or click to browse
``` 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 Open Closed In Progress Critical High Medium Low ``` JS: `lt.notif.set(el, n)` / `lt.notif.inc(el)` / `lt.notif.clear(el)` ### Tables ```html
Accessible caption
Name ↕
``` 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 ``` 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
``` ### Tabs ```html
``` JS: `lt.tabs.init()` — auto-wires all `.lt-tabs` on page. `lt.tabs.switch('tab-two')` — programmatic switch. ### Accordion ```html
``` JS: `lt.accordion.init()` (called by `lt.init()`) ### Tooltips ```html Hover me ``` JS: `lt.tooltip.init()` (called by `lt.init()`). Custom placement via `data-tooltip-pos="top|bottom|left|right"`. ### Notifications Bell ```html
``` ### Dropdown Widget Generic dropdown attached to any trigger button: ```html
``` Closes on outside click and Escape (wired in DOMContentLoaded). ### Combobox (Searchable Select) ```html
``` ```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 ``` ```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
Step 1
Step 2
Step 3
…Panel 1…
…Panel 2…
…Panel 3…
``` ```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
Open
Card 1
``` ```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 ``` ```js lt.pagination.init(document.getElementById('demo-pagination'), { total: 100, pageSize: 10, current: 1, onChange(page) { loadPage(page); }, }); ``` ### Split Pane ```html
…top panel…
…bottom panel…
``` ```js lt.splitPane.init(document.getElementById('my-split'), { minTop: 80, minBottom: 80 }); ``` ### Lightbox ```html … ``` ```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 ``` ```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
``` ```js lt.progress.set(el, 65); lt.progress.animate(el, 0, 65, 800); ``` ### WebSocket Status ```html
Connecting…
``` ```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
14:32 Build started
``` ### Skeleton Loaders ```html
``` Shimmer animation is disabled under `prefers-reduced-motion` and on touch devices. ### Inline Alert Banners ```html ``` 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 / 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 `` 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 ``` ```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 `` 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: ``. - 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 `…`. --- ## Starting a New App ### 1. Serve the design system files Add an nginx alias so every app on the same host can reference the same files: ```nginx # In each app's server block (or a shared include): location /web_template/ { alias /path/to/web_template/; expires 7d; add_header Cache-Control "public, immutable"; } ``` Then in your HTML: ```html ``` ### 2. Copy the right skeleton | Stack | Copy this file | Into your app as | |-------|---------------|-----------------| | PHP | `php/layout.php` | `views/layout.php` (or `layout_header.php`) | | Python/Flask | `python/base.html` | `templates/base.html` | | Node/Express | `node/middleware.js` + `node/layout.ejs` | `middleware.js` + `views/layout.ejs` | ### 3. Define your nav **PHP** — pass `$navLinks` before including the layout: ```php $navLinks = [ ['href' => '/', 'key' => 'dashboard', 'label' => 'Dashboard'], ['href' => '/reports', 'key' => 'reports', 'label' => 'Reports'], ['label' => 'Admin', 'key' => 'admin', 'adminOnly' => true, 'children' => [ ['href' => '/admin/users', 'label' => 'Users'], ]], ]; $activeNav = 'dashboard'; include __DIR__ . '/views/layout.php'; ``` **Python/Flask** — inject via context processor or pass directly to `render_template`: ```python nav_links = [ {'href': url_for('index'), 'key': 'dashboard', 'label': 'Dashboard'}, {'href': url_for('settings'), 'key': 'settings', 'label': 'Settings'}, ] return render_template('page.html', nav_links=nav_links) ``` **Node/Express** — set on `res.locals` via `injectLocals` middleware: ```js app.use((req, res, next) => { res.locals.navLinks = [ { href: '/', key: 'dashboard', label: 'Dashboard' }, { href: '/workers', key: 'workers', label: 'Workers' }, ]; next(); }); ``` ### 4. Add app-specific CSS Create `app.css` in your app. Import nothing from `base.css` — just override tokens or add components: ```css /* app.css — app-specific extensions only */ :root { --app-accent: #FF6B00; /* override if needed */ } /* Only put styles here that aren't already in base.css */ ``` ### 5. Initialise In your base template, after `base.js`: ```html ``` --- ## 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 component reference — open in browser to browse everything ├── README.md This file ├── AUTHELIA_INTEGRATION.md Theming Authelia portal with this design system └── framework skeletons/ ├── php/ │ └── layout.php Generic PHP base layout (pass $navLinks) ├── python/ │ ├── base.html Jinja2 base template (pass nav_links list) │ └── auth.py Authelia SSO helper for Flask └── node/ ├── middleware.js Express middleware (auth, CSRF, nonce, rate limit) └── layout.ejs EJS base template (uses res.locals.navLinks) ``` --- ## 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":"","password":""}' | 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=,ou=people,dc=example,dc=com" \ -w '' \ -s '' \ "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="" 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 $w[0], array_slice($words, 0, 2)))); $colors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', '']; $color = $colors[abs(crc32($displayName)) % count($colors)]; ?> ``` Key points: - Always render the initials — they are the fallback, never a broken state - The `onerror` on the `` 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 --- ## 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 ``` **Pattern: description / content display areas** Avoid rendering multi-line content in a `disabled` textarea — use a styled `
` 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 `
` replacement needed when `white-space: pre-wrap` is active. --- ## 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 (`