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:

<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.

.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:

/* Example: app-specific accent on top of base */
:root {
  --terminal-amber: #ffd000;   /* slightly brighter amber */
}

Key Variables

/* 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

<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

<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

<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)

<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

<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):

<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):

<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

<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):

<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):

<table class="lt-data-table"></table>

Row classes: lt-row-p1lt-row-p5 (priority left border) · lt-row-critical · lt-row-warning · lt-row-selected (keyboard nav highlight)

Forms

<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

<!-- 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

<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

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

<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

<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

<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)

/* 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)

<!-- 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)

// server.js: generate token and pass to template
res.render('index', { csrfToken: req.session.csrfToken });
<!-- 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:

<script nonce="{{ nonce }}">window.CSRF_TOKEN = '{{ csrf_token() }}';</script>

Auth Integration Patterns

PHP — Authelia headers

// 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

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

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 cornersborder-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 criticaltext-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 ✓)
Description
No description provided
Readme 105 KiB
Languages
CSS 39.3%
HTML 27.1%
JavaScript 26.7%
PHP 4.7%
Python 2.2%