diff --git a/docs/UI_GUIDE.md b/docs/UI_GUIDE.md new file mode 100644 index 0000000..cbd2ff2 --- /dev/null +++ b/docs/UI_GUIDE.md @@ -0,0 +1,608 @@ +# iNTERCEPT UI Guide + +This guide documents the UI design system, components, and patterns used in iNTERCEPT. + +## Table of Contents + +1. [Design Tokens](#design-tokens) +2. [Base Templates](#base-templates) +3. [Navigation](#navigation) +4. [Components](#components) +5. [Adding a New Module Page](#adding-a-new-module-page) +6. [Adding a New Dashboard](#adding-a-new-dashboard) + +--- + +## Design Tokens + +All design tokens are defined in `static/css/core/variables.css`. Import this file first in any stylesheet. + +### Colors + +```css +/* Backgrounds (layered depth) */ +--bg-primary: #0a0c10; /* Darkest - page background */ +--bg-secondary: #0f1218; /* Panels, sidebars */ +--bg-tertiary: #151a23; /* Cards, elevated elements */ +--bg-card: #121620; /* Card backgrounds */ +--bg-elevated: #1a202c; /* Hover states, modals */ + +/* Accent Colors */ +--accent-cyan: #4a9eff; /* Primary action color */ +--accent-green: #22c55e; /* Success, online status */ +--accent-red: #ef4444; /* Error, danger, stop */ +--accent-orange: #f59e0b; /* Warning */ +--accent-amber: #d4a853; /* Secondary highlight */ + +/* Text Hierarchy */ +--text-primary: #e8eaed; /* Main content */ +--text-secondary: #9ca3af; /* Secondary content */ +--text-dim: #4b5563; /* Disabled, placeholder */ +--text-muted: #374151; /* Barely visible */ + +/* Status Colors */ +--status-online: #22c55e; +--status-warning: #f59e0b; +--status-error: #ef4444; +--status-offline: #6b7280; +``` + +### Spacing Scale + +```css +--space-1: 4px; +--space-2: 8px; +--space-3: 12px; +--space-4: 16px; +--space-5: 20px; +--space-6: 24px; +--space-8: 32px; +--space-10: 40px; +--space-12: 48px; +--space-16: 64px; +``` + +### Typography + +```css +/* Font Families */ +--font-sans: 'Inter', -apple-system, sans-serif; +--font-mono: 'JetBrains Mono', monospace; + +/* Font Sizes */ +--text-xs: 10px; +--text-sm: 12px; +--text-base: 14px; +--text-lg: 16px; +--text-xl: 18px; +--text-2xl: 20px; +--text-3xl: 24px; +--text-4xl: 30px; +``` + +### Border Radius + +```css +--radius-sm: 4px; +--radius-md: 6px; +--radius-lg: 8px; +--radius-xl: 12px; +--radius-full: 9999px; +``` + +### Light Theme + +The design system supports light/dark themes via `data-theme` attribute: + +```html + +``` + +Toggle with JavaScript: +```javascript +document.documentElement.setAttribute('data-theme', 'light'); +``` + +--- + +## Base Templates + +### `templates/layout/base.html` + +The main base template for standard pages. Use for pages with sidebar + content layout. + +```html +{% extends 'layout/base.html' %} + +{% block title %}My Page Title{% endblock %} + +{% block styles %} + +{% endblock %} + +{% block navigation %} +{% set active_mode = 'mymode' %} +{% include 'partials/nav.html' %} +{% endblock %} + +{% block sidebar %} +
+ +
+{% endblock %} + +{% block content %} +
+

Page Title

+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} +``` + +### `templates/layout/base_dashboard.html` + +Extended base for full-screen dashboards (maps, visualizations). + +```html +{% extends 'layout/base_dashboard.html' %} + +{% set active_mode = 'mydashboard' %} + +{% block dashboard_title %}MY DASHBOARD{% endblock %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block stats_strip %} +
+ +
+{% endblock %} + +{% block dashboard_content %} +
+ +
+
+ +
+{% endblock %} +``` + +--- + +## Navigation + +### Including Navigation + +```html +{% set active_mode = 'pager' %} +{% include 'partials/nav.html' %} +``` + +### Valid `active_mode` Values + +| Mode | Description | +|------|-------------| +| `pager` | Pager decoding | +| `sensor` | 433MHz sensors | +| `rtlamr` | Utility meters | +| `adsb` | Aircraft tracking | +| `ais` | Vessel tracking | +| `aprs` | Amateur radio | +| `wifi` | WiFi scanning | +| `bluetooth` | Bluetooth scanning | +| `tscm` | Counter-surveillance | +| `satellite` | Satellite tracking | +| `sstv` | ISS SSTV | +| `listening` | Listening post | +| `spystations` | Spy stations | +| `meshtastic` | Mesh networking | + +### Navigation Groups + +The navigation is organized into groups: +- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic +- **Wireless**: WiFi, Bluetooth +- **Security**: TSCM +- **Space**: Satellite, ISS SSTV + +--- + +## Components + +### Card / Panel + +```html +{% call card(title='PANEL TITLE', indicator=true, indicator_active=false) %} +

Panel content here

+{% endcall %} +``` + +Or manually: +```html +
+
+ PANEL TITLE +
+
+
+

Content here

