Initial commit — Alta Video Player (WebAVP)
Web-based surveillance video player for Alta/Ava Security camera exports with multi-camera sync, timeline, digital zoom, motion analytics, and cryptographic integrity verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Alta Video Player (WebAVP)** — a web-based surveillance video player for Alta/Ava Security camera exports. Users drag-drop video files or ZIP archives (including AES-256 encrypted ones) and get a multi-camera synchronized playback experience with timeline, digital zoom, cryptographic integrity verification, and automatic motion analytics.
|
||||
|
||||
## Running the App
|
||||
|
||||
```bash
|
||||
python3 app.py
|
||||
# Serves on http://0.0.0.0:5152
|
||||
```
|
||||
|
||||
No build step. No dependencies needed at runtime — `app.py` uses only Python stdlib (`http.server`). The `requirements.txt` (flask, requests, gunicorn) is vestigial; the server was rewritten to pure stdlib.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (`app.py`)
|
||||
|
||||
Minimal HTTP server with three routes:
|
||||
- `GET /` — serves `templates/index.html`
|
||||
- `GET /static/*` — serves static files with path traversal protection
|
||||
- `GET /api/verify-cert?serial=...&certificateHash=...` — proxies certificate verification to `https://aware.avasecurity.com/api/v1/public/verifyServerCertificate`
|
||||
|
||||
### Frontend (`templates/index.html`)
|
||||
|
||||
Single self-contained HTML file (~5000 lines) with inline CSS and JavaScript in an IIFE. This is the entire application — there is no framework, no build system, no separate JS modules.
|
||||
|
||||
**State model:** Global `channels` Map keyed by channel name. Each channel holds segments (video blobs + time ranges), metadata, DOM references, color, and zoom state. Global timeline state (`globalStart`, `globalEnd`, `currentTime`) synchronizes all cameras. Motion analytics state lives in the `motionState` object.
|
||||
|
||||
**Key subsystems:**
|
||||
- **File ingestion** — drag-drop files/folders, ZIP extraction (JSZip for plain, custom AES-256-CTR for encrypted), metadata pairing by base filename, concurrent import guard (`isImporting` flag)
|
||||
- **Multi-camera grid** — responsive CSS grid (1–9 cameras), drag-to-reorder, click-to-expand
|
||||
- **Playback engine** — `requestAnimationFrame` tick loop, per-channel segment visibility management, variable speed (0.25x–8x), frame stepping
|
||||
- **Timeline** — interactive scrub bar with zoom (mouse wheel), minimap, per-channel segment indicators with color-coded dots, motion heatmap row
|
||||
- **Digital zoom** — per-camera scroll-to-zoom (up to 10x) with click-drag panning
|
||||
- **Magnifier tool** — draw rectangle to zoom into region
|
||||
- **Slideshow mode** — animated grid showing only currently active feeds with transitions
|
||||
- **Motion analytics** — automatic background motion detection on load (see below)
|
||||
- **Integrity verification** — offline X.509 certificate parsing, RSASSA-PKCS1-v1_5 and ECDSA signature verification via Web Crypto API, optional cloud verification through `/api/verify-cert`
|
||||
- **Session persistence** — IndexedDB caching of video blobs and metadata for page refresh survival
|
||||
|
||||
**External dependency:** `/static/jszip.min.js` (vendored, for unencrypted ZIP parsing).
|
||||
|
||||
## Motion Analytics Subsystem
|
||||
|
||||
Zero-dependency canvas-based motion detection that runs automatically when videos are loaded.
|
||||
|
||||
### How it works
|
||||
1. **Auto-scan on load** — `scheduleAutoScan()` is called from `flushPending()` and `loadSessionData()` after videos finish loading. Debounced 800ms to batch all segments.
|
||||
2. **Dedicated scan videos** — scan creates temporary offscreen `<video>` elements per segment (with `preload='auto'` for fast seeking) that don't interfere with the playback engine. Each is destroyed after its segment is processed.
|
||||
3. **Pixel-diff at 160x120** — frames are drawn to a small offscreen canvas. Each pixel's RGB delta is compared against a threshold. Changed pixels are counted and mapped to a 10x8 hotspot grid.
|
||||
4. **Sensitivity slider (1–100)** — maps to two internal thresholds via `sensitivityToThresholds()`: `pixelThreshold` (how different a pixel must be) and `changeThreshold` (what % of pixels must change). Presets: Indoor (30), Default (40), Parking (45), Outdoor (55).
|
||||
5. **Motion clusters** — consecutive motion detections within `clusterGap` (5s) are merged into clusters with start/end times, peak change %, and hotspot data.
|
||||
6. **Results visualization** — motion heatmap row on timeline (purple/red intensity), clickable cluster markers, scrollable event cards in the analytics panel with hotspot mini-grids.
|
||||
|
||||
### Key state: `motionState`
|
||||
- `sensitivity` (1–100), `scanInterval` (2s default), `clusterGap` (5s)
|
||||
- `detector.canvas` / `detector.ctx` — 160x120 offscreen canvas for pixel comparison
|
||||
- `detector.prevFrames` — Map of channelName → previous ImageData
|
||||
- `motionProfiles` — Map of channelName → array of `{ time, changePercent, hotspots, hasMotion }`
|
||||
- `motionClusters` — sorted array of `{ startTime, endTime, channelName, peakChange, ... }`
|
||||
- `isScanning` / `scanAbort` — scan lifecycle
|
||||
- `isMonitoring` — live monitor mode flag
|
||||
|
||||
### Key functions
|
||||
- `detectMotion(videoEl, channelName)` — core pixel-diff, returns `{ hasMotion, changePercent, hotspots }`
|
||||
- `scanTimeline({ auto })` — full scan orchestrator, creates offscreen videos, yields to UI every 12 frames
|
||||
- `scheduleAutoScan()` — debounced auto-trigger after video load
|
||||
- `setScanUIState('scanning'|'idle')` — manages rail button grey-out and progress UI
|
||||
- `seekToMotion(direction)` — skip to next/previous motion cluster
|
||||
- `monitorTick(absTime)` — called from `tick()` during playback for live detection
|
||||
- `buildMotionClusters()` — post-scan cluster identification
|
||||
- `renderMotionResults()` — populates panel: sparkline, summary, event cards
|
||||
|
||||
### UI elements
|
||||
- **Analytics panel** — right slide-out (400px), class `.analytics-panel`, toggled via `railAnalyticsBtn`
|
||||
- **Rail button** — greyed out with pulsing purple border (`.scanning` class) during auto-scan
|
||||
- **Skip-to-motion buttons** — `btnPrevMotion` / `btnNextMotion` in transport controls
|
||||
- **Keyboard shortcuts** — Shift+N (next motion), Shift+P (previous motion)
|
||||
|
||||
### Two-tier distribution strategy
|
||||
The browser version provides motion detection only (zero dependencies, fully offline). A future downloadable local app will add:
|
||||
- Object classification via bundled ONNX/YOLO model (person/vehicle/animal detection)
|
||||
- BYOA (Bring Your Own Agent) cloud AI integration for scene understanding, license plates, cross-camera tracking
|
||||
- Optional local LLM support (Ollama) for fully offline AI reasoning
|
||||
|
||||
## Memory Management
|
||||
|
||||
Video elements must be properly destroyed to avoid browser memory exhaustion:
|
||||
- **`destroyVideoEl(videoEl)`** — pauses video, removes `src`, calls `.load()` to force browser to release buffered data. Must be called before removing video elements from DOM.
|
||||
- **`video.preload = 'metadata'`** — all playback videos use metadata-only preloading to avoid buffering entire files into RAM. Scan videos use `'auto'` temporarily and are destroyed after use.
|
||||
- **`newSession()`** — comprehensive teardown: stops scan/monitor, destroys all video elements, revokes all blob URLs, nulls blob references, releases WebGL contexts, clears all state.
|
||||
- **`isImporting` guard** — prevents concurrent file imports which could cause race conditions and duplicate segments.
|
||||
- Slideshow video elements are destroyed on pane transitions and when slideshow is toggled off.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- All frontend code lives in `templates/index.html` — CSS at top, then the IIFE script block
|
||||
- Video elements are created on-demand and hidden (not removed) for performance
|
||||
- Segment visibility is recalculated every animation frame during playback
|
||||
- The `batchingSegments` flag defers rendering during bulk file imports
|
||||
- Keyboard shortcuts are defined inline (Space=play/pause, arrows=seek, S=slideshow, M=magnifier, F=fullscreen, [/]=speed, 0=reset zoom, Shift+N/P=skip motion)
|
||||
- Motion scan runs automatically on load — the analytics rail button is disabled until scan completes
|
||||
- Right-side panels (log, analytics) auto-close when the other opens
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Alta Video Player — lightweight HTTPS server using only Python stdlib."""
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
PORT = 5152
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
|
||||
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path == "/":
|
||||
self._serve_file(os.path.join(TEMPLATES_DIR, "index.html"), "text/html")
|
||||
elif path.startswith("/static/"):
|
||||
rel = path[len("/static/"):]
|
||||
file_path = os.path.join(STATIC_DIR, rel)
|
||||
if not os.path.realpath(file_path).startswith(os.path.realpath(STATIC_DIR)):
|
||||
self.send_error(403)
|
||||
return
|
||||
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
||||
self._serve_file(file_path, mime)
|
||||
elif path == "/api/verify-cert":
|
||||
self._proxy_verify(parsed.query)
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def _serve_file(self, file_path, content_type):
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = f.read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", len(data))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
except FileNotFoundError:
|
||||
self.send_error(404)
|
||||
|
||||
def _proxy_verify(self, query_string):
|
||||
params = parse_qs(query_string)
|
||||
serial = params.get("serial", [""])[0]
|
||||
cert_hash = params.get("certificateHash", [""])[0]
|
||||
|
||||
if not serial or not cert_hash:
|
||||
self._json_response(400, {"verified": False, "error": "Missing parameters"})
|
||||
return
|
||||
|
||||
try:
|
||||
qs = urlencode({"serial": serial, "certificateHash": cert_hash})
|
||||
url = f"https://aware.avasecurity.com/api/v1/public/verifyServerCertificate?{qs}"
|
||||
resp = urlopen(url, timeout=10)
|
||||
if 200 <= resp.status < 300:
|
||||
self._json_response(200, {"verified": True})
|
||||
else:
|
||||
self._json_response(200, {"verified": False, "error": f"HTTP {resp.status}"})
|
||||
except URLError as e:
|
||||
self._json_response(200, {"verified": False, "error": str(e)})
|
||||
|
||||
def _json_response(self, status, data):
|
||||
body = json.dumps(data).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", len(body))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(f"[AVP] {args[0]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = HTTPServer(("0.0.0.0", PORT), Handler)
|
||||
print(f"Alta Video Player running on http://0.0.0.0:{PORT}")
|
||||
server.serve_forever()
|
||||
@@ -0,0 +1,322 @@
|
||||
# Design System — Alta Style
|
||||
|
||||
> This document defines the visual identity for all tools and interfaces built under
|
||||
> the Alta brand. It covers colors, typography, component styling, and interaction
|
||||
> patterns. It does NOT prescribe layouts, page structures, or application architecture —
|
||||
> use whatever structure fits your application.
|
||||
>
|
||||
> Apply this to web apps, Electron apps, dashboards, internal tools, Chrome extensions,
|
||||
> or any UI that should feel like part of the Alta product family.
|
||||
>
|
||||
> Colors are sourced from the Alta Video platform's global CSS variables
|
||||
> (`--colorPrimary`, `--colorLink`, etc.) and computed element styles.
|
||||
|
||||
---
|
||||
|
||||
## 1. Color Palette
|
||||
|
||||
### 1.1 Backgrounds
|
||||
|
||||
| Token | Value | When to use |
|
||||
|----------------------|------------------------------|------------------------------------------------|
|
||||
| `--bg-app` | `#121826` | Page/body background — deep navy |
|
||||
| `--bg-panel` | `#121826` | Headers, nav bars, top-level panels |
|
||||
| `--bg-surface` | `#181F32` | Cards, containers, secondary sections |
|
||||
| `--bg-surface-hover` | `rgba(0, 110, 215, 0.12)` | Hovered rows, list items, interactive elements |
|
||||
| `--bg-surface-active`| `rgba(0, 110, 215, 0.20)` | Selected/pressed/active states |
|
||||
| `--bg-input` | `rgba(0, 110, 215, 0.10)` | Text inputs, search bars, selects, dropdowns |
|
||||
| `--bg-button` | `rgba(0, 110, 215, 0.20)` | Default button background |
|
||||
| `--bg-button-hover` | `rgba(0, 110, 215, 0.32)` | Hovered button background |
|
||||
|
||||
> **Key principle:** Alta layers semi-transparent tints of the brand blue
|
||||
> (`rgba(0, 110, 215, *)`) on the navy background. The secondary surface color
|
||||
> `#181F32` provides a subtle lift for cards and sections without transparency.
|
||||
|
||||
### 1.2 Borders
|
||||
|
||||
| Token | Value | When to use |
|
||||
|--------------------|-------------|----------------------------------------------------|
|
||||
| `--border-default` | `#C2C7CC` | Standard borders on components in dark context |
|
||||
| `--border-light` | `#EBEEF0` | Lighter borders, dividers within cards/panels |
|
||||
| `--border-subtle` | `rgba(244, 244, 246, 0.12)` | Very subtle separators on dark backgrounds |
|
||||
|
||||
> Alta uses light-colored borders (`#C2C7CC`, `#EBEEF0`) even on the dark theme.
|
||||
> These are typically used at low opacity or on specific components. For most
|
||||
> dark-on-dark dividers, use `--border-subtle`.
|
||||
|
||||
### 1.3 Text
|
||||
|
||||
| Token | Value | When to use |
|
||||
|--------------------|-------------|------------------------------------------|
|
||||
| `--text-primary` | `#F4F4F6` | Headings, labels, body content |
|
||||
| `--text-secondary` | `#656972` | Descriptions, metadata, timestamps |
|
||||
| `--text-muted` | `#8D9399` | Placeholders, disabled text, hints |
|
||||
| `--text-on-accent` | `#FFFFFF` | Text on accent or status backgrounds |
|
||||
|
||||
> Primary text is off-white (`#F4F4F6`), not pure white. Reserve `#FFFFFF` for
|
||||
> text on accent-colored or status-colored backgrounds.
|
||||
|
||||
### 1.4 Accent & Brand
|
||||
|
||||
| Token | Value | When to use |
|
||||
|--------------------------|------------------------------|---------------------------------------|
|
||||
| `--accent-primary` | `#006ED7` | Brand blue — links, primary actions, active states (Alta `--colorPrimary` / `--colorLink`) |
|
||||
| `--accent-primary-hover` | `#0080F0` | Hovered links and primary elements |
|
||||
| `--accent-primary-muted` | `rgba(0, 110, 215, 0.20)` | Tinted backgrounds using brand blue |
|
||||
|
||||
### 1.5 Status
|
||||
|
||||
| Token | Value | When to use |
|
||||
|--------------------|-----------|---------------------------------------------------|
|
||||
| `--status-success` | `#20C62F` | Success, connected, healthy, complete |
|
||||
| `--status-error` | `#DE1111` | Error, failed, disconnected, destructive actions |
|
||||
| `--status-warning` | `#EAA301` | Warning, caution, pending, needs attention |
|
||||
| `--status-info` | `#8D9399` | Informational, neutral, secondary indicators |
|
||||
| `--status-purple` | `#8957E5` | Special category badges, analytics |
|
||||
| `--status-motion` | `#A855F7` | Activity, progress, event indicators |
|
||||
|
||||
### 1.6 Overlays
|
||||
|
||||
| Token | Value | When to use |
|
||||
|-------------------|----------------------------|--------------------------------------|
|
||||
| `--overlay-dark` | `rgba(18, 24, 38, 0.60)` | Dimming layer behind modals/dialogs |
|
||||
| `--overlay-panel` | `rgba(18, 24, 38, 0.85)` | Floating panels, dropdown backdrops |
|
||||
|
||||
---
|
||||
|
||||
## 2. Typography
|
||||
|
||||
**Font stack:** `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`
|
||||
|
||||
| Role | Size | Weight | Notes |
|
||||
|--------------------|-------|--------|----------------------------------|
|
||||
| Page / section title | 16px | 600 | |
|
||||
| Body / labels | 14px | 400 | |
|
||||
| Emphasized labels | 14px | 600 | Selected items, active states |
|
||||
| Compact labels | 13px | 500 | Dense UI, toolbar text |
|
||||
| Captions | 12px | 400 | Metadata, timestamps, subtitles |
|
||||
| Badges | 10px | 600 | Inline status/category badges |
|
||||
| Micro | 11px | 600 | Tiny indicators, status labels |
|
||||
|
||||
**Spacing:** letter-spacing `-0.01em` on titles, `0` on body.
|
||||
**Line-height:** `1.3` titles, `1.4` body.
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Styles
|
||||
|
||||
### 3.1 Buttons
|
||||
|
||||
| Variant | Background | Text | Hover |
|
||||
|------------|---------------------------|---------------------|--------------------------------|
|
||||
| Primary | `--accent-primary` | `#FFFFFF` | `--accent-primary-hover` |
|
||||
| Secondary | `--bg-button` | `--text-primary` | `--bg-button-hover` |
|
||||
| Ghost/Icon | `transparent` | `--text-secondary` | `--bg-button`, text brightens |
|
||||
| Danger | `--status-error` | `#FFFFFF` | lighten ~10% |
|
||||
|
||||
- No visible borders on buttons — Alta buttons are borderless
|
||||
- Border-radius: `6px`
|
||||
- Padding: `8px 16px` (text) / `8px` (icon-only)
|
||||
- Heights: `32px` small, `36px` default, `40px` large
|
||||
- Font: 13px, weight 500
|
||||
|
||||
### 3.2 Inputs
|
||||
|
||||
- Height: `36px`
|
||||
- Background: `--bg-input`
|
||||
- Border: `1px solid var(--border-subtle)`, radius `6px`
|
||||
- Placeholder: `--text-muted`
|
||||
- Focus: border changes to `--accent-primary`, add `var(--shadow-focus)` ring
|
||||
|
||||
### 3.3 Badges
|
||||
|
||||
- Padding: `2px 6px`, radius `4px`
|
||||
- Font: 10px, weight 600, `#FFFFFF` text
|
||||
- Use `--status-purple` for special/category badges
|
||||
- Use `--accent-primary` for general tags
|
||||
|
||||
### 3.4 Status Indicators
|
||||
|
||||
- Dot size: `8px` circle
|
||||
- Colors: `--status-success`, `--status-error`, `--status-warning`
|
||||
- Always pair with a text label or icon — never color alone
|
||||
- Pulsing dots: `animation: pulse 1.5s ease-in-out infinite` (opacity 1 → 0.35)
|
||||
|
||||
### 3.5 Cards & Containers
|
||||
|
||||
- Background: `--bg-surface` (`#181F32`)
|
||||
- Border: `1px solid var(--border-subtle)`
|
||||
- Border-radius: `8px`
|
||||
- Elevated variant adds: `var(--shadow-elevated)`
|
||||
|
||||
### 3.6 Tables & Lists
|
||||
|
||||
- Header row: `--text-muted`, 11px, weight 600, uppercase, `letter-spacing: 0.05em`
|
||||
- Body rows: `--text-primary`, 13–14px
|
||||
- Row hover: `--bg-surface-hover`
|
||||
- Selected row: `--bg-surface-active`
|
||||
- Row dividers: `rgba(244, 244, 246, 0.08)`
|
||||
|
||||
### 3.7 Tooltips & Popovers
|
||||
|
||||
- Background: `--bg-surface`
|
||||
- Border: `1px solid var(--border-subtle)`
|
||||
- Border-radius: `8px`, padding `8px 12px`
|
||||
- Shadow: `var(--shadow-elevated)`
|
||||
- Text: `--text-primary`, 13px
|
||||
|
||||
### 3.8 Navigation Items
|
||||
|
||||
- Default: icon/text in `--text-secondary`
|
||||
- Hover: bg `--bg-surface-hover`, text brightens to `--text-primary`
|
||||
- Active: bg `--accent-primary`, text/icon `#FFFFFF`, radius `8px`
|
||||
|
||||
### 3.9 Modals & Dialogs
|
||||
|
||||
- Backdrop: `--overlay-dark`
|
||||
- Panel: `--bg-surface` background, `1px solid var(--border-subtle)`, radius `12px`
|
||||
- Shadow: `var(--shadow-elevated)`
|
||||
- Title: 16px weight 600, body: 14px weight 400
|
||||
|
||||
### 3.10 Tabs
|
||||
|
||||
- Inactive: `--text-secondary`, transparent background
|
||||
- Active: `--text-primary`, bottom border `2px solid var(--accent-primary)`
|
||||
- Hover: text brightens toward `--text-primary`
|
||||
|
||||
### 3.11 Progress & Bar Charts
|
||||
|
||||
- Track: `--bg-input`
|
||||
- Fill: use status colors based on meaning (green good, yellow caution, red bad)
|
||||
- Height: `8px`, radius `4px`
|
||||
|
||||
### 3.12 Toast / Notification
|
||||
|
||||
- Background: `--bg-surface`
|
||||
- Border-left: `3px solid` using appropriate status color
|
||||
- Border-radius: `8px`
|
||||
- Shadow: `var(--shadow-elevated)`
|
||||
- Auto-dismiss with fade-out
|
||||
|
||||
---
|
||||
|
||||
## 4. Iconography
|
||||
|
||||
- **Style:** Outline, 1.5px stroke weight
|
||||
- **Library:** Lucide Icons (preferred) or Heroicons Outline
|
||||
- **Sizes:** `20px` navigation, `16px` inline, `14px` compact/dense UI
|
||||
- **Colors:** `--text-secondary` default → `--text-primary` on hover → `#FFFFFF` on accent backgrounds
|
||||
- Don't mix filled and outline styles in the same interface
|
||||
|
||||
---
|
||||
|
||||
## 5. Animation & Transitions
|
||||
|
||||
| What | Duration | Easing |
|
||||
|---------------------|----------|----------------------------|
|
||||
| Hover states | 150ms | `ease-out` |
|
||||
| Panel open/close | 250ms | `cubic-bezier(.4,0,.2,1)` |
|
||||
| Fade in/out | 150ms | `ease` |
|
||||
| Pulse indicators | 1500ms | `ease-in-out` (infinite) |
|
||||
| Content transitions | 200ms | `ease-out` |
|
||||
|
||||
All motion is functional. No decorative animation.
|
||||
|
||||
---
|
||||
|
||||
## 6. Theme Rules
|
||||
|
||||
1. **Dark-only.** No light mode.
|
||||
2. **Navy, not black.** The base is `#121826`. Never use `#000000` for backgrounds.
|
||||
3. **Two surface levels.** `#121826` for the page, `#181F32` for elevated cards/sections.
|
||||
4. **Vivid brand blue.** The accent is `#006ED7` — a strong, saturated blue. Not muted, not pastel.
|
||||
5. **Off-white text.** Use `#F4F4F6`, not `#FFFFFF`. Pure white is only for accent backgrounds.
|
||||
6. **Real status colors.** Green `#20C62F`, red `#DE1111`, orange `#EAA301` — these are vivid and intentional.
|
||||
7. **Pair color with meaning.** Never use color alone for status — always include a label or icon.
|
||||
8. **WCAG AA contrast.** All text must meet ≥ 4.5:1 ratio against its background.
|
||||
|
||||
---
|
||||
|
||||
## 7. CSS Variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Backgrounds */
|
||||
--bg-app: #121826;
|
||||
--bg-panel: #121826;
|
||||
--bg-surface: #181F32;
|
||||
--bg-surface-hover: rgba(0, 110, 215, 0.12);
|
||||
--bg-surface-active: rgba(0, 110, 215, 0.20);
|
||||
--bg-input: rgba(0, 110, 215, 0.10);
|
||||
--bg-button: rgba(0, 110, 215, 0.20);
|
||||
--bg-button-hover: rgba(0, 110, 215, 0.32);
|
||||
|
||||
/* Borders */
|
||||
--border-default: #C2C7CC;
|
||||
--border-light: #EBEEF0;
|
||||
--border-subtle: rgba(244, 244, 246, 0.12);
|
||||
|
||||
/* Text */
|
||||
--text-primary: #F4F4F6;
|
||||
--text-secondary: #656972;
|
||||
--text-muted: #8D9399;
|
||||
--text-on-accent: #FFFFFF;
|
||||
|
||||
/* Accent */
|
||||
--accent-primary: #006ED7;
|
||||
--accent-primary-hover: #0080F0;
|
||||
--accent-primary-muted: rgba(0, 110, 215, 0.20);
|
||||
|
||||
/* Status */
|
||||
--status-success: #20C62F;
|
||||
--status-error: #DE1111;
|
||||
--status-warning: #EAA301;
|
||||
--status-info: #8D9399;
|
||||
--status-purple: #8957E5;
|
||||
--status-motion: #A855F7;
|
||||
|
||||
/* Overlays */
|
||||
--overlay-dark: rgba(18, 24, 38, 0.60);
|
||||
--overlay-panel: rgba(18, 24, 38, 0.85);
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 24px;
|
||||
--space-2xl: 32px;
|
||||
|
||||
/* Radii */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
--shadow-focus: 0 0 0 3px rgba(0, 110, 215, 0.25);
|
||||
|
||||
/* Typography */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Applying This System
|
||||
|
||||
When starting any new UI:
|
||||
|
||||
1. Set `body { background: var(--bg-app); color: var(--text-primary); font-family: var(--font-family); }`
|
||||
2. Use `--bg-surface` (`#181F32`) for any card or container — not a custom gray
|
||||
3. Use `--accent-primary` (`#006ED7`) for links, active states, and primary actions
|
||||
4. Use `--bg-button` / `--bg-button-hover` for secondary buttons
|
||||
5. Apply hover/active states using the brand blue opacity scale
|
||||
6. Pick status colors from the status tokens — `#20C62F` green, `#DE1111` red, `#EAA301` orange
|
||||
7. Keep transitions ≤ 250ms and functional
|
||||
8. Use Lucide outline icons at 1.5px stroke weight
|
||||
|
||||
This system works for dashboards, forms, data tables, settings pages, tools,
|
||||
admin panels, extensions, or any other interface. The layout is yours —
|
||||
the colors, type, and component feel are Alta.
|
||||
@@ -0,0 +1,3 @@
|
||||
flask
|
||||
requests
|
||||
gunicorn
|
||||
Vendored
+13
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user