Files
web_template/README.md
Jared Vititoe 997450aaf1 Fix boot sequence alignment, rate limiter memory leak, and docs gaps
- base.js: Fix boot sequence title centering — old formula was off by 1
  char for odd-length app names (PULSE, GANDALF). Remove unused `bar`
  variable. New logic computes left/right padding independently to
  handle both even and odd title lengths correctly.
- node/middleware.js: Prune expired rate-limit entries from Map when
  size exceeds 5000 to prevent unbounded memory growth. Also use
  req.socket?.remoteAddress as fallback for req.ip.
- README.md: Document lt.beep() in the JS API section. Clarify Quick
  Start to distinguish core files from platform-specific helpers.
  Note that tableNav.init() and sortTable.init() require explicit calls.
- aesthetic_diff.md: Correct §8 toast icon format — base.js uses
  bracketed symbols [✓][✗][!][i], not >> prefix as previously stated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 21:27:31 -04:00

603 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LotusGuild Terminal Design System v1.0
Unified layout, styling, and JavaScript utilities for all LotusGuild web applications.
**Applies to:** Tinker Tickets (PHP) · PULSE (Node.js) · GANDALF (Flask)
---
## Quick Start
**Core files** (needed by every app):
```
base.css → all variables, components, animations, responsive rules
base.js → toast, modal, tabs, CSRF, fetch helpers, keyboard shortcuts
base.html → full component reference (static demo, not for production)
```
**Platform-specific helpers** (one per backend):
```
php/layout.php → PHP base layout with CSP nonce + CSRF injection
python/auth.py → Flask @require_auth / @require_admin decorators
python/base.html → Jinja2 base template with block inheritance
node/middleware.js → Express requireAuth, csrfMiddleware, cspNonce, etc.
```
Then load them in your HTML:
```html
<link rel="stylesheet" href="/web_template/base.css">
<!-- app-specific overrides after: -->
<link rel="stylesheet" href="/assets/css/app.css">
<script src="/web_template/base.js"></script>
<!-- app-specific JS after: -->
<script nonce="NONCE" src="/assets/js/app.js?v=YYYYMMDD"></script>
```
---
## Aesthetic at a Glance
| Property | Value |
|---|---|
| Background | `#0a0a0a` (near-black) |
| Primary accent | `#00ff41` (phosphor green) |
| Secondary accent | `#ffb000` (amber) |
| Tertiary accent | `#00ffff` (cyan) |
| Error / critical | `#ff4444` (red) |
| Font | `'Courier New', 'Consolas', 'Monaco', 'Menlo', monospace` |
| Border radius | `0` — no rounded corners ever |
| Box shadow | Glow halos only (`0 0 Xpx color`) — no drop shadows |
| Borders | `2px solid` standard · `3px double` for outer frames / modals |
| CRT effects | Scanline animation · screen flicker at 30s · binary corner watermark |
---
## CSS Prefix
All classes use the `.lt-` prefix (*LotusGuild Terminal*) to prevent collisions with app-specific CSS.
```css
.lt-btn /* buttons */
.lt-card /* cards */
.lt-frame /* ASCII outer frames */
.lt-modal-overlay /* modals */
.lt-toast /* toasts */
.lt-status-* /* status badges */
.lt-p1 .lt-p5 /* priority badges */
/* etc. */
```
---
## CSS Custom Properties
All colour, spacing, and layout values are CSS custom properties on `:root`.
Override them per-app in your `app.css`:
```css
/* Example: app-specific accent on top of base */
:root {
--terminal-amber: #ffd000; /* slightly brighter amber */
}
```
### Key Variables
```css
/* Backgrounds */
--bg-primary: #0a0a0a
--bg-secondary: #1a1a1a
--bg-tertiary: #2a2a2a
--bg-terminal: #001a00
/* Terminal colours */
--terminal-green: #00ff41
--terminal-amber: #ffb000
--terminal-cyan: #00ffff
--terminal-red: #ff4444
/* Glow stacks (text-shadow) */
--glow-green
--glow-green-intense
--glow-amber
--glow-amber-intense
--glow-cyan
--glow-red
/* Box-shadow halos */
--box-glow-green
--box-glow-amber
--box-glow-red
/* Z-index ladder */
--z-dropdown: 100 --z-modal: 500 --z-toast: 800 --z-overlay: 9999
```
---
## Components
### Buttons
```html
<button class="lt-btn">Default</button>
<button class="lt-btn lt-btn-primary">Primary</button>
<button class="lt-btn lt-btn-danger">Delete</button>
<button class="lt-btn lt-btn-sm">Small</button>
<button class="lt-btn lt-btn-ghost">Ghost</button>
<a href="/create" class="lt-btn lt-btn-primary">Link Button</a>
```
All buttons:
- Transparent background, green border, `[ text ]` bracket decoration via `::before`/`::after`
- Hover → amber colour, `translateY(-2px)` lift, amber glow
- `border-radius: 0` — terminal style, no rounding
### Status Badges
```html
<span class="lt-status lt-status-open">Open</span>
<span class="lt-status lt-status-in-progress">In Progress</span>
<span class="lt-status lt-status-closed">Closed</span>
<span class="lt-status lt-status-online">Online</span>
<span class="lt-status lt-status-failed">Failed</span>
```
Spinning `◐` indicator on `lt-status-in-progress` and `lt-status-running`.
### Priority Badges
```html
<span class="lt-priority lt-p1">P1 Critical</span>
<span class="lt-priority lt-p2">P2 High</span>
<span class="lt-priority lt-p3">P3 Med</span>
<span class="lt-priority lt-p4">P4 Low</span>
<span class="lt-priority lt-p5">P5 Min</span>
```
P1 has a pulsing glow animation.
### Chips (compact inline status)
```html
<span class="lt-chip lt-chip-ok">OK</span>
<span class="lt-chip lt-chip-warn">Warn</span>
<span class="lt-chip lt-chip-critical">Critical</span>
<span class="lt-chip lt-chip-info">Info</span>
```
### Inline Messages
```html
<div class="lt-msg lt-msg-error">Something went wrong</div>
<div class="lt-msg lt-msg-success">Saved successfully</div>
<div class="lt-msg lt-msg-warning">Rate limit approaching</div>
<div class="lt-msg lt-msg-info">Auto-refresh every 30s</div>
```
### ASCII Frames
Outer frame (double-line, for major sections):
```html
<div class="lt-frame">
<span class="lt-frame-bl"></span>
<span class="lt-frame-br"></span>
<div class="lt-section-header">Section Title</div>
<div class="lt-section-body">
<!-- content -->
</div>
</div>
```
Inner frame (single-line, for sub-panels):
```html
<div class="lt-frame-inner">
<div class="lt-subsection-header">Sub Section</div>
<!-- content -->
</div>
```
The `::before` / `::after` pseudo-elements draw the top-left `╔`/`┌` and top-right `╗`/`┐` corners automatically. Bottom corners require explicit `<span>` children because CSS cannot target `:nth-pseudo-element`.
### Cards
```html
<div class="lt-card">
<div class="lt-card-title">Card Title</div>
<!-- content -->
</div>
<!-- Grid of cards -->
<div class="lt-grid"> <!-- auto-fill, min 300px -->
<div class="lt-grid-2"> <!-- 2 columns -->
<div class="lt-grid-3"> <!-- 3 columns -->
<div class="lt-grid-4"> <!-- 4 columns -->
```
### Tables
Full-border table (simple data, terminal look):
```html
<div class="lt-table-wrap">
<table class="lt-table" id="my-table">
<thead><tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="status">Status</th>
</tr></thead>
<tbody>
<tr class="lt-row-p1 lt-row-critical">
<td>ticket-001</td>
<td><span class="lt-status lt-status-open">Open</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Wire sorting -->
<script>lt.sortTable.init('my-table');</script>
```
Data table (compact, row-only separators, for dense data):
```html
<table class="lt-data-table"></table>
```
Row classes: `lt-row-p1``lt-row-p5` (priority left border) · `lt-row-critical` · `lt-row-warning` · `lt-row-selected` (keyboard nav highlight)
### Forms
```html
<div class="lt-form-group">
<label class="lt-label lt-label-required" for="title">Title</label>
<input id="title" type="text" class="lt-input" placeholder="…">
<span class="lt-form-hint">Markdown supported.</span>
</div>
<div class="lt-form-group">
<label class="lt-label" for="priority">Priority</label>
<select id="priority" class="lt-select">
<option>P1 — Critical</option>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label">
<input type="checkbox" class="lt-checkbox"> Confidential
</label>
</div>
<!-- Search with prompt prefix -->
<div class="lt-search">
<input type="search" class="lt-input lt-search-input" placeholder="Search…">
</div>
```
Labels are amber-coloured and uppercase. Inputs: green border, focus → amber border + glow pulse.
### Modals
```html
<!-- Trigger -->
<button class="lt-btn" data-modal-open="my-modal">Open Modal</button>
<!-- Modal -->
<div id="my-modal" class="lt-modal-overlay" aria-hidden="true">
<div class="lt-modal">
<div class="lt-modal-header">
<span class="lt-modal-title">Modal Title</span>
<button class="lt-modal-close" data-modal-close aria-label="Close"></button>
</div>
<div class="lt-modal-body">
<!-- content -->
</div>
<div class="lt-modal-footer">
<button class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
<button class="lt-btn lt-btn-primary">Confirm</button>
</div>
</div>
</div>
```
- `data-modal-open="id"` → opens the overlay
- `data-modal-close` → closes the nearest `.lt-modal-overlay`
- Click on backdrop (`.lt-modal-overlay`) → closes automatically
- ESC key → closes all open modals
### Tabs
```html
<div class="lt-tabs">
<button class="lt-tab active" data-tab="panel-a">Tab A</button>
<button class="lt-tab" data-tab="panel-b">Tab B</button>
</div>
<div id="panel-a" class="lt-tab-panel active"></div>
<div id="panel-b" class="lt-tab-panel"></div>
<script>lt.tabs.init();</script>
```
Active tab persists across page reloads via `localStorage`.
### Toast Notifications
```js
lt.toast.success('Ticket #123 saved');
lt.toast.error('Connection refused', 5000); // custom duration ms
lt.toast.warning('Rate limit 80% used');
lt.toast.info('Auto-refresh triggered');
```
Toasts appear bottom-right, stack vertically, auto-dismiss (default 3.5s), include an audible beep.
### Stats Widgets
```html
<div class="lt-stats-grid">
<div class="lt-stat-card" data-filter-key="status" data-filter-val="Open">
<span class="lt-stat-icon">📋</span>
<div class="lt-stat-info">
<span class="lt-stat-value">42</span>
<span class="lt-stat-label">Open</span>
</div>
</div>
</div>
```
Clicking a stat card calls `window.lt_onStatFilter(key, val)` — implement this function in your app JS to wire it to your filter logic.
### Sidebar
```html
<aside class="lt-sidebar" id="lt-sidebar">
<div class="lt-sidebar-header">
Filters
<button class="lt-sidebar-toggle" data-sidebar-toggle="lt-sidebar"></button>
</div>
<div class="lt-sidebar-body">
<div class="lt-filter-group">
<span class="lt-filter-label">Category</span>
<label class="lt-filter-option">
<input type="checkbox" class="lt-checkbox"> Hardware
</label>
</div>
</div>
</aside>
```
Collapse state persists across page reloads via `sessionStorage`.
### Boot Sequence
```html
<div id="lt-boot" class="lt-boot-overlay" data-app-name="MY APP" style="display:none">
<pre id="lt-boot-text" class="lt-boot-text"></pre>
</div>
```
Runs once per session. Shows animated ASCII boot sequence. Add `data-app-name` to customise the banner text.
---
## JavaScript API (`window.lt`)
```js
/* HTML safety */
lt.escHtml(str) // escape for innerHTML
/* Toasts */
lt.toast.success(msg, durationMs?)
lt.toast.error(msg, durationMs?)
lt.toast.warning(msg, durationMs?)
lt.toast.info(msg, durationMs?)
/* Audio */
lt.beep('success' | 'error' | 'info') // Web Audio API beep; silent-fails if unavailable
// Note: toasts automatically call lt.beep() — only call directly for non-toast events
/* Modals */
lt.modal.open('modal-id')
lt.modal.close('modal-id')
lt.modal.closeAll()
/* Tabs */
lt.tabs.init()
lt.tabs.switch('panel-id')
/* Boot */
lt.boot.run('APP NAME', forceFlag?)
/* Keyboard shortcuts */
lt.keys.on('ctrl+s', handler)
lt.keys.off('ctrl+s')
lt.keys.initDefaults() // ESC close, Ctrl+K focus search, ? help
/* Sidebar */
lt.sidebar.init()
/* CSRF */
lt.csrf.headers() // returns { 'X-CSRF-Token': token } or {}
/* Fetch helpers */
await lt.api.get('/api/tickets')
await lt.api.post('/api/update_ticket.php', { id: 1, status: 'Closed' })
await lt.api.put('/api/workflows/5', payload)
await lt.api.delete('/api/comment', { id: 9 })
// All throw Error on non-2xx with the server's error message
/* Time */
lt.time.ago(isoString) // "5m ago"
lt.time.uptime(seconds) // "14d 6h 3m"
lt.time.format(isoString) // locale datetime string
/* Bytes */
lt.bytes.format(1234567) // "1.18 MB"
/* Table utilities — must be called explicitly (not auto-initialized) */
lt.tableNav.init('table-id') // j/k/Enter keyboard navigation on <tbody> rows
lt.sortTable.init('table-id') // click-to-sort on <th data-sort-key> headers
/* Stats widget filtering */
lt.statsFilter.init() // wires data-filter-key clicks
/* Auto-refresh */
lt.autoRefresh.start(fn, 30000) // call fn every 30 s
lt.autoRefresh.stop()
lt.autoRefresh.now() // trigger immediately + restart timer
```
---
## CSRF Integration
### PHP (Tinker Tickets)
```php
<!-- In your view, using SecurityHeadersMiddleware nonce: -->
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
window.APP_TIMEZONE = '<?php echo $config['TIMEZONE']; ?>';
</script>
```
`lt.api.*` automatically includes the token as `X-CSRF-Token` header.
### Node.js (PULSE)
```js
// server.js: generate token and pass to template
res.render('index', { csrfToken: req.session.csrfToken });
```
```html
<!-- EJS template -->
<script nonce="<%= nonce %>">window.CSRF_TOKEN = '<%= csrfToken %>';</script>
```
### Flask (GANDALF)
GANDALF uses Authelia SSO with `SameSite=Strict` cookies so CSRF tokens are not required on API endpoints. For new apps using Flask-WTF:
```html
<script nonce="{{ nonce }}">window.CSRF_TOKEN = '{{ csrf_token() }}';</script>
```
---
## Auth Integration Patterns
### PHP — Authelia headers
```php
// AuthMiddleware.php
$user = [
'username' => $_SERVER['HTTP_REMOTE_USER'] ?? '',
'name' => $_SERVER['HTTP_REMOTE_NAME'] ?? '',
'email' => $_SERVER['HTTP_REMOTE_EMAIL'] ?? '',
'groups' => explode(',', $_SERVER['HTTP_REMOTE_GROUPS'] ?? ''),
'is_admin' => in_array('admin', explode(',', $_SERVER['HTTP_REMOTE_GROUPS'] ?? '')),
];
```
### Python/Flask — `@require_auth` decorator
```python
def require_auth(f):
@wraps(f)
def wrapper(*args, **kwargs):
user = request.headers.get('Remote-User', '')
if not user:
return '401 Not Authenticated', 401
groups = [g.strip() for g in request.headers.get('Remote-Groups','').split(',')]
if 'admin' not in groups:
return '403 Forbidden', 403
return f(*args, **kwargs)
return wrapper
```
### Node.js — Express middleware
```js
function requireAuth(req, res, next) {
const user = req.headers['remote-user'];
if (!user) return res.status(401).json({ error: 'Not authenticated' });
req.user = {
username: user,
name: req.headers['remote-name'] || user,
email: req.headers['remote-email'] || '',
groups: (req.headers['remote-groups'] || '').split(',').map(s => s.trim()),
isAdmin: (req.headers['remote-groups'] || '').split(',').includes('admin'),
};
next();
}
```
---
## Framework Skeleton Files
| File | Framework | Description |
|---|---|---|
| `php/layout.php` | PHP / Tinker Tickets | Complete page layout with CSP nonce, CSRF, session |
| `python/base.html` | Flask / Jinja2 | Jinja2 template extending pattern |
| `python/auth.py` | Flask | `@require_auth` decorator + `_get_user()` |
| `node/middleware.js` | Node.js / Express | Auth + CSRF + CSP nonce middleware |
---
## Directory Layout for New Apps
```
my-app/
├── assets/
│ ├── css/
│ │ └── app.css # Extends base.css — app-specific rules only
│ ├── js/
│ │ └── app.js # App logic — uses lt.* from base.js
│ └── images/
│ └── favicon.png
├── (php|python|node) files…
└── README.md # Link back to web_template/README.md for styling docs
```
---
## Responsive Breakpoints
| Breakpoint | Width | Changes |
|---|---|---|
| XL | > 1400px | Full layout, 4-col grids |
| LG | ≤ 1400px | 3-col grids, 4-col stats |
| MD | ≤ 1200px | 2-col grids, narrow sidebar |
| SM (tablet) | ≤ 1024px | Sidebar stacks below content |
| XS (phone) | ≤ 768px | Nav hidden, single-col |
| XXS | ≤ 480px | Minimal padding, 2-col stats |
---
## Key Design Rules
1. **No rounded corners**`border-radius: 0` everywhere
2. **No drop shadows** — only glow halos (`box-shadow: 0 0 Xpx color`)
3. **No sans-serif fonts** — 100% monospace stack
4. **ASCII borders** — box-drawing chars via `::before`/`::after`, never images
5. **All uppercase** — headings, labels, nav links, button text
6. **`[ brackets ]`** — buttons and nav links wrap their text in `[ ]`
7. **Green = primary** — informational / active state
8. **Amber = secondary** — headings, highlights, hover state
9. **Cyan = tertiary** — uplinks, info, POE active, prompts
10. **Red = critical** — errors, P1 priority, offline state
11. **Glow on everything critical**`text-shadow` glow stacks for P1 and errors
12. **Amber form labels** — labels are amber, inputs are green
13. **Scanlines always on** — the `body::before` CRT overlay is non-negotiable
---
## Accessibility Notes
- All interactive elements have `:focus-visible` outlines (amber, 2px)
- `@media (prefers-reduced-motion)` disables all animations
- Screen-reader utility class: `.lt-sr-only`
- Modals set `aria-hidden` and trap focus
- Status elements use semantic colour + text (not colour alone)
- Minimum contrast: text-muted `#00bb33` on `#0a0a0a` ≈ 5.3:1 (WCAG AA ✓)