590 lines
16 KiB
Markdown
590 lines
16 KiB
Markdown
|
|
# 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
|
|||
|
|
|
|||
|
|
Copy three files into your app's static/public directory:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
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)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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?)
|
|||
|
|
|
|||
|
|
/* 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 */
|
|||
|
|
lt.tableNav.init('table-id') // j/k/Enter keyboard navigation
|
|||
|
|
lt.sortTable.init('table-id') // click-to-sort on 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 ✓)
|