+
+
+``` + +### Empty State + +```html +{% include 'components/empty_state.html' with context %} +{# Or with variables: #} +{% with title='No data yet', description='Start scanning to see results', action_text='Start Scan', action_onclick='startScan()' %} +{% include 'components/empty_state.html' %} +{% endwith %} +``` + +### Loading State + +```html +{# Inline spinner #} +{% include 'components/loading.html' %} + +{# With text #} +{% with text='Loading data...', size='lg' %} +{% include 'components/loading.html' %} +{% endwith %} + +{# Full overlay #} +{% with overlay=true, text='Please wait...' %} +{% include 'components/loading.html' %} +{% endwith %} +``` + +### Status Badge + +```html +{% with status='online', text='Connected', id='connectionStatus' %} +{% include 'components/status_badge.html' %} +{% endwith %} +``` + +Status values: `online`, `offline`, `warning`, `error`, `inactive` + +### Buttons + +```html + + + + + + + + + + + + + + + + + + +``` + +### Badges + +```html +Default +Primary +Online +Warning +Error +``` + +### Form Groups + +```html +
+ + + Enter frequency in MHz +
+ +
+ + +
+ + +``` + +### Stats Strip + +Used in dashboards for horizontal statistics display: + +```html +
+
+
+ 0 + COUNT +
+
+
+
+ TRACKING +
+
--:--:-- UTC
+
+
+``` + +--- + +## Adding a New Module Page + +### 1. Create the Route + +In `routes/mymodule.py`: + +```python +from flask import Blueprint, render_template + +mymodule_bp = Blueprint('mymodule', __name__, url_prefix='/mymodule') + +@mymodule_bp.route('/dashboard') +def dashboard(): + return render_template('mymodule_dashboard.html', + offline_settings=get_offline_settings()) +``` + +### 2. Register the Blueprint + +In `routes/__init__.py`: + +```python +from routes.mymodule import mymodule_bp +app.register_blueprint(mymodule_bp) +``` + +### 3. Create the Template + +Option A: Simple page extending base.html +```html +{% extends 'layout/base.html' %} +{% set active_mode = 'mymodule' %} + +{% block title %}My Module{% endblock %} + +{% block navigation %} +{% include 'partials/nav.html' %} +{% endblock %} + +{% block content %} + +{% endblock %} +``` + +Option B: Full-screen dashboard +```html +{% extends 'layout/base_dashboard.html' %} +{% set active_mode = 'mymodule' %} + +{% block dashboard_title %}MY MODULE{% endblock %} + +{% block dashboard_content %} + +{% endblock %} +``` + +### 4. Add to Navigation + +In `templates/partials/nav.html`, add your module to the appropriate group: + +```html + +``` + +Or if it's a dashboard link: +```html + + + My Module + +``` + +### 5. Create Stylesheet + +In `static/css/mymodule.css`: + +```css +/** + * My Module Styles + */ +@import url('./core/variables.css'); + +/* Your styles using design tokens */ +.mymodule-container { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-4); +} +``` + +--- + +## Adding a New Dashboard + +For full-screen dashboards like ADSB, AIS, or Satellite: + +### 1. Create the Template + +```html + + + + + + MY DASHBOARD // iNTERCEPT + + + + + + + {% if offline_settings.fonts_source == 'local' %} + + {% else %} + + {% endif %} + + + + + + + + + + + +
+
+ + +
+ +
+ Back + Main Dashboard +
+
+ + + {% set active_mode = 'mydashboard' %} + {% include 'partials/nav.html' %} + + +
+ +
+ + +
+ +
+ + + + +``` + +### 2. Create the Stylesheet + +```css +/** + * My Dashboard Styles + */ +@import url('./core/variables.css'); + +:root { + /* Dashboard-specific aliases */ + --bg-dark: var(--bg-primary); + --bg-panel: var(--bg-secondary); + --bg-card: var(--bg-tertiary); + --grid-line: rgba(74, 158, 255, 0.08); +} + +/* Your dashboard styles */ +``` + +--- + +## Best Practices + +### DO + +- Use design tokens for all colors, spacing, and typography +- Include the nav partial on all pages for consistent navigation +- Set `active_mode` before including the nav partial +- Use semantic component classes (`btn`, `panel`, `badge`, etc.) +- Support both light and dark themes +- Test on mobile viewports + +### DON'T + +- Hardcode color values - use CSS variables +- Create new color variations without adding to tokens +- Duplicate navigation markup - use the partial +- Skip the favicon and design tokens imports +- Use inline styles for layout (use utility classes) + +--- + +## File Structure + +``` +templates/ +├── layout/ +│ ├── base.html # Standard page base +│ └── base_dashboard.html # Dashboard page base +├── partials/ +│ ├── nav.html # Unified navigation +│ ├── page_header.html # Page title component +│ └── settings-modal.html # Settings modal +├── components/ +│ ├── card.html # Panel/card component +│ ├── empty_state.html # Empty state placeholder +│ ├── loading.html # Loading spinner +│ ├── stats_strip.html # Stats bar component +│ └── status_badge.html # Status indicator +├── index.html # Main dashboard +├── adsb_dashboard.html # Aircraft tracking +├── ais_dashboard.html # Vessel tracking +└── satellite_dashboard.html # Satellite tracking + +static/css/ +├── core/ +│ ├── variables.css # Design tokens +│ ├── base.css # Reset & typography +│ ├── components.css # Component styles +│ └── layout.css # Layout styles +├── index.css # Main dashboard styles +├── adsb_dashboard.css # Aircraft dashboard +├── ais_dashboard.css # Vessel dashboard +├── satellite_dashboard.css # Satellite dashboard +└── responsive.css # Responsive breakpoints +``` diff --git a/static/css/components/function-strip.css b/static/css/components/function-strip.css new file mode 100644 index 0000000..492be77 --- /dev/null +++ b/static/css/components/function-strip.css @@ -0,0 +1,371 @@ +/* Function Strip (Action Bar) - Shared across modes + * Based on APRS strip pattern, reusable for Pager, Sensor, Bluetooth, WiFi, TSCM, etc. + */ + +.function-strip { + background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 8px 12px; + margin-bottom: 10px; + overflow: visible; + min-height: 44px; +} + +.function-strip-inner { + display: flex; + align-items: center; + gap: 8px; + min-width: max-content; +} + +/* Stats */ +.function-strip .strip-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 6px 10px; + background: rgba(74, 158, 255, 0.05); + border: 1px solid rgba(74, 158, 255, 0.15); + border-radius: 4px; + min-width: 55px; +} + +.function-strip .strip-stat:hover { + background: rgba(74, 158, 255, 0.1); + border-color: rgba(74, 158, 255, 0.3); +} + +.function-strip .strip-value { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + font-weight: 600; + color: var(--accent-cyan); + line-height: 1.2; +} + +.function-strip .strip-label { + font-size: 8px; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 1px; +} + +.function-strip .strip-divider { + width: 1px; + height: 28px; + background: var(--border-color); + margin: 0 4px; +} + +/* Signal stat coloring */ +.function-strip .signal-stat.good .strip-value { color: var(--accent-green); } +.function-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); } +.function-strip .signal-stat.poor .strip-value { color: var(--accent-red); } + +/* Controls */ +.function-strip .strip-control { + display: flex; + align-items: center; + gap: 4px; +} + +.function-strip .strip-select { + background: rgba(0,0,0,0.3); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + cursor: pointer; +} + +.function-strip .strip-select:hover { + border-color: var(--accent-cyan); +} + +.function-strip .strip-select:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.function-strip .strip-input-label { + font-size: 9px; + color: var(--text-muted); + font-weight: 600; +} + +.function-strip .strip-input { + background: rgba(0,0,0,0.3); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 4px 6px; + border-radius: 4px; + font-size: 10px; + width: 50px; + text-align: center; +} + +.function-strip .strip-input:hover, +.function-strip .strip-input:focus { + border-color: var(--accent-cyan); + outline: none; +} + +.function-strip .strip-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Wider input for frequency values */ +.function-strip .strip-input.wide { + width: 70px; +} + +/* Tool Status Indicators */ +.function-strip .strip-tools { + display: flex; + gap: 4px; +} + +.function-strip .strip-tool { + font-size: 9px; + font-weight: 600; + padding: 3px 6px; + border-radius: 3px; + background: rgba(255, 59, 48, 0.2); + color: var(--accent-red); + border: 1px solid rgba(255, 59, 48, 0.3); +} + +.function-strip .strip-tool.ok { + background: rgba(0, 255, 136, 0.1); + color: var(--accent-green); + border-color: rgba(0, 255, 136, 0.3); +} + +.function-strip .strip-tool.warn { + background: rgba(255, 193, 7, 0.2); + color: var(--accent-yellow); + border-color: rgba(255, 193, 7, 0.3); +} + +/* Buttons */ +.function-strip .strip-btn { + background: rgba(74, 158, 255, 0.1); + border: 1px solid rgba(74, 158, 255, 0.2); + color: var(--text-primary); + padding: 6px 12px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.function-strip .strip-btn:hover:not(:disabled) { + background: rgba(74, 158, 255, 0.2); + border-color: rgba(74, 158, 255, 0.4); +} + +.function-strip .strip-btn.primary { + background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%); + border: none; + color: #000; +} + +.function-strip .strip-btn.primary:hover:not(:disabled) { + filter: brightness(1.1); +} + +.function-strip .strip-btn.stop { + background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%); + border: none; + color: #fff; +} + +.function-strip .strip-btn.stop:hover:not(:disabled) { + filter: brightness(1.1); +} + +.function-strip .strip-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Status indicator */ +.function-strip .strip-status { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(0,0,0,0.2); + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: var(--text-secondary); +} + +.function-strip .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); +} + +.function-strip .status-dot.inactive { + background: var(--text-muted); +} + +.function-strip .status-dot.active, +.function-strip .status-dot.scanning, +.function-strip .status-dot.decoding { + background: var(--accent-cyan); + animation: strip-pulse 1.5s ease-in-out infinite; +} + +.function-strip .status-dot.listening, +.function-strip .status-dot.tracking, +.function-strip .status-dot.receiving { + background: var(--accent-green); + animation: strip-pulse 1.5s ease-in-out infinite; +} + +.function-strip .status-dot.sweeping { + background: var(--accent-orange); + animation: strip-pulse 1s ease-in-out infinite; +} + +.function-strip .status-dot.error { + background: var(--accent-red); +} + +@keyframes strip-pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; } + 50% { opacity: 0.6; box-shadow: none; } +} + +/* Time display */ +.function-strip .strip-time { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-muted); + padding: 4px 8px; + background: rgba(0,0,0,0.2); + border-radius: 4px; + white-space: nowrap; +} + +/* Mode-specific accent colors */ +.function-strip.pager-strip .strip-stat { + background: rgba(255, 193, 7, 0.05); + border-color: rgba(255, 193, 7, 0.15); +} +.function-strip.pager-strip .strip-stat:hover { + background: rgba(255, 193, 7, 0.1); + border-color: rgba(255, 193, 7, 0.3); +} +.function-strip.pager-strip .strip-value { + color: var(--accent-yellow); +} + +.function-strip.sensor-strip .strip-stat { + background: rgba(0, 255, 136, 0.05); + border-color: rgba(0, 255, 136, 0.15); +} +.function-strip.sensor-strip .strip-stat:hover { + background: rgba(0, 255, 136, 0.1); + border-color: rgba(0, 255, 136, 0.3); +} +.function-strip.sensor-strip .strip-value { + color: var(--accent-green); +} + +.function-strip.bt-strip .strip-stat { + background: rgba(0, 122, 255, 0.05); + border-color: rgba(0, 122, 255, 0.15); +} +.function-strip.bt-strip .strip-stat:hover { + background: rgba(0, 122, 255, 0.1); + border-color: rgba(0, 122, 255, 0.3); +} +.function-strip.bt-strip .strip-value { + color: #0a84ff; +} + +.function-strip.wifi-strip .strip-stat { + background: rgba(255, 149, 0, 0.05); + border-color: rgba(255, 149, 0, 0.15); +} +.function-strip.wifi-strip .strip-stat:hover { + background: rgba(255, 149, 0, 0.1); + border-color: rgba(255, 149, 0, 0.3); +} +.function-strip.wifi-strip .strip-value { + color: var(--accent-orange); +} + +.function-strip.tscm-strip { + margin-top: 4px; /* Extra clearance to prevent top clipping */ +} + +.function-strip.tscm-strip .strip-stat { + background: rgba(255, 59, 48, 0.15); + border: 1px solid rgba(255, 59, 48, 0.4); +} +.function-strip.tscm-strip .strip-stat:hover { + background: rgba(255, 59, 48, 0.25); + border-color: rgba(255, 59, 48, 0.6); +} +.function-strip.tscm-strip .strip-value { + color: #ef4444; /* Explicit red color */ +} +.function-strip.tscm-strip .strip-label { + color: #9ca3af; /* Explicit light gray */ +} +.function-strip.tscm-strip .strip-select { + color: #e8eaed; /* Explicit white for selects */ + background: rgba(0, 0, 0, 0.4); +} +.function-strip.tscm-strip .strip-btn { + color: #e8eaed; /* Explicit white for buttons */ +} +.function-strip.tscm-strip .strip-tool { + color: #e8eaed; /* Explicit white for tool indicators */ +} +.function-strip.tscm-strip .strip-time, +.function-strip.tscm-strip .strip-status span { + color: #9ca3af; /* Explicit gray for status/time */ +} + +.function-strip.rtlamr-strip .strip-stat { + background: rgba(175, 82, 222, 0.05); + border-color: rgba(175, 82, 222, 0.15); +} +.function-strip.rtlamr-strip .strip-stat:hover { + background: rgba(175, 82, 222, 0.1); + border-color: rgba(175, 82, 222, 0.3); +} +.function-strip.rtlamr-strip .strip-value { + color: #af52de; +} + +.function-strip.listening-strip .strip-stat { + background: rgba(74, 158, 255, 0.05); + border-color: rgba(74, 158, 255, 0.15); +} +.function-strip.listening-strip .strip-stat:hover { + background: rgba(74, 158, 255, 0.1); + border-color: rgba(74, 158, 255, 0.3); +} +.function-strip.listening-strip .strip-value { + color: var(--accent-cyan); +} + +/* Threat-colored stats for TSCM */ +.function-strip .strip-stat.threat-high .strip-value { color: var(--accent-red); } +.function-strip .strip-stat.threat-review .strip-value { color: var(--accent-orange); } +.function-strip .strip-stat.threat-info .strip-value { color: var(--accent-cyan); } diff --git a/static/css/core/base.css b/static/css/core/base.css new file mode 100644 index 0000000..b6f9a81 --- /dev/null +++ b/static/css/core/base.css @@ -0,0 +1,420 @@ +/** + * INTERCEPT Base Styles + * Reset, typography, and foundational element styles + * Requires: variables.css to be imported first + */ + +/* ============================================ + CSS RESET + ============================================ */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + tab-size: 4; +} + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: var(--leading-normal); + color: var(--text-primary); + background: var(--bg-primary); + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ============================================ + TYPOGRAPHY + ============================================ */ +h1, h2, h3, h4, h5, h6 { + font-weight: var(--font-semibold); + line-height: var(--leading-tight); + color: var(--text-primary); +} + +h1 { font-size: var(--text-4xl); } +h2 { font-size: var(--text-3xl); } +h3 { font-size: var(--text-2xl); } +h4 { font-size: var(--text-xl); } +h5 { font-size: var(--text-lg); } +h6 { font-size: var(--text-base); } + +p { + margin-bottom: var(--space-4); +} + +a { + color: var(--accent-cyan); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--accent-cyan-hover); +} + +a:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; +} + +strong, b { + font-weight: var(--font-semibold); +} + +small { + font-size: var(--text-sm); +} + +code, kbd, pre, samp { + font-family: var(--font-mono); + font-size: 0.9em; +} + +code { + background: var(--bg-tertiary); + padding: 2px 6px; + border-radius: var(--radius-sm); +} + +pre { + background: var(--bg-tertiary); + padding: var(--space-4); + border-radius: var(--radius-md); + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +/* ============================================ + FORM ELEMENTS + ============================================ */ +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; +} + +button { + cursor: pointer; + border: none; + background: none; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +input, +select, +textarea { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-3); + color: var(--text-primary); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent-cyan); + box-shadow: 0 0 0 3px var(--accent-cyan-dim); +} + +input::placeholder, +textarea::placeholder { + color: var(--text-dim); +} + +select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + padding-right: 28px; +} + +input[type="checkbox"], +input[type="radio"] { + width: 16px; + height: 16px; + padding: 0; + cursor: pointer; + accent-color: var(--accent-cyan); +} + +label { + display: block; + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); + margin-bottom: var(--space-1); +} + +/* ============================================ + TABLES + ============================================ */ +table { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); +} + +th, +td { + padding: var(--space-2) var(--space-3); + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + font-weight: var(--font-semibold); + color: var(--text-secondary); + background: var(--bg-secondary); + text-transform: uppercase; + font-size: var(--text-xs); + letter-spacing: 0.05em; +} + +tr:hover td { + background: var(--bg-tertiary); +} + +/* ============================================ + LISTS + ============================================ */ +ul, ol { + padding-left: var(--space-6); + margin-bottom: var(--space-4); +} + +li { + margin-bottom: var(--space-1); +} + +/* ============================================ + UTILITY CLASSES + ============================================ */ + +/* Text colors */ +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-muted { color: var(--text-muted); } +.text-cyan { color: var(--accent-cyan); } +.text-green { color: var(--accent-green); } +.text-red { color: var(--accent-red); } +.text-orange { color: var(--accent-orange); } +.text-amber { color: var(--accent-amber); } + +/* Font utilities */ +.font-mono { font-family: var(--font-mono); } +.font-medium { font-weight: var(--font-medium); } +.font-semibold { font-weight: var(--font-semibold); } +.font-bold { font-weight: var(--font-bold); } + +/* Text sizes */ +.text-xs { font-size: var(--text-xs); } +.text-sm { font-size: var(--text-sm); } +.text-base { font-size: var(--text-base); } +.text-lg { font-size: var(--text-lg); } +.text-xl { font-size: var(--text-xl); } + +/* Display */ +.hidden { display: none !important; } +.block { display: block; } +.inline-block { display: inline-block; } +.flex { display: flex; } +.inline-flex { display: inline-flex; } +.grid { display: grid; } + +/* Flexbox */ +.items-center { align-items: center; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.flex-1 { flex: 1; } +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } + +/* Spacing */ +.m-0 { margin: 0; } +.mt-2 { margin-top: var(--space-2); } +.mt-4 { margin-top: var(--space-4); } +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } + +/* Borders */ +.rounded { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.border { border: 1px solid var(--border-color); } + +/* Truncate text */ +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Screen reader only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ============================================ + SCROLLBAR STYLING + ============================================ */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-light); + border-radius: var(--radius-full); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-dim); +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: var(--border-light) var(--bg-secondary); +} + +/* ============================================ + SELECTION + ============================================ */ +::selection { + background: var(--accent-cyan-dim); + color: var(--text-primary); +} + +/* ============================================ + UX POLISH - TRANSITIONS & INTERACTIONS + ============================================ */ + +/* Smooth page transitions */ +html { + scroll-behavior: smooth; +} + +/* Better focus ring for all interactive elements */ +:focus-visible { + outline: 2px solid var(--accent-cyan); + outline-offset: 2px; +} + +/* Remove focus ring for mouse users */ +:focus:not(:focus-visible) { + outline: none; +} + +/* Active state feedback */ +button:active:not(:disabled), +a:active, +[role="button"]:active { + transform: scale(0.98); +} + +/* Smooth transitions for all interactive elements */ +button, +a, +input, +select, +textarea, +[role="button"] { + transition: + color var(--transition-fast), + background-color var(--transition-fast), + border-color var(--transition-fast), + box-shadow var(--transition-fast), + transform var(--transition-fast), + opacity var(--transition-fast); +} + +/* Subtle hover lift effect for cards and panels */ +.card:hover, +.panel:hover { + box-shadow: var(--shadow-md); +} + +/* Link underline on hover */ +a:hover { + text-decoration: underline; + text-underline-offset: 2px; +} + +/* Skip link for accessibility */ +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--accent-cyan); + color: var(--bg-primary); + padding: var(--space-2) var(--space-4); + z-index: 9999; + transition: top var(--transition-fast); +} + +.skip-link:focus { + top: 0; +} + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --border-color: #4b5563; + --text-secondary: #d1d5db; + } +} diff --git a/static/css/core/components.css b/static/css/core/components.css new file mode 100644 index 0000000..1d4f576 --- /dev/null +++ b/static/css/core/components.css @@ -0,0 +1,723 @@ +/** + * INTERCEPT UI Components + * Reusable component styles for buttons, cards, badges, etc. + * Requires: variables.css and base.css + */ + +/* ============================================ + BUTTONS + ============================================ */ + +/* Base button */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--font-medium); + border-radius: var(--radius-md); + border: 1px solid transparent; + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; + text-decoration: none; +} + +.btn:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Button variants */ +.btn-primary { + background: var(--accent-cyan); + color: var(--text-inverse); + border-color: var(--accent-cyan); +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-cyan-hover); + border-color: var(--accent-cyan-hover); +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border-color: var(--border-color); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--bg-elevated); + border-color: var(--border-light); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border-color: transparent; +} + +.btn-ghost:hover:not(:disabled) { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.btn-danger { + background: var(--accent-red); + color: white; + border-color: var(--accent-red); +} + +.btn-danger:hover:not(:disabled) { + background: #dc2626; + border-color: #dc2626; +} + +.btn-success { + background: var(--accent-green); + color: white; + border-color: var(--accent-green); +} + +.btn-success:hover:not(:disabled) { + background: #16a34a; + border-color: #16a34a; +} + +/* Button sizes */ +.btn-sm { + padding: var(--space-1) var(--space-2); + font-size: var(--text-xs); +} + +.btn-lg { + padding: var(--space-3) var(--space-6); + font-size: var(--text-base); +} + +/* Icon button */ +.btn-icon { + padding: var(--space-2); + width: 36px; + height: 36px; +} + +.btn-icon.btn-sm { + width: 28px; + height: 28px; + padding: var(--space-1); +} + +/* ============================================ + CARDS / PANELS + ============================================ */ +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +.card-header-title { + font-size: var(--text-xs); + font-weight: var(--font-semibold); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); +} + +.card-body { + padding: var(--space-4); +} + +.card-footer { + padding: var(--space-3) var(--space-4); + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +/* Panel variant (used in dashboards) */ +.panel { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-2) var(--space-3); + border-bottom: 1px solid var(--border-color); + background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%); + font-size: var(--text-xs); + font-weight: var(--font-semibold); + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-secondary); +} + +.panel-indicator { + width: 8px; + height: 8px; + border-radius: var(--radius-full); + background: var(--status-offline); +} + +.panel-indicator.active { + background: var(--status-online); + box-shadow: 0 0 8px var(--status-online); +} + +.panel-content { + padding: var(--space-3); +} + +/* ============================================ + BADGES + ============================================ */ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 2px var(--space-2); + font-size: var(--text-xs); + font-weight: var(--font-medium); + border-radius: var(--radius-full); + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.badge-primary { + background: var(--accent-cyan-dim); + color: var(--accent-cyan); +} + +.badge-success { + background: var(--accent-green-dim); + color: var(--accent-green); +} + +.badge-warning { + background: var(--accent-orange-dim); + color: var(--accent-orange); +} + +.badge-danger { + background: var(--accent-red-dim); + color: var(--accent-red); +} + +/* ============================================ + STATUS INDICATORS + ============================================ */ +.status-dot { + width: 8px; + height: 8px; + border-radius: var(--radius-full); + background: var(--status-offline); + flex-shrink: 0; +} + +.status-dot.online, +.status-dot.active { + background: var(--status-online); + box-shadow: 0 0 6px var(--status-online); +} + +.status-dot.warning { + background: var(--status-warning); + box-shadow: 0 0 6px var(--status-warning); +} + +.status-dot.error, +.status-dot.offline { + background: var(--status-error); +} + +.status-dot.inactive { + background: var(--status-offline); +} + +/* Pulse animation for active status */ +.status-dot.pulse { + animation: statusPulse 2s ease-in-out infinite; +} + +@keyframes statusPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* ============================================ + EMPTY STATE + ============================================ */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-8) var(--space-4); + text-align: center; + color: var(--text-muted); +} + +.empty-state-icon { + width: 48px; + height: 48px; + margin-bottom: var(--space-4); + opacity: 0.5; +} + +.empty-state-title { + font-size: var(--text-base); + font-weight: var(--font-medium); + color: var(--text-secondary); + margin-bottom: var(--space-2); +} + +.empty-state-description { + font-size: var(--text-sm); + color: var(--text-dim); + max-width: 300px; +} + +.empty-state-action { + margin-top: var(--space-4); +} + +/* ============================================ + LOADING STATES + ============================================ */ +.spinner { + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + border-top-color: var(--accent-cyan); + border-radius: var(--radius-full); + animation: spin 0.8s linear infinite; +} + +.spinner-sm { + width: 14px; + height: 14px; + border-width: 2px; +} + +.spinner-lg { + width: 32px; + height: 32px; + border-width: 3px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Loading overlay */ +.loading-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-overlay); + z-index: var(--z-modal); +} + +/* Skeleton loader */ +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 25%, + var(--bg-elevated) 50%, + var(--bg-tertiary) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-sm); +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ============================================ + STATS STRIP + ============================================ */ +.stats-strip { + display: flex; + align-items: center; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + padding: 0 var(--space-4); + height: var(--stats-strip-height); + overflow-x: auto; + gap: var(--space-1); +} + +.strip-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 var(--space-3); + min-width: fit-content; +} + +.strip-value { + font-family: var(--font-mono); + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--accent-cyan); + line-height: 1; +} + +.strip-label { + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; + margin-top: 2px; +} + +.strip-divider { + width: 1px; + height: 20px; + background: var(--border-color); + margin: 0 var(--space-2); +} + +/* ============================================ + FORM GROUPS + ============================================ */ +.form-group { + margin-bottom: var(--space-4); +} + +.form-group label { + display: block; + margin-bottom: var(--space-1); + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; +} + +.form-help { + margin-top: var(--space-1); + font-size: var(--text-xs); + color: var(--text-dim); +} + +.form-error { + margin-top: var(--space-1); + font-size: var(--text-xs); + color: var(--accent-red); +} + +/* Inline checkbox/radio */ +.form-check { + display: flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; +} + +.form-check input { + width: auto; +} + +.form-check label { + margin-bottom: 0; + cursor: pointer; +} + +/* ============================================ + ALERTS / TOASTS + ============================================ */ +.alert { + display: flex; + align-items: flex-start; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-md); + border: 1px solid; + font-size: var(--text-sm); +} + +.alert-info { + background: var(--accent-cyan-dim); + border-color: var(--accent-cyan); + color: var(--accent-cyan); +} + +.alert-success { + background: var(--accent-green-dim); + border-color: var(--accent-green); + color: var(--accent-green); +} + +.alert-warning { + background: var(--accent-orange-dim); + border-color: var(--accent-orange); + color: var(--accent-orange); +} + +.alert-danger { + background: var(--accent-red-dim); + border-color: var(--accent-red); + color: var(--accent-red); +} + +/* ============================================ + TOOLTIPS + ============================================ */ +[data-tooltip] { + position: relative; +} + +[data-tooltip]::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: var(--space-1) var(--space-2); + background: var(--bg-elevated); + color: var(--text-primary); + font-size: var(--text-xs); + border-radius: var(--radius-sm); + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-fast), visibility var(--transition-fast); + z-index: var(--z-tooltip); + pointer-events: none; + margin-bottom: var(--space-1); + box-shadow: var(--shadow-md); +} + +[data-tooltip]:hover::after { + opacity: 1; + visibility: visible; +} + +/* ============================================ + ICONS + ============================================ */ +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.icon svg { + width: 100%; + height: 100%; +} + +.icon--sm { + width: 16px; + height: 16px; +} + +.icon--lg { + width: 24px; + height: 24px; +} + +/* ============================================ + SECTION HEADERS + ============================================ */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-4); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--border-color); +} + +.section-title { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); +} + +/* ============================================ + DIVIDERS + ============================================ */ +.divider { + height: 1px; + background: var(--border-color); + margin: var(--space-4) 0; +} + +.divider-vertical { + width: 1px; + height: 100%; + background: var(--border-color); + margin: 0 var(--space-3); +} + +/* ============================================ + UX POLISH - ENHANCED INTERACTIONS + ============================================ */ + +/* Button hover lift */ +.btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +/* Card/Panel hover effects */ +.card, +.panel { + transition: + box-shadow var(--transition-base), + border-color var(--transition-base), + transform var(--transition-base); +} + +.card:hover, +.panel:hover { + border-color: var(--border-light); +} + +/* Stats strip value highlight on hover */ +.strip-stat { + transition: background-color var(--transition-fast); + border-radius: var(--radius-sm); + cursor: default; +} + +.strip-stat:hover { + background: var(--bg-tertiary); +} + +/* Status dot pulse animation */ +.status-dot.online, +.status-dot.active { + animation: statusGlow 2s ease-in-out infinite; +} + +@keyframes statusGlow { + 0%, 100% { + box-shadow: 0 0 6px var(--status-online); + } + 50% { + box-shadow: 0 0 12px var(--status-online), 0 0 20px var(--status-online); + } +} + +/* Badge hover effect */ +.badge { + transition: transform var(--transition-fast), box-shadow var(--transition-fast); +} + +.badge:hover { + transform: scale(1.05); +} + +/* Alert entrance animation */ +.alert { + animation: alertSlideIn 0.3s ease-out; +} + +@keyframes alertSlideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Loading spinner smooth appearance */ +.spinner { + animation: spin 0.8s linear infinite, fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* Input focus glow */ +input:focus, +select:focus, +textarea:focus { + border-color: var(--accent-cyan); + box-shadow: 0 0 0 3px var(--accent-cyan-dim), 0 0 20px rgba(74, 158, 255, 0.1); +} + +/* Nav item active indicator */ +.mode-nav-btn.active::after, +.mobile-nav-btn.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + transform: translateX(-50%); + width: 60%; + height: 2px; + background: currentColor; + border-radius: var(--radius-full); +} + +/* Smooth tooltip appearance */ +[data-tooltip]::after { + transition: + opacity var(--transition-fast), + visibility var(--transition-fast), + transform var(--transition-fast); + transform: translateX(-50%) translateY(-4px); +} + +[data-tooltip]:hover::after { + transform: translateX(-50%) translateY(0); +} + +/* Disabled state with better visual feedback */ +:disabled, +.disabled { + opacity: 0.5; + cursor: not-allowed; + filter: grayscale(30%); +} diff --git a/static/css/core/layout.css b/static/css/core/layout.css new file mode 100644 index 0000000..e969223 --- /dev/null +++ b/static/css/core/layout.css @@ -0,0 +1,950 @@ +/** + * INTERCEPT Layout Styles + * Global layout structure: header, navigation, sidebar, main content + * Requires: variables.css, base.css, components.css + */ + +/* ============================================ + APP SHELL + ============================================ */ +.app-shell { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-main { + flex: 1; + display: flex; + flex-direction: column; +} + +/* ============================================ + GLOBAL HEADER + ============================================ */ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + height: var(--header-height); + padding: 0 var(--space-4); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: var(--z-sticky); +} + +.app-header-left { + display: flex; + align-items: center; + gap: var(--space-4); +} + +.app-header-right { + display: flex; + align-items: center; + gap: var(--space-3); +} + +/* Logo */ +.app-logo { + display: flex; + align-items: center; + gap: var(--space-3); + text-decoration: none; + color: var(--text-primary); +} + +.app-logo-icon { + width: 40px; + height: 40px; + flex-shrink: 0; +} + +.app-logo-text { + display: flex; + flex-direction: column; +} + +.app-logo-title { + font-size: var(--text-lg); + font-weight: var(--font-bold); + line-height: 1.2; + color: var(--text-primary); +} + +.app-logo-tagline { + font-size: var(--text-xs); + color: var(--text-dim); +} + +/* Page title in header */ +.app-header-title { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); +} + +.app-header-subtitle { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-left: var(--space-2); +} + +/* Header utilities */ +.header-utilities { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.header-clock { + display: flex; + align-items: center; + gap: var(--space-2); + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.header-clock-label { + font-size: var(--text-xs); + color: var(--text-dim); +} + +/* ============================================ + GLOBAL NAVIGATION + ============================================ */ +.app-nav { + display: flex; + align-items: center; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); + padding: 0 var(--space-4); + height: var(--nav-height); + gap: var(--space-1); + overflow-x: auto; +} + +.app-nav::-webkit-scrollbar { + height: 0; +} + +/* Nav groups */ +.nav-group { + display: flex; + align-items: center; + position: relative; +} + +/* Dropdown trigger */ +.nav-dropdown-trigger { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); + background: transparent; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.nav-dropdown-trigger:hover { + background: var(--bg-elevated); + color: var(--text-primary); +} + +.nav-dropdown-trigger.active { + background: var(--accent-cyan-dim); + color: var(--accent-cyan); +} + +.nav-dropdown-arrow { + width: 12px; + height: 12px; + transition: transform var(--transition-fast); +} + +.nav-group.open .nav-dropdown-arrow { + transform: rotate(180deg); +} + +/* Dropdown menu */ +.nav-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + min-width: 180px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: var(--space-1); + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: all var(--transition-fast); + z-index: var(--z-dropdown); +} + +.nav-group.open .nav-dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(4px); +} + +/* Nav items */ +.nav-item { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + font-size: var(--text-sm); + color: var(--text-secondary); + text-decoration: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition-fast); + border: none; + background: none; + width: 100%; + text-align: left; +} + +.nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.nav-item.active { + background: var(--accent-cyan-dim); + color: var(--accent-cyan); +} + +.nav-item-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* Nav divider */ +.nav-divider { + width: 1px; + height: 24px; + background: var(--border-color); + margin: 0 var(--space-2); +} + +/* Nav utilities (right side) */ +.nav-utilities { + display: flex; + align-items: center; + gap: var(--space-2); + margin-left: auto; + padding-left: var(--space-4); +} + +.nav-tool-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + text-decoration: none; +} + +.nav-tool-btn:hover { + background: var(--bg-elevated); + color: var(--text-primary); +} + +/* ============================================ + MOBILE NAVIGATION + ============================================ */ +.mobile-nav { + display: none; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); + padding: var(--space-2) var(--space-3); + overflow-x: auto; + gap: var(--space-2); +} + +.mobile-nav::-webkit-scrollbar { + height: 0; +} + +.mobile-nav-btn { + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-2) var(--space-3); + font-size: var(--text-xs); + font-weight: var(--font-medium); + color: var(--text-secondary); + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + white-space: nowrap; + text-decoration: none; + transition: all var(--transition-fast); +} + +.mobile-nav-btn:hover, +.mobile-nav-btn.active { + background: var(--accent-cyan-dim); + border-color: var(--accent-cyan); + color: var(--accent-cyan); +} + +/* Hamburger button */ +.hamburger-btn { + display: none; + flex-direction: column; + justify-content: center; + gap: 4px; + width: 32px; + height: 32px; + padding: 6px; + background: transparent; + border: none; + cursor: pointer; +} + +.hamburger-btn span { + display: block; + width: 100%; + height: 2px; + background: var(--text-secondary); + border-radius: 1px; + transition: all var(--transition-fast); +} + +.hamburger-btn.open span:nth-child(1) { + transform: rotate(45deg) translate(4px, 4px); +} + +.hamburger-btn.open span:nth-child(2) { + opacity: 0; +} + +.hamburger-btn.open span:nth-child(3) { + transform: rotate(-45deg) translate(4px, -4px); +} + +/* ============================================ + CONTENT LAYOUTS + ============================================ */ + +/* Main content with optional sidebar */ +.content-wrapper { + display: flex; + flex: 1; + overflow: hidden; +} + +/* Sidebar */ +.app-sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border-color); + overflow-y: auto; + flex-shrink: 0; +} + +.sidebar-section { + padding: var(--space-4); + border-bottom: 1px solid var(--border-color); +} + +.sidebar-section:last-child { + border-bottom: none; +} + +.sidebar-section-title { + font-size: var(--text-xs); + font-weight: var(--font-semibold); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: var(--space-3); +} + +/* Main content area */ +.app-content { + flex: 1; + overflow-y: auto; + padding: var(--space-4); +} + +.app-content-full { + flex: 1; + overflow: hidden; + position: relative; +} + +/* ============================================ + DASHBOARD LAYOUTS + ============================================ */ + +/* Full-screen dashboard (maps, etc.) */ +.dashboard-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.dashboard-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-2) var(--space-4); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.dashboard-header-logo { + font-size: var(--text-lg); + font-weight: var(--font-bold); + color: var(--text-primary); +} + +.dashboard-header-logo span { + font-size: var(--text-sm); + font-weight: var(--font-normal); + color: var(--text-dim); + margin-left: var(--space-2); +} + +.dashboard-main { + flex: 1; + display: flex; + overflow: hidden; + position: relative; +} + +.dashboard-map { + flex: 1; + position: relative; +} + +.dashboard-sidebar { + width: 320px; + background: var(--bg-secondary); + border-left: 1px solid var(--border-color); + overflow-y: auto; + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-3); +} + +/* ============================================ + PAGE LAYOUTS + ============================================ */ +.page-container { + max-width: var(--content-max-width); + margin: 0 auto; + padding: var(--space-6); +} + +.page-header { + margin-bottom: var(--space-6); +} + +.page-title { + font-size: var(--text-3xl); + font-weight: var(--font-bold); + color: var(--text-primary); + margin-bottom: var(--space-2); +} + +.page-description { + font-size: var(--text-base); + color: var(--text-secondary); +} + +/* ============================================ + RESPONSIVE BREAKPOINTS + ============================================ */ +@media (max-width: 1024px) { + .app-sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + z-index: var(--z-fixed); + transform: translateX(-100%); + transition: transform var(--transition-base); + } + + .app-sidebar.open { + transform: translateX(0); + } + + .dashboard-sidebar { + width: 280px; + } +} + +@media (max-width: 768px) { + .app-nav { + display: none; + } + + .mobile-nav { + display: flex; + } + + .hamburger-btn { + display: flex; + } + + .app-header { + padding: 0 var(--space-3); + } + + .app-logo-text { + display: none; + } + + .header-utilities { + gap: var(--space-1); + } + + .page-container { + padding: var(--space-4); + } + + .dashboard-sidebar { + position: fixed; + right: 0; + top: 0; + bottom: 0; + z-index: var(--z-fixed); + transform: translateX(100%); + transition: transform var(--transition-base); + } + + .dashboard-sidebar.open { + transform: translateX(0); + } +} + +/* ============================================ + OVERLAY (for mobile drawers) + ============================================ */ +.drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: calc(var(--z-fixed) - 1); + opacity: 0; + visibility: hidden; + transition: all var(--transition-base); +} + +.drawer-overlay.visible { + opacity: 1; + visibility: visible; +} + +/* ============================================ + BACK LINK + ============================================ */ +.back-link { + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-size: var(--text-sm); + color: var(--text-secondary); + text-decoration: none; + transition: color var(--transition-fast); +} + +.back-link:hover { + color: var(--accent-cyan); +} + +/* ============================================ + MODE NAVIGATION (from index.css) + Used by nav.html partial across all pages + ============================================ */ + +/* Mode Navigation Bar */ +.mode-nav { + display: none; + background: #151a23 !important; /* Explicit color - forced to ensure consistency */ + border-bottom: 1px solid var(--border-color); + padding: 0 20px; + position: relative; + z-index: 100; +} + +@media (min-width: 1024px) { + .mode-nav { + display: flex; + align-items: center; + gap: 8px; + height: 44px; + } +} + +.mode-nav-group { + display: flex; + align-items: center; + gap: 4px; +} + +.mode-nav-label { + font-size: 9px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1px; + margin-right: 8px; + font-weight: 500; +} + +.mode-nav-divider { + width: 1px; + height: 24px; + background: var(--border-color); + margin: 0 12px; +} + +.mode-nav-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + color: var(--text-secondary); + font-family: var(--font-sans); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.mode-nav-btn .nav-icon { + font-size: 14px; +} + +.mode-nav-btn .nav-icon svg { + width: 14px; + height: 14px; +} + +.mode-nav-btn .nav-label { + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.mode-nav-btn:hover { + background: var(--bg-elevated); + color: var(--text-primary); + border-color: var(--border-color); +} + +.mode-nav-btn.active { + background: var(--accent-cyan); + color: var(--bg-primary); + border-color: var(--accent-cyan); +} + +.mode-nav-btn.active .nav-icon { + filter: brightness(0); +} + +.mode-nav-actions { + display: flex; + align-items: center; + gap: 16px; +} + +.nav-action-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--bg-elevated); + border: 1px solid var(--accent-cyan); + border-radius: 4px; + color: var(--accent-cyan); + font-family: var(--font-sans); + font-size: 11px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.nav-action-btn .nav-icon { + font-size: 12px; +} + +.nav-action-btn .nav-label { + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.nav-action-btn:hover { + background: var(--accent-cyan); + color: var(--bg-primary); +} + +/* Dropdown Navigation */ +.mode-nav-dropdown { + position: relative; +} + +.mode-nav-dropdown-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + color: var(--text-secondary); + font-family: var(--font-sans); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.mode-nav-dropdown-btn:hover { + background: var(--bg-elevated); + color: var(--text-primary); + border-color: var(--border-color); +} + +.mode-nav-dropdown-btn .nav-icon { + font-size: 14px; +} + +.mode-nav-dropdown-btn .nav-icon svg { + width: 14px; + height: 14px; +} + +.mode-nav-dropdown-btn .nav-label { + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.mode-nav-dropdown-btn .dropdown-arrow { + font-size: 8px; + margin-left: 4px; + transition: transform 0.2s ease; +} + +.mode-nav-dropdown-btn .dropdown-arrow svg { + width: 10px; + height: 10px; +} + +.mode-nav-dropdown.open .mode-nav-dropdown-btn { + background: var(--bg-elevated); + color: var(--text-primary); + border-color: var(--border-color); +} + +.mode-nav-dropdown.open .dropdown-arrow { + transform: rotate(180deg); +} + +.mode-nav-dropdown.has-active .mode-nav-dropdown-btn { + background: var(--accent-cyan); + color: var(--bg-primary); + border-color: var(--accent-cyan); +} + +.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon { + filter: brightness(0); +} + +.mode-nav-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + margin-top: 4px; + min-width: 180px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: all 0.15s ease; + z-index: 1000; + padding: 6px; +} + +.mode-nav-dropdown.open .mode-nav-dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.mode-nav-dropdown-menu .mode-nav-btn { + width: 100%; + justify-content: flex-start; + padding: 10px 12px; + border-radius: 4px; + margin: 0; +} + +.mode-nav-dropdown-menu .mode-nav-btn:hover { + background: var(--bg-elevated); +} + +.mode-nav-dropdown-menu .mode-nav-btn.active { + background: var(--accent-cyan); + color: var(--bg-primary); +} + +/* Nav Bar Utilities (clock, theme, tools) */ +.nav-utilities { + display: none; + align-items: center; + gap: 12px; + margin-left: auto; + flex-shrink: 0; +} + +@media (min-width: 1024px) { + .nav-utilities { + display: flex; + } +} + +.nav-clock { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 11px; + flex-shrink: 0; + white-space: nowrap; +} + +.nav-clock .utc-label { + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +.nav-clock .utc-time { + color: var(--accent-cyan); + font-weight: 600; +} + +.nav-divider { + width: 1px; + height: 20px; + background: var(--border-color); +} + +.nav-tools { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.nav-tool-btn { + width: 28px; + height: 28px; + min-width: 28px; + border-radius: 4px; + background: transparent; + border: 1px solid transparent; + color: var(--text-secondary); + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.nav-tool-btn:hover { + background: var(--bg-elevated); + border-color: var(--border-color); + color: var(--accent-cyan); +} + +.nav-tool-btn svg { + width: 14px; + height: 14px; +} + +.nav-tool-btn .icon svg { + width: 14px; + height: 14px; +} + +/* Theme toggle icon states in nav bar */ +.nav-tool-btn .icon-sun, +.nav-tool-btn .icon-moon { + position: absolute; + transition: opacity 0.2s, transform 0.2s; + font-size: 14px; +} + +.nav-tool-btn .icon-sun { + opacity: 0; + transform: rotate(-90deg); +} + +.nav-tool-btn .icon-moon { + opacity: 1; + transform: rotate(0deg); +} + +[data-theme="light"] .nav-tool-btn .icon-sun { + opacity: 1; + transform: rotate(0deg); +} + +[data-theme="light"] .nav-tool-btn .icon-moon { + opacity: 0; + transform: rotate(90deg); +} + +/* Effects toggle icon states */ +.nav-tool-btn .icon-effects-off { + display: none; +} + +[data-animations="off"] .nav-tool-btn .icon-effects-on { + display: none; +} + +[data-animations="off"] .nav-tool-btn .icon-effects-off { + display: flex; +} diff --git a/static/css/core/variables.css b/static/css/core/variables.css new file mode 100644 index 0000000..32b79a0 --- /dev/null +++ b/static/css/core/variables.css @@ -0,0 +1,198 @@ +/** + * INTERCEPT Design Tokens + * Single source of truth for colors, spacing, typography, and effects + * Import this file FIRST in any stylesheet that needs design tokens + */ + +:root { + /* ============================================ + COLOR PALETTE - Dark Theme (Default) + ============================================ */ + + /* Backgrounds - layered depth system */ + --bg-primary: #0a0c10; + --bg-secondary: #0f1218; + --bg-tertiary: #151a23; + --bg-card: #121620; + --bg-elevated: #1a202c; + --bg-overlay: rgba(0, 0, 0, 0.7); + + /* Background aliases for components */ + --bg-dark: var(--bg-primary); + --bg-panel: var(--bg-secondary); + + /* Accent colors */ + --accent-cyan: #4a9eff; + --accent-cyan-dim: rgba(74, 158, 255, 0.15); + --accent-cyan-hover: #6bb3ff; + --accent-green: #22c55e; + --accent-green-dim: rgba(34, 197, 94, 0.15); + --accent-red: #ef4444; + --accent-red-dim: rgba(239, 68, 68, 0.15); + --accent-orange: #f59e0b; + --accent-orange-dim: rgba(245, 158, 11, 0.15); + --accent-amber: #d4a853; + --accent-amber-dim: rgba(212, 168, 83, 0.15); + --accent-yellow: #eab308; + --accent-purple: #a855f7; + + /* Text hierarchy */ + --text-primary: #e8eaed; + --text-secondary: #9ca3af; + --text-dim: #4b5563; + --text-muted: #374151; + --text-inverse: #0a0c10; + + /* Borders */ + --border-color: #1f2937; + --border-light: #374151; + --border-glow: rgba(74, 158, 255, 0.2); + --border-focus: var(--accent-cyan); + + /* Status colors */ + --status-online: #22c55e; + --status-warning: #f59e0b; + --status-error: #ef4444; + --status-offline: #6b7280; + --status-info: #3b82f6; + + /* ============================================ + SPACING SCALE + ============================================ */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + + /* ============================================ + TYPOGRAPHY + ============================================ */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + + /* Font sizes */ + --text-xs: 10px; + --text-sm: 12px; + --text-base: 14px; + --text-lg: 16px; + --text-xl: 18px; + --text-2xl: 20px; + --text-3xl: 24px; + --text-4xl: 30px; + + /* Font weights */ + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + + /* Line heights */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.75; + + /* ============================================ + BORDERS & RADIUS + ============================================ */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-full: 9999px; + + /* ============================================ + SHADOWS + ============================================ */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 20px rgba(74, 158, 255, 0.15); + + /* ============================================ + TRANSITIONS + ============================================ */ + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; + + /* ============================================ + Z-INDEX SCALE + ============================================ */ + --z-base: 0; + --z-dropdown: 100; + --z-sticky: 200; + --z-fixed: 300; + --z-modal-backdrop: 400; + --z-modal: 500; + --z-toast: 600; + --z-tooltip: 700; + + /* ============================================ + LAYOUT + ============================================ */ + --header-height: 60px; + --nav-height: 44px; + --sidebar-width: 280px; + --stats-strip-height: 36px; + --content-max-width: 1400px; +} + +/* ============================================ + LIGHT THEME OVERRIDES + ============================================ */ +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-secondary: #f1f5f9; + --bg-tertiary: #e2e8f0; + --bg-card: #ffffff; + --bg-elevated: #f8fafc; + --bg-overlay: rgba(255, 255, 255, 0.9); + + /* Background aliases for components */ + --bg-dark: var(--bg-primary); + --bg-panel: var(--bg-secondary); + + --accent-cyan: #2563eb; + --accent-cyan-dim: rgba(37, 99, 235, 0.1); + --accent-cyan-hover: #1d4ed8; + --accent-green: #16a34a; + --accent-green-dim: rgba(22, 163, 74, 0.1); + --accent-red: #dc2626; + --accent-red-dim: rgba(220, 38, 38, 0.1); + --accent-orange: #d97706; + --accent-orange-dim: rgba(217, 119, 6, 0.1); + --accent-amber: #b45309; + --accent-amber-dim: rgba(180, 83, 9, 0.1); + + --text-primary: #0f172a; + --text-secondary: #475569; + --text-dim: #94a3b8; + --text-muted: #cbd5e1; + --text-inverse: #f8fafc; + + --border-color: #e2e8f0; + --border-light: #cbd5e1; + --border-glow: rgba(37, 99, 235, 0.15); + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15); + --shadow-glow: 0 0 20px rgba(37, 99, 235, 0.1); +} + +/* ============================================ + REDUCED MOTION + ============================================ */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition-fast: 0ms; + --transition-base: 0ms; + --transition-slow: 0ms; + } +} diff --git a/templates/components/card.html b/templates/components/card.html new file mode 100644 index 0000000..c83088c --- /dev/null +++ b/templates/components/card.html @@ -0,0 +1,24 @@ +{# + Card/Panel Component + Reusable container with optional header and footer + + Variables: + - title: Optional card header title + - indicator: If true, shows status indicator dot in header + - indicator_active: If true, indicator is active/green + - no_padding: If true, removes body padding +#} + +
+ {% if title %} +
+ {{ title }} + {% if indicator %} +
+ {% endif %} +
+ {% endif %} +
+ {{ caller() }} +
+
diff --git a/templates/components/empty_state.html b/templates/components/empty_state.html new file mode 100644 index 0000000..8673707 --- /dev/null +++ b/templates/components/empty_state.html @@ -0,0 +1,38 @@ +{# + Empty State Component + Display when no data is available + + Variables: + - icon: Optional SVG icon (default: generic empty icon) + - title: Main message (default: "No data") + - description: Optional helper text + - action_text: Optional button text + - action_onclick: Optional button onclick handler + - action_href: Optional button link +#} + +
+
+ {% if icon %} + {{ icon|safe }} + {% else %} + + + + + {% endif %} +
+
{{ title|default('No data') }}
+ {% if description %} +
{{ description }}
+ {% endif %} + {% if action_text %} +
+ {% if action_href %} + {{ action_text }} + {% elif action_onclick %} + + {% endif %} +
+ {% endif %} +
diff --git a/templates/components/loading.html b/templates/components/loading.html new file mode 100644 index 0000000..c616b93 --- /dev/null +++ b/templates/components/loading.html @@ -0,0 +1,27 @@ +{# + Loading State Component + Display while data is being fetched + + Variables: + - text: Optional loading text (default: "Loading...") + - size: 'sm', 'md', or 'lg' (default: 'md') + - overlay: If true, renders as full overlay +#} + +{% if overlay %} +
+
+
+ {% if text %} +
{{ text }}
+ {% endif %} +
+
+{% else %} +
+
+ {% if text %} + {{ text }} + {% endif %} +
+{% endif %} diff --git a/templates/components/stats_strip.html b/templates/components/stats_strip.html new file mode 100644 index 0000000..b70e2db --- /dev/null +++ b/templates/components/stats_strip.html @@ -0,0 +1,47 @@ +{# + Stats Strip Component + Horizontal bar displaying key metrics + + Variables: + - stats: List of stat objects with 'id', 'value', 'label', and optional 'title' + - show_divider: Show divider after stats (default: true) + - status_dot_id: Optional ID for status indicator dot + - status_text_id: Optional ID for status text + - time_id: Optional ID for time display +#} + +
+
+ {% for stat in stats %} +
+ {{ stat.value|default('0') }} + {{ stat.label }} +
+ {% endfor %} + + {% if show_divider|default(true) %} +
+ {% endif %} + + {# Additional content from caller #} + {% if caller is defined %} + {{ caller() }} + {% endif %} + + {% if status_dot_id or status_text_id %} +
+
+ {% if status_dot_id %} +
+ {% endif %} + {% if status_text_id %} + STANDBY + {% endif %} +
+ {% endif %} + + {% if time_id %} +
--:--:-- UTC
+ {% endif %} +
+
diff --git a/templates/components/status_badge.html b/templates/components/status_badge.html new file mode 100644 index 0000000..783aeeb --- /dev/null +++ b/templates/components/status_badge.html @@ -0,0 +1,27 @@ +{# + Status Badge Component + Compact status indicator with dot and text + + Variables: + - status: 'online', 'offline', 'warning', 'error' (default: 'offline') + - text: Status text to display + - id: Optional ID for the text element (for JS updates) + - dot_id: Optional ID for the dot element (for JS updates) + - pulse: If true, adds pulse animation to dot +#} + +{% set status_class = { + 'online': 'online', + 'active': 'online', + 'offline': 'offline', + 'warning': 'warning', + 'error': 'error', + 'inactive': 'inactive' +}.get(status|default('offline'), 'inactive') %} + +
+
+ {{ text|default('Unknown') }} +
diff --git a/templates/layout/base.html b/templates/layout/base.html new file mode 100644 index 0000000..09e97ee --- /dev/null +++ b/templates/layout/base.html @@ -0,0 +1,169 @@ + + + + + + {% block title %}iNTERCEPT{% endblock %} // iNTERCEPT + + + {# Fonts - Conditional CDN/Local loading #} + {% if offline_settings and offline_settings.fonts_source == 'local' %} + + {% else %} + + {% endif %} + + {# Core CSS (Design System) #} + + + + + + {# Responsive styles #} + + + {# Page-specific CSS #} + {% block styles %}{% endblock %} + + {# Page-specific head content #} + {% block head %}{% endblock %} + + +
+ {# Global Header #} + {% block header %} +
+
+ + + {% if version %} + v{{ version }} + {% endif %} +
+
+ {% block header_right %} +
+ UTC + --:--:-- +
+ {% endblock %} +
+
+ {% endblock %} + + {# Global Navigation - opt-in for pages that need it #} + {# Override this block and include 'partials/nav.html' in child templates #} + {% block navigation %}{% endblock %} + + {# Main Content Area #} +
+ {% block main %} +
+ {# Optional Sidebar #} + {% block sidebar %}{% endblock %} + + {# Page Content #} +
+ {% block content %}{% endblock %} +
+
+ {% endblock %} +
+ + {# Toast/Notification Container #} +
+
+ + {# Core JavaScript #} + + + {# Page-specific JavaScript #} + {% block scripts %}{% endblock %} + + diff --git a/templates/layout/base_dashboard.html b/templates/layout/base_dashboard.html new file mode 100644 index 0000000..4853bd4 --- /dev/null +++ b/templates/layout/base_dashboard.html @@ -0,0 +1,226 @@ +{% extends 'layout/base.html' %} + +{# + Dashboard Base Template + Extended layout for full-screen dashboard pages (ADSB, AIS, Satellite, etc.) + Features: Full-height layout, stats strip, sidebar overlay on mobile + + Variables: + - active_mode: The current mode for nav highlighting (e.g., 'adsb', 'ais', 'satellite') +#} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block header %} +
+
+ +
+ + {% block dashboard_title %}DASHBOARD{% endblock %} + + + // iNTERCEPT + +
+
+
+ {% block dashboard_header_center %}{% endblock %} +
+ {% block agent_selector %}{% endblock %} +
+
+
+{% endblock %} + +{% block navigation %} +{# Include the unified nav partial with active_mode set #} +{% include 'partials/nav.html' with context %} +{% endblock %} + +{% block main %} +{# Background effects #} +
+ {% block dashboard_bg %} +
+ {% endblock %} +
+
+ +{# Stats strip #} +{% block stats_strip %}{% endblock %} + +{# Dashboard content #} +
+ {% block dashboard_content %}{% endblock %} +
+{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %} diff --git a/templates/partials/nav.html b/templates/partials/nav.html new file mode 100644 index 0000000..f2485cf --- /dev/null +++ b/templates/partials/nav.html @@ -0,0 +1,317 @@ +{# + Global Navigation Partial + Single source of truth for app navigation + + Compatible with: + - index.html (uses switchMode() for mode panels) + - Dashboard pages (uses navigation links) + + Variables: + - active_mode: Current active mode (e.g., 'pager', 'adsb', 'wifi') + - is_index_page: If true, Satellite/SSTV use switchMode (panel mode) + If false (default), Satellite links to dashboard +#} + +{% set is_index_page = is_index_page|default(false) %} + +{# Desktop Navigation - uses existing CSS class names for compatibility #} + + +{# Mobile Navigation Bar #} + + +{# JavaScript stub for pages that don't have switchMode defined #} + diff --git a/templates/partials/page_header.html b/templates/partials/page_header.html new file mode 100644 index 0000000..5d13ecd --- /dev/null +++ b/templates/partials/page_header.html @@ -0,0 +1,37 @@ +{# + Page Header Partial + Consistent page title with optional description and actions + + Variables: + - title: Page title (required) + - description: Optional description text + - back_url: Optional back link URL + - back_text: Optional back link text (default: "Back") +#} + +