8398832960
Rebuild homekit.html with centered layout, cleaner PIN input, and consistent styling matching the rest of the frontend. Add www/design-system.html as a living component reference for all UI elements used across the Strix frontend.
2038 lines
89 KiB
HTML
2038 lines
89 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#0a0a0f">
|
|
<title>Strix - Design System</title>
|
|
<style>
|
|
/* ============================================================
|
|
STRIX DESIGN SYSTEM
|
|
This file contains every UI component used across the Strix
|
|
frontend. When building new pages, copy the CSS variables
|
|
and component styles you need from here.
|
|
|
|
RULES:
|
|
- All pages are self-contained HTML files (no frameworks)
|
|
- Each page includes its own <style> block with only the
|
|
CSS it needs (copy from this file)
|
|
- JavaScript is vanilla ES5-compatible (var, not let/const)
|
|
- DOM is built imperatively with createElement
|
|
- Navigation between pages uses window.location.href with
|
|
URLSearchParams -- always pass ALL known data forward
|
|
- API calls use fetch() to api/* endpoints
|
|
- No external dependencies, no CDN, no build tools
|
|
- All SVG icons are inline, never use emoji
|
|
- Never use icon fonts or external icon libraries
|
|
============================================================ */
|
|
|
|
/* ============================================================
|
|
1. RESET & CSS VARIABLES
|
|
These variables are identical across ALL pages.
|
|
Copy this entire :root block into every new page.
|
|
============================================================ */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
/* Backgrounds (darkest to lightest) */
|
|
--bg-primary: #0a0a0f;
|
|
--bg-secondary: #1a1a24;
|
|
--bg-tertiary: #24242f;
|
|
--bg-elevated: #2a2a38;
|
|
|
|
/* Purple accent palette */
|
|
--purple-primary: #8b5cf6;
|
|
--purple-light: #a78bfa;
|
|
--purple-dark: #7c3aed;
|
|
--purple-glow: rgba(139, 92, 246, 0.3);
|
|
--purple-glow-strong: rgba(139, 92, 246, 0.5);
|
|
|
|
/* Text hierarchy */
|
|
--text-primary: #e0e0e8;
|
|
--text-secondary: #a0a0b0;
|
|
--text-tertiary: #606070;
|
|
--text-disabled: #404050;
|
|
|
|
/* Semantic colors */
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--error: #ef4444;
|
|
|
|
/* Borders */
|
|
--border-color: rgba(139, 92, 246, 0.15);
|
|
--border-focus: rgba(139, 92, 246, 0.5);
|
|
|
|
/* Typography */
|
|
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
|
|
/* Motion */
|
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
/* Shadows */
|
|
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
|
|
|
|
body {
|
|
font-family: var(--font-primary);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ============================================================
|
|
2. PAGE LAYOUTS
|
|
============================================================ */
|
|
|
|
/* --- 2a. Centered layout (index.html, homekit.html) ---
|
|
For single-purpose pages: one form, one action. */
|
|
.screen-centered {
|
|
min-height: 100vh;
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
animation: fadeIn var(--transition-base);
|
|
}
|
|
|
|
.container-narrow {
|
|
max-width: 480px;
|
|
width: 100%;
|
|
margin-top: 8vh;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screen-centered { padding: 3rem 1.5rem; }
|
|
.container-narrow { max-width: 540px; }
|
|
}
|
|
|
|
/* --- 2b. Standard layout (most pages) ---
|
|
For content pages with back button and scrollable content. */
|
|
.screen {
|
|
padding: 1.5rem;
|
|
animation: fadeIn var(--transition-base);
|
|
}
|
|
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screen { padding: 3rem 1.5rem; }
|
|
.container { max-width: 700px; }
|
|
}
|
|
|
|
/* --- 2c. Wide layout (test.html) ---
|
|
For data-heavy pages with grids. */
|
|
.container-wide {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
}
|
|
|
|
/* --- 2d. Two-column layout (config.html) ---
|
|
Settings on left, live preview on right. Collapses to tabs on mobile. */
|
|
.columns {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.col-left { flex: 1; min-width: 0; }
|
|
.col-right { flex: 1; min-width: 0; position: sticky; top: 1.5rem; }
|
|
|
|
@media (max-width: 768px) {
|
|
.columns { flex-direction: column; }
|
|
.col-left, .col-right { width: 100%; }
|
|
.col-right { position: static; }
|
|
.col-left.hidden, .col-right.hidden { display: none; }
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
3. TYPOGRAPHY
|
|
============================================================ */
|
|
.hero { text-align: center; margin-bottom: 3rem; }
|
|
|
|
.logo {
|
|
width: 64px; height: 64px;
|
|
color: var(--purple-primary);
|
|
margin: 0 auto 1rem;
|
|
filter: drop-shadow(0 4px 12px var(--purple-glow));
|
|
}
|
|
|
|
.title {
|
|
font-size: 2rem; font-weight: 700;
|
|
letter-spacing: 0.05em; margin-bottom: 0.5rem;
|
|
background: linear-gradient(135deg, var(--purple-light), var(--purple-primary));
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.subtitle { font-size: 0.875rem; color: var(--text-secondary); }
|
|
|
|
.page-title { font-size: 1.375rem; font-weight: 600; margin-bottom: 1.5rem; }
|
|
.page-subtitle { font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 2rem; }
|
|
|
|
.screen-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
|
|
.screen-subtitle { font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 2rem; }
|
|
|
|
.section-title {
|
|
font-size: 0.6875rem; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.06em; color: var(--text-tertiary);
|
|
padding-bottom: 0.5rem; margin-bottom: 0.75rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.section-divider { height: 1px; background: var(--border-color); margin: 1.5rem 0; }
|
|
|
|
|
|
/* ============================================================
|
|
4. BUTTONS
|
|
============================================================ */
|
|
|
|
/* --- 4a. Primary button --- */
|
|
.btn {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
gap: 0.5rem; padding: 1rem 1.5rem; border-radius: 8px;
|
|
font-size: 1rem; font-weight: 600; font-family: var(--font-primary);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
border: none; outline: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--purple-primary), var(--purple-dark));
|
|
color: white; box-shadow: 0 4px 12px var(--purple-glow);
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px var(--purple-glow-strong);
|
|
}
|
|
|
|
.btn-primary:active:not(:disabled) { transform: translateY(0); }
|
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Full-width variant */
|
|
.btn-large { width: 100%; padding: 1.5rem; font-size: 1.125rem; }
|
|
|
|
/* --- 4b. Back button (top-left navigation) --- */
|
|
.btn-back {
|
|
display: inline-flex; align-items: center; gap: 0.5rem;
|
|
background: none; border: none;
|
|
color: var(--text-secondary); font-size: 0.875rem;
|
|
font-family: var(--font-primary); cursor: pointer;
|
|
padding: 0.5rem 0; margin-bottom: 1.5rem;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.btn-back:hover { color: var(--purple-primary); }
|
|
|
|
/* --- 4c. Small button (config actions, copy) --- */
|
|
.btn-sm {
|
|
padding: 0.25rem 0.625rem; border-radius: 4px;
|
|
font-size: 0.6875rem; font-weight: 600; font-family: var(--font-primary);
|
|
cursor: pointer; border: 1px solid var(--border-color);
|
|
background: var(--bg-tertiary); color: var(--text-secondary);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
.btn-sm:hover { border-color: var(--purple-primary); color: var(--purple-light); }
|
|
|
|
/* --- 4d. Outline button --- */
|
|
.btn-outline {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
gap: 0.5rem; padding: 1rem 1.5rem; border-radius: 8px;
|
|
font-size: 0.9375rem; font-weight: 600; font-family: var(--font-primary);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
background: transparent; color: var(--text-secondary);
|
|
border: 1px solid var(--border-color); width: 100%;
|
|
}
|
|
.btn-outline:hover { border-color: var(--purple-primary); color: var(--purple-light); }
|
|
|
|
/* --- 4e. Danger / Stop button --- */
|
|
.btn-stop {
|
|
padding: 0.5rem 1rem; border-radius: 6px;
|
|
font-size: 0.75rem; font-weight: 600; font-family: var(--font-primary);
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
background: rgba(239, 68, 68, 0.12); color: var(--error);
|
|
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
}
|
|
.btn-stop:hover { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); }
|
|
.btn-stop:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
|
|
/* --- 4f. Add sub-item button --- */
|
|
.btn-add-sub {
|
|
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
padding: 0.375rem 0.75rem; background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color); border-radius: 6px;
|
|
color: var(--text-secondary); font-size: 0.75rem; font-weight: 500;
|
|
font-family: var(--font-primary); cursor: pointer;
|
|
transition: all var(--transition-fast); margin-bottom: 0.75rem;
|
|
}
|
|
.btn-add-sub:hover { border-color: var(--purple-primary); color: var(--purple-light); }
|
|
|
|
/* --- 4g. Save button (green, for destructive saves) --- */
|
|
.btn-save {
|
|
flex: 1; padding: 0.625rem; border-radius: 6px;
|
|
background: var(--success); color: #000; border: none;
|
|
font-size: 0.8125rem; font-weight: 700; font-family: var(--font-primary);
|
|
cursor: pointer; transition: opacity var(--transition-fast);
|
|
}
|
|
.btn-save:hover:not(:disabled) { opacity: 0.9; }
|
|
.btn-save:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
/* --- 4h. Copy small button (icon button for inline copy) --- */
|
|
.btn-copy-sm {
|
|
flex-shrink: 0; padding: 0.375rem;
|
|
background: var(--bg-tertiary); border: 1px solid var(--border-color);
|
|
border-radius: 4px; cursor: pointer; color: var(--text-tertiary);
|
|
display: flex; transition: all var(--transition-fast);
|
|
}
|
|
.btn-copy-sm:hover { border-color: var(--purple-primary); color: var(--purple-light); }
|
|
.btn-copy-sm svg { width: 14px; height: 14px; }
|
|
|
|
/* --- 4i. Copy block button --- */
|
|
.btn-copy-block {
|
|
margin-top: 0.75rem; padding: 0.5rem 1rem;
|
|
background: var(--bg-tertiary); border: 1px solid var(--border-color);
|
|
border-radius: 6px; cursor: pointer;
|
|
color: var(--text-secondary); font-size: 0.75rem; font-weight: 600;
|
|
font-family: var(--font-primary); transition: all var(--transition-fast);
|
|
}
|
|
.btn-copy-block:hover { border-color: var(--purple-primary); color: var(--purple-light); }
|
|
|
|
/* --- 4j. Add button (+) --- */
|
|
.btn-add {
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--purple-primary);
|
|
font-size: 1.125rem; font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.btn-add:hover { border-color: var(--purple-primary); background: var(--bg-elevated); }
|
|
|
|
|
|
/* ============================================================
|
|
5. FORM ELEMENTS
|
|
============================================================ */
|
|
|
|
.form-group { margin-bottom: 1.5rem; }
|
|
|
|
.label {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
font-size: 0.875rem; font-weight: 500;
|
|
color: var(--text-secondary); margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 0.75rem; font-weight: 500; color: var(--text-secondary);
|
|
margin-bottom: 0.375rem; display: flex; align-items: center; gap: 0.375rem;
|
|
}
|
|
|
|
.optional { color: var(--text-tertiary); font-weight: 400; }
|
|
.hint { margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-tertiary); }
|
|
.field-hint { font-size: 0.6875rem; color: var(--text-tertiary); margin-top: 0.25rem; }
|
|
|
|
/* --- 5a. Text input --- */
|
|
.input {
|
|
width: 100%; padding: 1rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
font-size: 1rem; font-family: var(--font-primary);
|
|
transition: all var(--transition-fast);
|
|
outline: none;
|
|
}
|
|
|
|
.input:focus {
|
|
border-color: var(--purple-primary);
|
|
box-shadow: 0 0 0 3px var(--purple-glow);
|
|
}
|
|
|
|
.input::placeholder { color: var(--text-tertiary); }
|
|
.input:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
|
|
.input-large { padding: 1.5rem; font-size: 1.125rem; }
|
|
.input-sm { max-width: 120px; }
|
|
.input-mono { font-family: var(--font-mono); font-size: 0.8125rem; }
|
|
|
|
/* --- 5b. Compact input (config.html style) --- */
|
|
.input-compact {
|
|
width: 100%; padding: 0.625rem 0.75rem;
|
|
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
|
border-radius: 6px; color: var(--text-primary);
|
|
font-size: 0.8125rem; font-family: var(--font-primary);
|
|
outline: none; transition: all var(--transition-fast);
|
|
}
|
|
.input-compact:focus { border-color: var(--purple-primary); box-shadow: 0 0 0 2px var(--purple-glow); }
|
|
.input-compact::placeholder { color: var(--text-tertiary); }
|
|
|
|
/* --- 5c. Select dropdown --- */
|
|
select.input {
|
|
cursor: pointer; appearance: none; padding-right: 2rem;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23606070' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat; background-position: right 0.75rem center;
|
|
}
|
|
|
|
/* --- 5d. Validated input with checkmark --- */
|
|
.input-validated { position: relative; }
|
|
.input-validated .input { padding-right: 3rem; }
|
|
.icon-check {
|
|
position: absolute; right: 1rem; top: 50%; transform: translateY(-50%);
|
|
color: var(--success);
|
|
}
|
|
|
|
/* --- 5e. Password input with toggle --- */
|
|
.input-password { position: relative; }
|
|
.input-password .input { padding-right: 3rem; }
|
|
.btn-toggle-pass {
|
|
position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%);
|
|
background: none; border: none; padding: 0.5rem; cursor: pointer;
|
|
color: var(--text-tertiary); display: flex;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.btn-toggle-pass:hover { color: var(--purple-primary); }
|
|
|
|
/* --- 5f. Add row (input + button side by side) --- */
|
|
.add-row { display: flex; gap: 0.5rem; }
|
|
|
|
.add-input {
|
|
flex: 1; padding: 0.75rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
font-size: 0.875rem; font-family: var(--font-mono);
|
|
outline: none;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.add-input:focus {
|
|
border-color: var(--purple-primary);
|
|
box-shadow: 0 0 0 3px var(--purple-glow);
|
|
}
|
|
|
|
.add-input::placeholder { color: var(--text-tertiary); }
|
|
|
|
/* --- 5g. Row layout (side-by-side fields) --- */
|
|
.row { display: flex; gap: 0.75rem; }
|
|
.row > .form-group { flex: 1; }
|
|
|
|
/* --- 5h. Toggle switch --- */
|
|
.toggle-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0.5rem 0;
|
|
}
|
|
.toggle-label { font-size: 0.8125rem; color: var(--text-primary); }
|
|
|
|
.toggle {
|
|
width: 36px; height: 20px; border-radius: 10px;
|
|
background: var(--bg-tertiary); border: 1px solid var(--border-color);
|
|
cursor: pointer; position: relative; transition: all var(--transition-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
.toggle.on { background: var(--purple-primary); border-color: var(--purple-primary); }
|
|
.toggle::after {
|
|
content: ''; position: absolute; top: 2px; left: 2px;
|
|
width: 14px; height: 14px; border-radius: 50%;
|
|
background: white; transition: transform var(--transition-fast);
|
|
}
|
|
.toggle.on::after { transform: translateX(16px); }
|
|
|
|
|
|
/* ============================================================
|
|
6. TOOLTIPS & INFO ICONS
|
|
============================================================ */
|
|
.info-icon {
|
|
position: relative; display: inline-flex;
|
|
align-items: center; justify-content: center;
|
|
width: 16px; height: 16px; cursor: help;
|
|
color: var(--text-tertiary); transition: color var(--transition-fast);
|
|
}
|
|
|
|
.info-icon:hover { color: var(--purple-primary); }
|
|
.info-icon svg { width: 16px; height: 16px; }
|
|
|
|
.tooltip {
|
|
position: absolute; top: calc(100% + 8px); left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--purple-primary);
|
|
border-radius: 8px; padding: 1rem;
|
|
width: 320px; max-width: 90vw;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--purple-glow);
|
|
z-index: 1000; opacity: 0; visibility: hidden;
|
|
transition: opacity var(--transition-fast), visibility var(--transition-fast);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tooltip::after {
|
|
content: ''; position: absolute; bottom: 100%; left: 50%;
|
|
transform: translateX(-50%);
|
|
border: 6px solid transparent; border-bottom-color: var(--purple-primary);
|
|
}
|
|
|
|
.info-icon:hover .tooltip { opacity: 1; visibility: visible; }
|
|
|
|
.tooltip-title { font-weight: 600; color: var(--purple-primary); margin-bottom: 0.5rem; font-size: 0.875rem; }
|
|
.tooltip-text { font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary); margin-bottom: 0.75rem; }
|
|
.tooltip-text:last-child { margin-bottom: 0; }
|
|
.tooltip-examples { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border-color); }
|
|
.tooltip-examples-title { font-weight: 600; color: var(--text-primary); font-size: 0.75rem; margin-bottom: 0.5rem; }
|
|
.tooltip-example {
|
|
font-family: var(--font-mono); font-size: 0.75rem;
|
|
color: var(--purple-light); background: var(--bg-secondary);
|
|
padding: 0.25rem 0.5rem; border-radius: 4px;
|
|
margin-bottom: 0.25rem; display: block;
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
7. BADGES & TAGS
|
|
============================================================ */
|
|
|
|
/* --- 7a. Type badges (probe result) --- */
|
|
.type-badge {
|
|
display: inline-block; padding: 0.125rem 0.5rem;
|
|
border-radius: 4px; font-weight: 600; font-size: 0.6875rem;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
}
|
|
.type-standard { background: var(--success); color: #000; }
|
|
.type-homekit { background: var(--purple-primary); color: #fff; }
|
|
.type-unreachable { background: var(--error); color: #fff; }
|
|
|
|
/* --- 7b. Mode badge --- */
|
|
.mode-badge {
|
|
font-size: 0.75rem; font-weight: 600;
|
|
padding: 0.25rem 0.625rem; border-radius: 4px;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
background: rgba(139, 92, 246, 0.15); color: var(--purple-light);
|
|
margin-left: 0.75rem; vertical-align: middle;
|
|
}
|
|
|
|
/* --- 7c. Status badge (running/done) --- */
|
|
.status-badge {
|
|
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
font-family: var(--font-mono); font-size: 0.6875rem;
|
|
font-weight: 500; text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
padding: 0.25rem 0.625rem; border-radius: 3px;
|
|
}
|
|
|
|
.status-badge.running { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
|
.status-badge.done { background: rgba(16, 185, 129, 0.12); color: var(--success); }
|
|
|
|
.status-dot {
|
|
width: 6px; height: 6px; border-radius: 50%; background: currentColor;
|
|
}
|
|
.status-badge.running .status-dot { animation: pulse 1.2s infinite; }
|
|
|
|
/* --- 7d. Tags (selected items) --- */
|
|
.tag {
|
|
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
padding: 0.375rem 0.625rem;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
font-size: 0.75rem; color: var(--text-primary);
|
|
}
|
|
|
|
.tag .tag-type {
|
|
font-size: 0.625rem; font-weight: 600;
|
|
text-transform: uppercase; color: var(--text-tertiary);
|
|
}
|
|
|
|
.tag .tag-remove {
|
|
background: none; border: none; cursor: pointer;
|
|
color: var(--text-tertiary); padding: 0;
|
|
display: flex; align-items: center;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
|
|
.tag .tag-remove:hover { color: var(--error); }
|
|
|
|
/* --- 7e. Card tags (codec overlays on thumbnails) --- */
|
|
.card-tag {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
font-weight: 500; padding: 0.125rem 0.375rem; border-radius: 2px;
|
|
background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px);
|
|
color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
8. CARDS
|
|
============================================================ */
|
|
|
|
/* --- 8a. Stream card (simple info card) --- */
|
|
.stream-card {
|
|
padding: 1rem 1.25rem;
|
|
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
|
border-radius: 8px; margin-bottom: 0.75rem;
|
|
}
|
|
.stream-label {
|
|
font-size: 0.625rem; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.05em; color: var(--text-tertiary); margin-bottom: 0.375rem;
|
|
}
|
|
.stream-url {
|
|
font-family: var(--font-mono); font-size: 0.75rem;
|
|
color: var(--text-secondary); word-break: break-all; line-height: 1.5;
|
|
}
|
|
.stream-url .scheme { color: var(--purple-light); }
|
|
|
|
/* --- 8b. Media card (test results with thumbnail) --- */
|
|
.card {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
transition: border-color var(--transition-fast), transform var(--transition-fast);
|
|
animation: cardIn 0.25s ease;
|
|
}
|
|
.card:hover { border-color: var(--purple-primary); transform: translateY(-2px); }
|
|
|
|
.card-thumb {
|
|
position: relative; width: 100%;
|
|
aspect-ratio: 16/9;
|
|
background: var(--bg-primary); overflow: hidden;
|
|
}
|
|
|
|
.card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
.card-thumb-placeholder {
|
|
width: 100%; height: 100%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.card-thumb-placeholder svg { width: 32px; height: 32px; color: var(--text-tertiary); }
|
|
|
|
.card-overlay { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.25rem; }
|
|
|
|
.card-resolution {
|
|
position: absolute; bottom: 0.5rem; left: 0.5rem;
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 2px;
|
|
background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px);
|
|
color: var(--purple-light);
|
|
}
|
|
|
|
.card-body { padding: 0.75rem 1rem; }
|
|
|
|
.card-url {
|
|
font-family: var(--font-mono); font-size: 0.6875rem;
|
|
color: var(--text-secondary); line-height: 1.5;
|
|
word-break: break-all;
|
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical; overflow: hidden;
|
|
}
|
|
.card-url .scheme { color: var(--purple-light); }
|
|
|
|
.card-meta {
|
|
display: flex; align-items: center; gap: 0.75rem;
|
|
margin-top: 0.5rem; padding-top: 0.5rem;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.card-meta-item {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
color: var(--text-tertiary); display: flex;
|
|
align-items: center; gap: 0.25rem;
|
|
}
|
|
|
|
.card-action { padding: 0 1rem 0.75rem; }
|
|
|
|
.btn-use {
|
|
width: 100%; padding: 0.5rem;
|
|
background: linear-gradient(135deg, var(--purple-primary), var(--purple-dark));
|
|
color: white; border: none; border-radius: 6px;
|
|
font-size: 0.75rem; font-weight: 600; font-family: var(--font-primary);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
box-shadow: 0 2px 8px var(--purple-glow);
|
|
}
|
|
.btn-use:hover { box-shadow: 0 4px 16px var(--purple-glow-strong); transform: translateY(-1px); }
|
|
|
|
/* Cards grid */
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
@media (max-width: 640px) { .cards-grid { grid-template-columns: 1fr; } }
|
|
|
|
|
|
/* ============================================================
|
|
9. LISTS & BOXES
|
|
============================================================ */
|
|
|
|
/* --- 9a. Streams box (scrollable list) --- */
|
|
.streams-box {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
max-height: 60vh; overflow-y: auto;
|
|
}
|
|
|
|
.streams-box::-webkit-scrollbar { width: 6px; }
|
|
.streams-box::-webkit-scrollbar-track { background: transparent; }
|
|
.streams-box::-webkit-scrollbar-thumb { background: var(--purple-primary); border-radius: 3px; }
|
|
|
|
.stream-url-item {
|
|
padding: 0.5rem 0.75rem;
|
|
font-family: var(--font-mono); font-size: 0.6875rem;
|
|
color: var(--text-secondary);
|
|
border-bottom: 1px solid rgba(139, 92, 246, 0.07);
|
|
word-break: break-all; line-height: 1.5;
|
|
}
|
|
.stream-url-item:last-child { border-bottom: none; }
|
|
|
|
/* --- 9b. Device info table --- */
|
|
.device-info {
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.device-row {
|
|
display: flex; justify-content: space-between;
|
|
padding: 0.375rem 0;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.device-row:not(:last-child) {
|
|
border-bottom: 1px solid rgba(139, 92, 246, 0.07);
|
|
}
|
|
|
|
.device-label { color: var(--text-tertiary); }
|
|
.device-value { color: var(--text-primary); font-family: var(--font-mono); font-size: 0.75rem; }
|
|
|
|
/* --- 9c. Stream count --- */
|
|
.stream-count {
|
|
font-size: 0.75rem; color: var(--text-tertiary);
|
|
font-family: var(--font-mono);
|
|
margin-bottom: 0.75rem;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
10. PROGRESS & STATUS
|
|
============================================================ */
|
|
|
|
/* --- 10a. Status bar --- */
|
|
.status-bar {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 1rem 1.25rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.status-top {
|
|
display: flex; align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.status-session { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-tertiary); }
|
|
|
|
/* --- 10b. Counters grid --- */
|
|
.counters { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
|
|
|
|
@media (max-width: 640px) { .counters { grid-template-columns: repeat(2, 1fr); } }
|
|
|
|
.counter {
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
padding: 0.75rem; text-align: center;
|
|
}
|
|
|
|
.counter-value {
|
|
font-family: var(--font-mono); font-size: 1.5rem;
|
|
font-weight: 600; line-height: 1; margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.counter-label {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
color: var(--text-secondary); text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
/* Counter color variants */
|
|
.counter.total .counter-value { color: var(--text-primary); }
|
|
.counter.tested .counter-value { color: #3b82f6; }
|
|
.counter.alive .counter-value { color: var(--success); }
|
|
.counter.screenshots .counter-value { color: var(--warning); }
|
|
|
|
/* --- 10c. Progress bar --- */
|
|
.progress-track {
|
|
height: 3px; background: var(--border-color);
|
|
border-radius: 2px; margin-top: 1rem; overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%; background: #3b82f6;
|
|
border-radius: 2px; width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.progress-fill.complete { background: var(--success); }
|
|
|
|
/* --- 10d. Loading spinner --- */
|
|
.loading {
|
|
text-align: center; padding: 3rem;
|
|
color: var(--text-tertiary); font-size: 0.875rem;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 24px; height: 24px;
|
|
border: 2px solid var(--border-color);
|
|
border-top-color: var(--purple-primary);
|
|
border-radius: 50%;
|
|
margin: 0 auto 1rem;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
/* --- 10e. Pairing spinner (inline, white) --- */
|
|
.pairing-spinner {
|
|
width: 20px; height: 20px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
border-top-color: white;
|
|
border-radius: 50%;
|
|
animation: spin 0.7s linear infinite;
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
11. COLLAPSIBLE SECTIONS & GROUPS
|
|
============================================================ */
|
|
|
|
/* --- 11a. Expand button (config settings) --- */
|
|
.expand-btn {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
width: 100%; padding: 0.75rem 0; background: none; border: none;
|
|
color: var(--text-secondary); font-size: 0.8125rem; font-weight: 600;
|
|
font-family: var(--font-primary); cursor: pointer;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.expand-btn:hover { color: var(--purple-primary); }
|
|
.expand-btn .chevron { transition: transform var(--transition-fast); width: 12px; height: 12px; }
|
|
.expand-btn.open .chevron { transform: rotate(90deg); }
|
|
|
|
.expandable { display: none; }
|
|
.expandable.open { display: block; }
|
|
|
|
/* --- 11b. Group with collapse (test results) --- */
|
|
.group { margin-bottom: 1.5rem; }
|
|
|
|
.group-header {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
margin-bottom: 1rem;
|
|
cursor: pointer; user-select: none;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.group-header:hover { color: var(--purple-primary); }
|
|
|
|
.group-toggle {
|
|
display: flex; background: none; border: none;
|
|
padding: 0; cursor: pointer; color: var(--text-tertiary);
|
|
}
|
|
.group-toggle .chevron { transition: transform var(--transition-fast); }
|
|
.group.collapsed .group-toggle .chevron { transform: rotate(-90deg); }
|
|
.group.collapsed .group-content { display: none; }
|
|
|
|
.group-title {
|
|
font-size: 0.8125rem; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
}
|
|
.group-count { font-size: 0.8125rem; color: var(--text-tertiary); }
|
|
|
|
|
|
/* ============================================================
|
|
12. MOBILE TABS
|
|
============================================================ */
|
|
.tabs { display: none; margin-bottom: 1rem; }
|
|
|
|
.tabs-row {
|
|
display: flex; gap: 0;
|
|
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
|
border-radius: 8px; padding: 3px; overflow: hidden;
|
|
}
|
|
|
|
.tab-btn {
|
|
flex: 1; padding: 0.625rem; text-align: center;
|
|
background: none; border: none; border-radius: 6px;
|
|
font-size: 0.8125rem; font-weight: 600; font-family: var(--font-primary);
|
|
color: var(--text-tertiary); cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
.tab-btn.active { background: var(--purple-primary); color: white; }
|
|
|
|
@media (max-width: 768px) { .tabs { display: block; } }
|
|
|
|
|
|
/* ============================================================
|
|
13. CONFIG / CODE PANELS
|
|
============================================================ */
|
|
|
|
.config-panel {
|
|
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
|
border-radius: 8px; overflow: hidden;
|
|
}
|
|
|
|
.config-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.config-title { font-size: 0.8125rem; font-weight: 600; }
|
|
.config-actions { display: flex; gap: 0.5rem; }
|
|
|
|
.config-code {
|
|
padding: 1rem; font-family: var(--font-mono); font-size: 0.6875rem;
|
|
color: var(--text-secondary); line-height: 1.7;
|
|
max-height: 75vh; overflow-y: auto; white-space: pre;
|
|
}
|
|
.config-code::-webkit-scrollbar { width: 5px; }
|
|
.config-code::-webkit-scrollbar-track { background: transparent; }
|
|
.config-code::-webkit-scrollbar-thumb { background: var(--purple-primary); border-radius: 3px; }
|
|
|
|
/* Config example block */
|
|
.config-example {
|
|
margin-top: 1rem; padding: 1rem;
|
|
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
}
|
|
.config-label { font-size: 0.6875rem; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
|
|
|
|
/* ============================================================
|
|
14. FEEDBACK: TOASTS, ERRORS, RESULTS, NOTICES
|
|
============================================================ */
|
|
|
|
/* --- 14a. Toast notification --- */
|
|
.toast {
|
|
position: fixed; bottom: 1.5rem; left: 50%;
|
|
transform: translateX(-50%) translateY(100px);
|
|
padding: 1rem 1.5rem;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px; box-shadow: var(--shadow-lg);
|
|
font-size: 0.875rem; color: var(--text-primary);
|
|
z-index: 1000; transition: transform var(--transition-base);
|
|
}
|
|
|
|
.toast.show { transform: translateX(-50%) translateY(0); }
|
|
.toast.hidden { display: none; }
|
|
|
|
/* --- 14b. Error box --- */
|
|
.error-box {
|
|
padding: 1rem;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
border-radius: 8px;
|
|
color: var(--error);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* --- 14c. Result messages --- */
|
|
.result {
|
|
margin-top: 1.25rem; padding: 1rem;
|
|
border-radius: 8px; font-size: 0.875rem;
|
|
}
|
|
.result-ok { background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); color: var(--success); }
|
|
.result-err { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); color: var(--error); }
|
|
|
|
.result-ok a {
|
|
color: var(--purple-light); text-decoration: none;
|
|
font-weight: 600;
|
|
}
|
|
.result-ok a:hover { text-decoration: underline; }
|
|
|
|
/* --- 14d. Info box --- */
|
|
.info-box {
|
|
padding: 1rem; background: rgba(139, 92, 246, 0.06);
|
|
border: 1px solid rgba(139, 92, 246, 0.2);
|
|
border-radius: 8px; margin-bottom: 1.5rem;
|
|
}
|
|
.info-title { font-size: 0.875rem; font-weight: 600; color: var(--purple-light); margin-bottom: 0.5rem; }
|
|
.info-text { font-size: 0.8125rem; color: var(--text-secondary); line-height: 1.6; }
|
|
|
|
/* --- 14e. Notice banner --- */
|
|
.notice {
|
|
padding: 1rem; background: rgba(139, 92, 246, 0.06);
|
|
border: 1px solid rgba(139, 92, 246, 0.2);
|
|
border-radius: 8px; margin-bottom: 1.5rem;
|
|
}
|
|
.notice-title { font-size: 0.875rem; font-weight: 600; color: var(--purple-light); margin-bottom: 0.375rem; }
|
|
.notice-text { font-size: 0.75rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 0.5rem; }
|
|
|
|
/* --- 14f. Contribute banner --- */
|
|
.contribute-banner {
|
|
margin-top: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(139, 92, 246, 0.08);
|
|
border: 1px solid rgba(139, 92, 246, 0.25);
|
|
border-radius: 8px;
|
|
animation: fadeIn var(--transition-base);
|
|
}
|
|
|
|
.contribute-text {
|
|
font-size: 0.8125rem; color: var(--text-secondary);
|
|
margin-bottom: 0.625rem; line-height: 1.5;
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
15. STICKY BOTTOM BAR
|
|
============================================================ */
|
|
.bottom-bar {
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|
|
padding: 1rem 1.5rem;
|
|
background: var(--bg-primary);
|
|
border-top: 1px solid var(--border-color);
|
|
display: flex; justify-content: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
.bottom-bar .btn { max-width: 700px; width: 100%; }
|
|
|
|
.btn-count {
|
|
font-size: 0.75rem; opacity: 0.8;
|
|
font-weight: 400;
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
16. AUTOCOMPLETE DROPDOWN
|
|
============================================================ */
|
|
.autocomplete-wrapper { position: relative; }
|
|
|
|
.autocomplete-dropdown {
|
|
position: absolute; top: 100%; left: 0; right: 0;
|
|
margin-top: 0.5rem;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
max-height: 300px; overflow-y: auto;
|
|
z-index: 100; box-shadow: var(--shadow-lg);
|
|
display: none;
|
|
}
|
|
|
|
.autocomplete-dropdown.open { display: block; }
|
|
|
|
.autocomplete-item {
|
|
padding: 0.75rem 1rem; cursor: pointer;
|
|
transition: background-color var(--transition-fast);
|
|
font-size: 0.875rem;
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
}
|
|
|
|
.autocomplete-item:hover { background: var(--bg-tertiary); }
|
|
.autocomplete-item.selected { background: var(--bg-tertiary); border-left: 2px solid var(--purple-primary); }
|
|
.autocomplete-item.disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
|
|
|
|
.autocomplete-item .item-type {
|
|
font-size: 0.6875rem; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
padding: 0.125rem 0.375rem; border-radius: 3px;
|
|
flex-shrink: 0; min-width: 48px; text-align: center;
|
|
}
|
|
|
|
.item-type-preset { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
|
|
.item-type-brand { background: rgba(16, 185, 129, 0.15); color: var(--success); }
|
|
.item-type-model { background: rgba(139, 92, 246, 0.15); color: var(--purple-primary); }
|
|
|
|
|
|
/* ============================================================
|
|
17. LATENCY COLORS
|
|
============================================================ */
|
|
.latency-fast { color: var(--success); }
|
|
.latency-medium { color: var(--warning); }
|
|
.latency-slow { color: var(--error); }
|
|
|
|
|
|
/* ============================================================
|
|
18. ANIMATIONS
|
|
============================================================ */
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
@keyframes cardIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
20%, 60% { transform: translateX(-4px); }
|
|
40%, 80% { transform: translateX(4px); }
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
DEMO PAGE STYLES (only for this design-system page)
|
|
============================================================ */
|
|
.ds-section {
|
|
margin-bottom: 3rem;
|
|
padding-bottom: 2rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.ds-section:last-child { border-bottom: none; }
|
|
|
|
.ds-title {
|
|
font-size: 1.125rem; font-weight: 700;
|
|
color: var(--purple-light);
|
|
margin-bottom: 0.25rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.ds-subtitle {
|
|
font-size: 0.75rem; color: var(--text-tertiary);
|
|
margin-bottom: 1.5rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.ds-row {
|
|
display: flex; flex-wrap: wrap; gap: 1rem;
|
|
align-items: flex-start;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.ds-col {
|
|
flex: 1; min-width: 280px;
|
|
}
|
|
|
|
.ds-label {
|
|
font-size: 0.625rem; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.08em;
|
|
color: var(--text-tertiary);
|
|
margin-bottom: 0.5rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.ds-spacer { height: 1rem; }
|
|
|
|
.ds-demo {
|
|
padding: 1.5rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.ds-inline { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
|
|
|
|
.ds-page { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
|
|
.ds-hero {
|
|
text-align: center; margin-bottom: 3rem; padding: 2rem 0;
|
|
}
|
|
|
|
.ds-hero-title {
|
|
font-size: 1.5rem; font-weight: 700;
|
|
background: linear-gradient(135deg, var(--purple-light), var(--purple-primary));
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.ds-hero-sub { font-size: 0.875rem; color: var(--text-tertiary); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="ds-page">
|
|
<div class="ds-hero">
|
|
<h1 class="ds-hero-title">Strix Design System</h1>
|
|
<p class="ds-hero-sub">All components used across the Strix frontend</p>
|
|
</div>
|
|
|
|
<!-- ==================== COLORS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">1. Colors</div>
|
|
<div class="ds-subtitle">CSS variables from :root</div>
|
|
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Backgrounds</div>
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--bg-primary); border:1px solid var(--border-color);" title="--bg-primary"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--bg-secondary);" title="--bg-secondary"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--bg-tertiary);" title="--bg-tertiary"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--bg-elevated);" title="--bg-elevated"></div>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Purple Accent</div>
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--purple-dark);" title="--purple-dark"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--purple-primary);" title="--purple-primary"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--purple-light);" title="--purple-light"></div>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Semantic</div>
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--success);" title="--success"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--warning);" title="--warning"></div>
|
|
<div style="width:60px; height:60px; border-radius:8px; background:var(--error);" title="--error"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Text Hierarchy</div>
|
|
<div style="margin-bottom:0.25rem; color:var(--text-primary);">--text-primary: Main content text</div>
|
|
<div style="margin-bottom:0.25rem; color:var(--text-secondary); font-size:0.875rem;">--text-secondary: Labels, descriptions</div>
|
|
<div style="margin-bottom:0.25rem; color:var(--text-tertiary); font-size:0.875rem;">--text-tertiary: Hints, placeholders</div>
|
|
<div style="color:var(--text-disabled); font-size:0.875rem;">--text-disabled: Disabled elements</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== TYPOGRAPHY ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">2. Typography</div>
|
|
<div class="ds-subtitle">Headings, labels, mono text</div>
|
|
|
|
<div class="ds-demo">
|
|
<h1 class="title" style="margin-bottom:1rem;">STRIX</h1>
|
|
<div class="page-title" style="margin-bottom:0.75rem;">Page Title (.page-title)</div>
|
|
<div class="screen-title" style="margin-bottom:0.75rem;">Screen Title (.screen-title)</div>
|
|
<div class="section-title" style="margin-bottom:0.75rem;">Section Title (.section-title)</div>
|
|
<div class="label" style="margin-bottom:0.5rem;">Label (.label)</div>
|
|
<div class="field-label">Field Label (.field-label)</div>
|
|
<div class="ds-spacer"></div>
|
|
<div style="font-family:var(--font-mono); font-size:0.75rem; color:var(--text-secondary);">Monospace text (--font-mono)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== BUTTONS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">3. Buttons</div>
|
|
<div class="ds-subtitle">All button variants</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-label">Primary</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<button class="btn btn-primary">Primary Button</button>
|
|
<button class="btn btn-primary" disabled>Disabled</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Primary Large (full width)</div>
|
|
<div style="max-width:400px; margin-bottom:1.5rem;">
|
|
<button class="btn btn-primary btn-large">Discover Streams</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Outline</div>
|
|
<div style="max-width:400px; margin-bottom:1.5rem;">
|
|
<button class="btn-outline">Try Standard Discovery</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Small Buttons</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<button class="btn-sm">Copy</button>
|
|
<button class="btn-sm">Download</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Stop / Danger</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<button class="btn-stop">Cancel</button>
|
|
<button class="btn-stop" disabled>Disabled</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Add Sub / Secondary Action</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<button class="btn-add-sub">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
Add Sub Stream
|
|
</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Save (Green)</div>
|
|
<div style="max-width:300px; margin-bottom:1.5rem;">
|
|
<button class="btn-save">Save to Frigate & Restart</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Back Navigation</div>
|
|
<button class="btn-back" style="margin-bottom:0;">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M12 4L6 10l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
Back
|
|
</button>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Add (+) Button</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<button class="btn-add" style="width:48px;">+</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Use as Stream Button (card action)</div>
|
|
<div style="max-width:300px;">
|
|
<button class="btn-use">Use as Main Stream</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== FORM ELEMENTS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">4. Form Elements</div>
|
|
<div class="ds-subtitle">Inputs, selects, toggles</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Standard Input</div>
|
|
<div class="form-group">
|
|
<label class="label">
|
|
Network Address
|
|
<span class="info-icon">
|
|
<svg viewBox="0 0 16 16" fill="none">
|
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
|
|
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
<div class="tooltip">
|
|
<div class="tooltip-title">Network Address</div>
|
|
<p class="tooltip-text">Enter the IP address of your camera.</p>
|
|
<div class="tooltip-examples">
|
|
<div class="tooltip-examples-title">Examples:</div>
|
|
<code class="tooltip-example">192.168.1.100</code>
|
|
<code class="tooltip-example">10.0.0.50</code>
|
|
</div>
|
|
</div>
|
|
</span>
|
|
</label>
|
|
<input type="text" class="input" placeholder="192.168.1.100" value="">
|
|
<p class="hint">IP address or stream URL</p>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Large Input</div>
|
|
<div class="form-group">
|
|
<input type="text" class="input input-large" placeholder="Large input variant">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Validated Input (readonly)</div>
|
|
<div class="form-group">
|
|
<div class="input-validated">
|
|
<input type="text" class="input" value="192.168.1.100" readonly>
|
|
<svg class="icon-check" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M4 10l4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Password with Toggle</div>
|
|
<div class="form-group">
|
|
<div class="input-password">
|
|
<input type="password" class="input" placeholder="Camera password" value="secret123">
|
|
<button class="btn-toggle-pass" type="button">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
<circle cx="12" cy="12" r="3"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Select Dropdown</div>
|
|
<div class="form-group">
|
|
<div class="field-label">HW Acceleration</div>
|
|
<select class="input">
|
|
<option value="">auto</option>
|
|
<option value="preset-vaapi">VAAPI</option>
|
|
<option value="preset-nvidia-h264">NVIDIA H264</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Small Inputs in Row</div>
|
|
<div class="row">
|
|
<div class="form-group"><div class="field-label">FPS</div><input class="input" style="max-width:120px;" type="number" placeholder="5"></div>
|
|
<div class="form-group"><div class="field-label">Width</div><input class="input" style="max-width:120px;" type="number" placeholder="1920"></div>
|
|
<div class="form-group"><div class="field-label">Height</div><input class="input" style="max-width:120px;" type="number" placeholder="1080"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-label">Add Row (input + button)</div>
|
|
<div class="add-row" style="max-width:500px; margin-bottom:1.5rem;">
|
|
<input class="add-input" type="text" placeholder="rtsp://user:pass@host/path">
|
|
<button class="btn-add">+</button>
|
|
</div>
|
|
|
|
<div class="ds-label">Toggle Switches</div>
|
|
<div style="max-width:300px;">
|
|
<div class="toggle-row"><span class="toggle-label">Enabled (on)</span><div class="toggle on" id="demo-toggle-1"></div></div>
|
|
<div class="toggle-row"><span class="toggle-label">Disabled (off)</span><div class="toggle" id="demo-toggle-2"></div></div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
<div class="ds-label">Optional Label</div>
|
|
<div class="form-group">
|
|
<label class="label">Camera Model <span class="optional">(optional)</span></label>
|
|
<input type="text" class="input" placeholder="Search brand, model or preset...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== BADGES & TAGS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">5. Badges & Tags</div>
|
|
<div class="ds-subtitle">Status indicators, type labels</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-label">Type Badges</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<span class="type-badge type-standard">standard</span>
|
|
<span class="type-badge type-homekit">homekit</span>
|
|
<span class="type-badge type-unreachable">unreachable</span>
|
|
</div>
|
|
|
|
<div class="ds-label">Status Badges</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<div class="status-badge running"><div class="status-dot"></div><span>running</span></div>
|
|
<div class="status-badge done"><div class="status-dot"></div><span>done</span></div>
|
|
</div>
|
|
|
|
<div class="ds-label">Mode Badge</div>
|
|
<div style="margin-bottom:1.5rem;">
|
|
<span style="font-size:1.25rem; font-weight:600;">Stream Testing</span>
|
|
<span class="mode-badge">sub</span>
|
|
</div>
|
|
|
|
<div class="ds-label">Tags (selected items)</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<span class="tag">
|
|
<span class="tag-type">preset</span>
|
|
Top 1000 Stream Patterns
|
|
<button class="tag-remove" type="button">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
</button>
|
|
</span>
|
|
<span class="tag">
|
|
<span class="tag-type">brand</span>
|
|
Hikvision
|
|
<button class="tag-remove" type="button">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="ds-label">Card Tags (codec overlays)</div>
|
|
<div class="ds-inline" style="margin-bottom:1.5rem;">
|
|
<span class="card-tag">H264</span>
|
|
<span class="card-tag">AAC</span>
|
|
<span class="card-tag">MJPEG</span>
|
|
</div>
|
|
|
|
<div class="ds-label">Latency Colors</div>
|
|
<div class="ds-inline">
|
|
<span class="latency-fast" style="font-family:var(--font-mono); font-size:0.75rem;">42ms (fast)</span>
|
|
<span class="latency-medium" style="font-family:var(--font-mono); font-size:0.75rem;">320ms (medium)</span>
|
|
<span class="latency-slow" style="font-family:var(--font-mono); font-size:0.75rem;">850ms (slow)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== CARDS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">6. Cards</div>
|
|
<div class="ds-subtitle">Stream cards, media cards</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-label">Stream Card (info)</div>
|
|
<div style="max-width:500px; margin-bottom:1.5rem;">
|
|
<div class="stream-card">
|
|
<div class="stream-label">Main Stream</div>
|
|
<div class="stream-url"><span class="scheme">rtsp://</span>admin:pass@192.168.1.100:554/stream1</div>
|
|
</div>
|
|
<div class="stream-card">
|
|
<div class="stream-label">Sub Stream</div>
|
|
<div class="stream-url"><span class="scheme">rtsp://</span>admin:pass@192.168.1.100:554/stream2</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-label">Media Card (test result)</div>
|
|
<div class="cards-grid" style="max-width:700px;">
|
|
<div class="card">
|
|
<div class="card-thumb">
|
|
<div class="card-thumb-placeholder">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="2" y="2" width="20" height="20" rx="2"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="m21 15-5-5L5 21"/>
|
|
</svg>
|
|
</div>
|
|
<div class="card-overlay">
|
|
<span class="card-tag">H264</span>
|
|
<span class="card-tag">AAC</span>
|
|
</div>
|
|
<span class="card-resolution">1920x1080</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="card-url"><span class="scheme">rtsp://</span>admin:***@192.168.1.100:554/stream1</div>
|
|
<div class="card-meta">
|
|
<span class="card-meta-item latency-fast">42ms</span>
|
|
<span class="card-meta-item">1920x1080</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-action">
|
|
<button class="btn-use">Use as Main Stream</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-thumb">
|
|
<div class="card-thumb-placeholder">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="2" y="2" width="20" height="20" rx="2"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="m21 15-5-5L5 21"/>
|
|
</svg>
|
|
</div>
|
|
<div class="card-overlay">
|
|
<span class="card-tag">H265</span>
|
|
</div>
|
|
<span class="card-resolution">640x480</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="card-url"><span class="scheme">rtsp://</span>admin:***@192.168.1.100:554/stream2</div>
|
|
<div class="card-meta">
|
|
<span class="card-meta-item latency-medium">280ms</span>
|
|
<span class="card-meta-item">640x480</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-action">
|
|
<button class="btn-use">Use as Sub Stream</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== PROGRESS & STATUS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">7. Progress & Status</div>
|
|
<div class="ds-subtitle">Status bars, counters, progress, spinners</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-label">Status Bar (complete component)</div>
|
|
<div class="status-bar" style="max-width:800px;">
|
|
<div class="status-top">
|
|
<div class="status-badge running"><div class="status-dot"></div><span>running</span></div>
|
|
<span class="status-session">sess_abc123</span>
|
|
</div>
|
|
<div class="counters">
|
|
<div class="counter total"><div class="counter-value">48</div><div class="counter-label">total</div></div>
|
|
<div class="counter tested"><div class="counter-value">32</div><div class="counter-label">tested</div></div>
|
|
<div class="counter alive"><div class="counter-value">5</div><div class="counter-label">alive</div></div>
|
|
<div class="counter screenshots"><div class="counter-value">3</div><div class="counter-label">screenshots</div></div>
|
|
</div>
|
|
<div class="progress-track">
|
|
<div class="progress-fill" style="width:67%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Loading Spinner</div>
|
|
<div class="loading">
|
|
<div class="loading-spinner"></div>
|
|
Building stream URLs...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== COLLAPSIBLE ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">8. Collapsible Sections</div>
|
|
<div class="ds-subtitle">Expandable settings, grouped results</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-label">Expand Button (settings)</div>
|
|
<button class="expand-btn" id="demo-expand">
|
|
<svg class="chevron" viewBox="0 0 12 12" fill="none"><path d="M4.5 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
Advanced Settings
|
|
</button>
|
|
<div class="expandable" id="demo-expandable">
|
|
<div class="section-title">FFmpeg</div>
|
|
<div class="form-group"><div class="field-label">Hardware Acceleration</div><input class="input" placeholder="auto"></div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Group with Collapse (results)</div>
|
|
<div class="group" id="demo-group">
|
|
<div class="group-header" id="demo-group-header">
|
|
<button class="group-toggle" type="button">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="chevron"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
</button>
|
|
<span class="group-title">Recommended - Main</span>
|
|
<span class="group-count">(3)</span>
|
|
</div>
|
|
<div class="group-content">
|
|
<div style="padding:1rem; color:var(--text-tertiary); font-size:0.875rem;">Cards would go here...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== FEEDBACK ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">9. Feedback</div>
|
|
<div class="ds-subtitle">Toasts, errors, results, info boxes, notices</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Error Box</div>
|
|
<div class="error-box">Connection error: timeout after 5s</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Info Box</div>
|
|
<div class="info-box" style="margin-bottom:0;">
|
|
<div class="info-title">How to use</div>
|
|
<div class="info-text">Add these URLs to your NVR software.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Result OK</div>
|
|
<div class="result result-ok" style="margin-top:0;">Added to go2rtc: camera_main. <a href="#">Open go2rtc UI</a></div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Result Error</div>
|
|
<div class="result result-err" style="margin-top:0;">camera_main: go2rtc not found</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Notice Banner</div>
|
|
<div class="notice" style="margin-bottom:0;">
|
|
<div class="notice-title">Frigate NVR not detected</div>
|
|
<div class="notice-text">If you have Frigate NVR, we recommend running Strix on the same server.</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Toast (click to demo)</div>
|
|
<button class="btn-sm" id="demo-toast-btn">Show Toast</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== CONFIG PANEL ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">10. Config Panel</div>
|
|
<div class="ds-subtitle">Code display with diff highlighting</div>
|
|
|
|
<div class="config-panel" style="max-width:600px;">
|
|
<div class="config-header">
|
|
<span class="config-title">Generated Config</span>
|
|
<div class="config-actions">
|
|
<button class="btn-sm">Copy</button>
|
|
<button class="btn-sm">Download</button>
|
|
</div>
|
|
</div>
|
|
<div class="config-code">go2rtc:
|
|
streams:
|
|
camera_main:
|
|
- rtsp://admin:pass@192.168.1.100:554/stream1
|
|
camera_sub:
|
|
- rtsp://admin:pass@192.168.1.100:554/stream2
|
|
|
|
cameras:
|
|
camera:
|
|
ffmpeg:
|
|
inputs:
|
|
- path: rtsp://127.0.0.1:8554/camera_sub
|
|
input_args: preset-rtsp-restream
|
|
roles:
|
|
- detect
|
|
- path: rtsp://127.0.0.1:8554/camera_main
|
|
input_args: preset-rtsp-restream
|
|
roles:
|
|
- record</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Config Example Block (copyable)</div>
|
|
<div class="config-example" style="max-width:600px;">
|
|
<div class="config-label">go2rtc.yaml</div>
|
|
<div class="config-code" style="padding:0; max-height:none;">streams:
|
|
'camera_main':
|
|
- rtsp://admin:pass@192.168.1.100:554/stream1</div>
|
|
<button class="btn-copy-block">Copy</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== LISTS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">11. Lists & Tables</div>
|
|
<div class="ds-subtitle">Stream lists, device info tables</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-row">
|
|
<div class="ds-col">
|
|
<div class="ds-label">Streams Box (scrollable list)</div>
|
|
<div class="stream-count"><span>3 streams</span></div>
|
|
<div class="streams-box" style="max-height:200px;">
|
|
<div class="stream-url-item"><span class="scheme">rtsp://</span>admin:pass@192.168.1.100:554/stream1</div>
|
|
<div class="stream-url-item"><span class="scheme">rtsp://</span>admin:pass@192.168.1.100:554/stream2</div>
|
|
<div class="stream-url-item"><span class="scheme">http://</span>192.168.1.100:80/video.cgi</div>
|
|
</div>
|
|
</div>
|
|
<div class="ds-col">
|
|
<div class="ds-label">Device Info Table</div>
|
|
<div class="device-info">
|
|
<div class="device-row"><span class="device-label">IP Address</span><span class="device-value">192.168.1.100</span></div>
|
|
<div class="device-row"><span class="device-label">Model</span><span class="device-value">DS-2CD2043G2-I</span></div>
|
|
<div class="device-row"><span class="device-label">Vendor</span><span class="device-value">Hikvision</span></div>
|
|
<div class="device-row"><span class="device-label">MAC</span><span class="device-value">AA:BB:CC:DD:EE:FF</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ds-spacer"></div>
|
|
|
|
<div class="ds-label">Stream URL with Copy Button</div>
|
|
<div class="stream-card" style="max-width:500px;">
|
|
<div class="stream-label">Main Stream</div>
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<div class="stream-url" style="flex:1;"><span class="scheme">rtsp://</span>admin:pass@192.168.1.100:554/stream1</div>
|
|
<button class="btn-copy-sm" type="button">
|
|
<svg viewBox="0 0 16 16" fill="none">
|
|
<rect x="5" y="5" width="9" height="9" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
|
<path d="M11 5V3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h2" stroke="currentColor" stroke-width="1.5"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== MOBILE TABS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">12. Mobile Tabs</div>
|
|
<div class="ds-subtitle">Tab switcher for two-column layouts on mobile</div>
|
|
|
|
<div class="ds-demo">
|
|
<div style="max-width:400px;">
|
|
<div class="tabs-row">
|
|
<button class="tab-btn active">Settings</button>
|
|
<button class="tab-btn">Config</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== BOTTOM BAR ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">13. Sticky Bottom Bar</div>
|
|
<div class="ds-subtitle">Fixed action bar at page bottom (not shown fixed in this demo)</div>
|
|
|
|
<div class="ds-demo">
|
|
<div style="padding:1rem 1.5rem; background:var(--bg-primary); border:1px solid var(--border-color); border-radius:8px; display:flex; justify-content:center;">
|
|
<button class="btn btn-primary" style="max-width:700px; width:100%;">
|
|
Test Streams <span class="btn-count">(12)</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== AUTOCOMPLETE ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">14. Autocomplete Dropdown</div>
|
|
<div class="ds-subtitle">Searchable dropdown with type badges</div>
|
|
|
|
<div class="ds-demo">
|
|
<div style="max-width:500px; position:relative;">
|
|
<input type="text" class="input" placeholder="Search brand, model or preset..." value="hikvi">
|
|
<div class="autocomplete-dropdown" style="display:block; position:relative; margin-top:0.5rem;">
|
|
<div style="display:flex; flex-wrap:wrap; gap:0.375rem; padding:0.625rem 0.75rem; border-bottom:1px solid var(--border-color);">
|
|
<span class="tag">
|
|
<span class="tag-type">preset</span>
|
|
Top 1000
|
|
<button class="tag-remove" type="button"><svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
|
|
</span>
|
|
</div>
|
|
<div class="autocomplete-item">
|
|
<span class="item-type item-type-brand">brand</span>
|
|
Hikvision
|
|
</div>
|
|
<div class="autocomplete-item">
|
|
<span class="item-type item-type-model">model</span>
|
|
Hikvision DS-2CD2043G2-I
|
|
</div>
|
|
<div class="autocomplete-item">
|
|
<span class="item-type item-type-model">model</span>
|
|
Hikvision DS-2CD2347G2-LU
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== SVG ICONS ==================== -->
|
|
<div class="ds-section">
|
|
<div class="ds-title">15. SVG Icons</div>
|
|
<div class="ds-subtitle">All inline SVG icons used across pages (never use emoji or icon fonts)</div>
|
|
|
|
<div class="ds-demo">
|
|
<div class="ds-inline" style="gap:1.5rem;">
|
|
<div style="text-align:center;">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="color:var(--text-secondary);"><path d="M12 4L6 10l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">back</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="color:var(--text-secondary);"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/><path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">info</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="color:var(--text-secondary);"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">add</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="color:var(--text-secondary);"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">close</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="color:var(--text-secondary);"><path d="M4.5 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">chevron</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="color:var(--text-secondary);"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">chevron-down</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="color:var(--text-secondary);"><path d="M4 10l4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">check</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="color:var(--text-secondary);">
|
|
<rect x="5" y="5" width="9" height="9" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
|
<path d="M11 5V3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h2" stroke="currentColor" stroke-width="1.5"/>
|
|
</svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">copy</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--text-secondary);">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
|
|
</svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">eye</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="color:var(--text-secondary);">
|
|
<rect x="2" y="2" width="20" height="20" rx="2"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="m21 15-5-5L5 21"/>
|
|
</svg>
|
|
<div style="font-size:0.5rem; color:var(--text-tertiary); margin-top:0.25rem;">image</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast element (required on every page) -->
|
|
<div id="toast" class="toast hidden"></div>
|
|
|
|
<script>
|
|
/* ================================================================
|
|
JAVASCRIPT PATTERNS
|
|
All Strix pages use these same patterns. Copy and adapt.
|
|
================================================================ */
|
|
|
|
// --- Toggle switches ---
|
|
document.querySelectorAll('.toggle').forEach(function(el) {
|
|
el.addEventListener('click', function() {
|
|
el.classList.toggle('on');
|
|
});
|
|
});
|
|
|
|
// --- Expand/collapse buttons ---
|
|
document.getElementById('demo-expand').addEventListener('click', function() {
|
|
this.classList.toggle('open');
|
|
document.getElementById('demo-expandable').classList.toggle('open');
|
|
});
|
|
|
|
// --- Group collapse ---
|
|
document.getElementById('demo-group-header').addEventListener('click', function() {
|
|
document.getElementById('demo-group').classList.toggle('collapsed');
|
|
});
|
|
|
|
// --- Toast ---
|
|
document.getElementById('demo-toast-btn').addEventListener('click', function() {
|
|
showToast('Stream URL copied to clipboard');
|
|
});
|
|
|
|
// --- Password toggle ---
|
|
document.querySelector('.btn-toggle-pass').addEventListener('click', function() {
|
|
var input = document.querySelector('.input-password .input');
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
});
|
|
|
|
/* ================================================================
|
|
STANDARD TOAST FUNCTION
|
|
Include this on every page. Always use the same pattern.
|
|
================================================================ */
|
|
function showToast(msg, duration) {
|
|
var t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.classList.remove('hidden');
|
|
t.classList.add('show');
|
|
setTimeout(function() {
|
|
t.classList.remove('show');
|
|
setTimeout(function() { t.classList.add('hidden'); }, 250);
|
|
}, duration || 3000);
|
|
}
|
|
|
|
/* ================================================================
|
|
STANDARD NAVIGATION PATTERN
|
|
Always pass ALL known data to the next page via URL params.
|
|
|
|
Example:
|
|
function navigateToTest(sessionId) {
|
|
var p = new URLSearchParams();
|
|
p.set('id', sessionId);
|
|
if (ip) p.set('ip', ip);
|
|
if (mac) p.set('mac', mac);
|
|
if (vendor) p.set('vendor', vendor);
|
|
if (model) p.set('model', model);
|
|
if (server) p.set('server', server);
|
|
if (hostname) p.set('hostname', hostname);
|
|
if (ports) p.set('ports', ports);
|
|
if (user) p.set('user', user);
|
|
if (channel) p.set('channel', channel);
|
|
if (ids) p.set('ids', ids);
|
|
window.location.href = 'test.html?' + p.toString();
|
|
}
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
STANDARD API CALL PATTERN
|
|
All API calls go to api/* endpoints. Handle errors consistently.
|
|
|
|
Example:
|
|
async function loadData() {
|
|
try {
|
|
var r = await fetch('api/endpoint?param=' + encodeURIComponent(value));
|
|
if (!r.ok) {
|
|
var text = await r.text();
|
|
showToast(text || 'Error ' + r.status);
|
|
return;
|
|
}
|
|
var data = await r.json();
|
|
// use data...
|
|
} catch (e) {
|
|
showToast('Connection error: ' + e.message);
|
|
}
|
|
}
|
|
|
|
POST example:
|
|
async function submitData(payload) {
|
|
try {
|
|
var r = await fetch('api/endpoint', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!r.ok) {
|
|
var text = await r.text();
|
|
showToast(text || 'Error ' + r.status);
|
|
return;
|
|
}
|
|
var data = await r.json();
|
|
// use data...
|
|
} catch (e) {
|
|
showToast('Connection error: ' + e.message);
|
|
}
|
|
}
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
STANDARD URL FORMAT HELPER
|
|
Used to colorize scheme:// prefix in stream URLs.
|
|
|
|
Example:
|
|
function formatURL(el, url) {
|
|
var schemeEnd = url.indexOf('://');
|
|
if (schemeEnd === -1) {
|
|
el.textContent = url;
|
|
return;
|
|
}
|
|
var scheme = document.createElement('span');
|
|
scheme.className = 'scheme';
|
|
scheme.textContent = url.substring(0, schemeEnd + 3);
|
|
el.appendChild(scheme);
|
|
el.appendChild(document.createTextNode(url.substring(schemeEnd + 3)));
|
|
}
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
STANDARD COPY TO CLIPBOARD
|
|
Example:
|
|
function copyText(text) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.left = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
}
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
PAGE INIT PATTERN
|
|
Every page starts by reading URL params:
|
|
|
|
var params = new URLSearchParams(location.search);
|
|
var ip = params.get('ip') || '';
|
|
var mac = params.get('mac') || '';
|
|
var vendor = params.get('vendor') || '';
|
|
var model = params.get('model') || '';
|
|
var server = params.get('server') || '';
|
|
var hostname = params.get('hostname') || '';
|
|
var ports = params.get('ports') || '';
|
|
var user = params.get('user') || '';
|
|
var channel = params.get('channel') || '';
|
|
|
|
Back button always goes to history.back() or a specific page:
|
|
document.getElementById('btn-back').addEventListener('click', function() {
|
|
history.back();
|
|
});
|
|
================================================================ */
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|