958 lines
49 KiB
HTML
958 lines
49 KiB
HTML
<!-- Synthwave Design System Showcase — reference for mail-server frontend -->
|
|
<!DOCTYPE html>
|
|
<html lang="en" class="h-full">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Angular Aria Synthwave Design System Showcase</title>
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<!-- FontAwesome Icons -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<!-- Google Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800;900&family=Rajdhani:wght@400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
orbitron: ['Orbitron', 'sans-serif'],
|
|
rajdhani: ['Rajdhani', 'sans-serif'],
|
|
mono: ['Fira Code', 'monospace'],
|
|
},
|
|
colors: {
|
|
synth: {
|
|
bg: 'var(--bg-color)',
|
|
pink: 'var(--primary-color)',
|
|
cyan: 'var(--secondary-color)',
|
|
purple: 'var(--accent-color)',
|
|
yellow: 'var(--highlight-color)',
|
|
dark: '#0a0518',
|
|
glass: 'rgba(15, 12, 35, 0.65)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
:root {
|
|
--primary-color: #ff007f;
|
|
--secondary-color: #00f0ff;
|
|
--accent-color: #7928ca;
|
|
--highlight-color: #ffbe0b;
|
|
--bg-color: #0d0221;
|
|
--grid-color: rgba(255, 0, 127, 0.4);
|
|
--glow-color: rgba(0, 240, 255, 0.6);
|
|
--blur-quality: 16px;
|
|
}
|
|
|
|
/* Color Theme Presets */
|
|
.theme-cyber-magenta {
|
|
--primary-color: #ff007f;
|
|
--secondary-color: #00f0ff;
|
|
--accent-color: #7928ca;
|
|
--highlight-color: #ffbe0b;
|
|
--grid-color: rgba(255, 0, 127, 0.4);
|
|
--glow-color: rgba(0, 240, 255, 0.6);
|
|
}
|
|
|
|
.theme-neon-cyan {
|
|
--primary-color: #00f0ff;
|
|
--secondary-color: #00ff88;
|
|
--accent-color: #0077ff;
|
|
--highlight-color: #e0f7fa;
|
|
--grid-color: rgba(0, 240, 255, 0.4);
|
|
--glow-color: rgba(0, 255, 136, 0.6);
|
|
}
|
|
|
|
.theme-sunset-orange {
|
|
--primary-color: #ff5e00;
|
|
--secondary-color: #ffb703;
|
|
--accent-color: #d62828;
|
|
--highlight-color: #fcbf49;
|
|
--grid-color: rgba(255, 94, 0, 0.4);
|
|
--glow-color: rgba(255, 183, 3, 0.6);
|
|
}
|
|
|
|
.theme-matrix-green {
|
|
--primary-color: #00ff66;
|
|
--secondary-color: #39ff14;
|
|
--accent-color: #008000;
|
|
--highlight-color: #ccff00;
|
|
--grid-color: rgba(0, 255, 102, 0.4);
|
|
--glow-color: rgba(57, 255, 20, 0.6);
|
|
}
|
|
|
|
/* High Contrast Mode Overrides */
|
|
.high-contrast {
|
|
--bg-color: #000000;
|
|
--primary-color: #ffff00 !important;
|
|
--secondary-color: #00ffff !important;
|
|
--accent-color: #ffffff !important;
|
|
--highlight-color: #ffffff !important;
|
|
--grid-color: rgba(0, 255, 255, 0.8) !important;
|
|
}
|
|
|
|
.high-contrast .glass-panel {
|
|
background: rgba(0, 0, 0, 0.95) !important;
|
|
border: 2px solid var(--secondary-color) !important;
|
|
box-shadow: 0 0 15px var(--secondary-color) !important;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: #f1f5f9;
|
|
font-family: 'Rajdhani', sans-serif;
|
|
overflow-x: hidden;
|
|
transition: background-color 0.4s ease;
|
|
}
|
|
|
|
/* Synthwave Background World */
|
|
.synth-stage {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Sun Graphic */
|
|
.synth-sun {
|
|
position: absolute;
|
|
top: 15%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 280px;
|
|
height: 280px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(to bottom, #ffbe0b, var(--primary-color), var(--accent-color));
|
|
box-shadow: 0 0 80px var(--primary-color), 0 0 120px var(--accent-color);
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Sun Bars Cutout Effect */
|
|
.synth-sun::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
background: repeating-linear-gradient(
|
|
to bottom,
|
|
transparent 0px,
|
|
transparent 6px,
|
|
var(--bg-color) 6px,
|
|
var(--bg-color) 12px
|
|
);
|
|
mask-image: linear-gradient(to bottom, transparent 40%, black 100%);
|
|
-webkit-mask-image: linear-gradient(to bottom, transparent 40%, black 100%);
|
|
}
|
|
|
|
/* Horizon Silhouette */
|
|
.synth-mountains {
|
|
position: absolute;
|
|
bottom: 45%;
|
|
width: 100%;
|
|
height: 160px;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath d='M0,120 L0,80 L150,110 L300,50 L450,95 L600,30 L750,85 L900,40 L1050,90 L1200,60 L1200,120 Z' fill='%23080214'/%3E%3C/svg%3E");
|
|
background-size: cover;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Perspective Grid Floor */
|
|
.grid-container {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 50%;
|
|
perspective: 450px;
|
|
z-index: 3;
|
|
mask-image: linear-gradient(to top, rgba(0,0,0,1) 60%, rgba(0,0,0,0) 100%);
|
|
-webkit-mask-image: linear-gradient(to top, rgba(0,0,0,1) 60%, rgba(0,0,0,0) 100%);
|
|
}
|
|
|
|
.grid-plane {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 200%;
|
|
top: 0;
|
|
left: 0;
|
|
transform: rotateX(75deg);
|
|
background-size: 50px 50px;
|
|
background-image:
|
|
linear-gradient(to right, var(--grid-color) 1px, transparent 1px),
|
|
linear-gradient(to bottom, var(--grid-color) 1px, transparent 1px);
|
|
animation: moveGrid 2s linear infinite;
|
|
}
|
|
|
|
@keyframes moveGrid {
|
|
0% { transform: rotateX(75deg) translateY(0); }
|
|
100% { transform: rotateX(75deg) translateY(50px); }
|
|
}
|
|
|
|
/* Glassmorphism Cards */
|
|
.glass-panel {
|
|
background: var(--synth-glass, rgba(15, 12, 35, 0.65));
|
|
backdrop-filter: blur(var(--blur-quality));
|
|
-webkit-backdrop-filter: blur(var(--blur-quality));
|
|
border: 1px solid rgba(0, 240, 255, 0.25);
|
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 15px rgba(255, 255, 255, 0.03);
|
|
border-radius: 16px;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.glass-panel:hover {
|
|
border-color: var(--secondary-color);
|
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.6), 0 0 15px var(--glow-color);
|
|
}
|
|
|
|
/* Neon Text & Glow Effects */
|
|
.neon-text-primary {
|
|
color: #ffffff;
|
|
text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color);
|
|
}
|
|
|
|
.neon-text-secondary {
|
|
color: #ffffff;
|
|
text-shadow: 0 0 5px var(--secondary-color), 0 0 15px var(--secondary-color);
|
|
}
|
|
|
|
.neon-border {
|
|
box-shadow: 0 0 10px var(--secondary-color);
|
|
}
|
|
|
|
/* Cyber Button Style */
|
|
.cyber-btn {
|
|
position: relative;
|
|
background: linear-gradient(135deg, rgba(255, 0, 127, 0.2), rgba(0, 240, 255, 0.2));
|
|
border: 1px solid var(--secondary-color);
|
|
color: #ffffff;
|
|
font-family: 'Orbitron', sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
transition: all 0.2s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.cyber-btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
|
transition: 0.5s;
|
|
}
|
|
|
|
.cyber-btn:hover::before {
|
|
left: 100%;
|
|
}
|
|
|
|
.cyber-btn:hover {
|
|
background: var(--secondary-color);
|
|
color: #0d0221;
|
|
box-shadow: 0 0 20px var(--secondary-color);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.cyber-btn:focus-visible {
|
|
outline: 3px solid var(--highlight-color);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
/* ARIA Inspector Focus Highlight */
|
|
.aria-inspect-active {
|
|
outline: 2px dashed var(--highlight-color) !important;
|
|
outline-offset: 4px;
|
|
animation: pulseAria 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulseAria {
|
|
0%, 100% { outline-color: var(--highlight-color); }
|
|
50% { outline-color: var(--primary-color); }
|
|
}
|
|
|
|
/* Custom Scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: rgba(13, 2, 33, 0.8);
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--primary-color);
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 10px var(--primary-color);
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--secondary-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="min-h-screen text-slate-100 flex flex-col justify-between relative selection:bg-pink-500 selection:text-white">
|
|
|
|
<!-- SYNTHWAVE ANIMATED BACKGROUND -->
|
|
<div class="synth-stage" aria-hidden="true">
|
|
<div class="synth-sun"></div>
|
|
<div class="synth-mountains"></div>
|
|
<div class="grid-container">
|
|
<div class="grid-plane"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ACCESSIBILITY LIVE ANNOUNCER REGION (Screen Reader Only) -->
|
|
<div id="aria-live-polite" class="sr-only" aria-live="polite" aria-atomic="true"></div>
|
|
<div id="aria-live-assertive" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
|
|
|
|
<!-- CONTENT WRAPPER -->
|
|
<div class="relative z-10 flex flex-col min-h-screen">
|
|
|
|
<!-- HEADER / NAVIGATION -->
|
|
<header class="p-4 lg:p-6 border-b border-cyan-500/20 glass-panel rounded-none border-t-0 border-x-0 sticky top-0 z-40 backdrop-blur-xl">
|
|
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
|
|
|
<!-- Logo & Branding -->
|
|
<div class="flex items-center space-x-3">
|
|
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-pink-500 to-cyan-400 flex items-center justify-center text-slate-950 font-black text-xl shadow-[0_0_15px_rgba(255,0,127,0.8)] font-orbitron">
|
|
A
|
|
</div>
|
|
<div>
|
|
<h1 class="text-2xl font-extrabold font-orbitron tracking-wider neon-text-primary">
|
|
ANGULAR <span class="text-cyan-400 font-normal">ARIA</span>
|
|
</h1>
|
|
<p class="text-xs text-cyan-300/70 font-mono tracking-widest uppercase">Synthwave Design System Showcase</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats & Diagnostics Indicator -->
|
|
<div class="flex items-center space-x-4 bg-slate-950/60 px-4 py-2 rounded-full border border-pink-500/30 text-xs font-mono">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></span>
|
|
<span class="text-slate-300">FPS: <span id="fps-counter" class="text-emerald-400 font-bold">60</span></span>
|
|
</div>
|
|
<span class="text-slate-600">|</span>
|
|
<div class="text-slate-300">CDK: <span class="text-cyan-400 font-bold">v18.2.0</span></div>
|
|
<span class="text-slate-600">|</span>
|
|
<div class="text-slate-300">A11Y: <span class="text-pink-400 font-bold">100%</span></div>
|
|
</div>
|
|
|
|
<!-- Controls & Palette Selector -->
|
|
<div class="flex items-center space-x-3">
|
|
<label for="palette-select" class="sr-only">Select Synthwave Theme Palette</label>
|
|
<div class="relative">
|
|
<select id="palette-select" onchange="changeTheme(this.value)" class="bg-slate-900/90 text-cyan-300 border border-cyan-500/40 rounded-lg px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-pink-500 cursor-pointer">
|
|
<option value="theme-cyber-magenta">💖 Cyber Magenta</option>
|
|
<option value="theme-neon-cyan">🩵 Neon Cyan</option>
|
|
<option value="theme-sunset-orange">🧡 Sunset Orange</option>
|
|
<option value="theme-matrix-green">💚 Matrix Green</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- High Contrast Toggle -->
|
|
<button onclick="toggleHighContrast()" id="hc-btn" aria-pressed="false" title="Toggle High Contrast Mode" class="px-3 py-1.5 rounded-lg border border-cyan-500/40 bg-slate-900/80 text-xs font-mono hover:bg-cyan-500/20 focus:outline-none focus:ring-2 focus:ring-pink-500 transition-all flex items-center gap-1.5">
|
|
<i class="fa-solid fa-circle-half-stroke text-yellow-400"></i>
|
|
<span>Contrast</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- MAIN SHOWCASE GRID -->
|
|
<main class="flex-grow max-w-7xl w-full mx-auto p-4 md:p-6 grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
|
|
<!-- LEFT COLUMN: CONTROLS & INSPECTOR (4 Cols) -->
|
|
<section class="lg:col-span-4 flex flex-col gap-6">
|
|
|
|
<!-- System Config & A11y Tools Panel -->
|
|
<div class="glass-panel p-5">
|
|
<h2 class="text-lg font-orbitron font-bold text-cyan-300 flex items-center gap-2 mb-4 border-b border-cyan-500/20 pb-2">
|
|
<i class="fa-solid fa-sliders text-pink-500"></i> System A11y Controls
|
|
</h2>
|
|
|
|
<div class="space-y-4 text-sm font-rajdhani">
|
|
<!-- Backdrop Blur Quality Slider -->
|
|
<div>
|
|
<div class="flex justify-between text-xs font-mono text-slate-300 mb-1">
|
|
<span>Backdrop Blur Filter</span>
|
|
<span id="blur-val" class="text-pink-400">16px</span>
|
|
</div>
|
|
<input type="range" min="0" max="30" value="16" oninput="updateBlur(this.value)" class="w-full accent-pink-500 bg-slate-900 rounded-lg h-2 cursor-pointer">
|
|
</div>
|
|
|
|
<!-- ARIA Inspector Toggle -->
|
|
<div class="flex items-center justify-between p-3 bg-slate-900/50 rounded-lg border border-pink-500/20">
|
|
<div>
|
|
<div class="font-bold text-slate-200">Inspect ARIA Roles</div>
|
|
<div class="text-xs text-slate-400">Highlight A11y attributes on hover</div>
|
|
</div>
|
|
<button id="inspect-toggle-btn" onclick="toggleAriaInspector()" aria-pressed="false" class="w-12 h-6 bg-slate-800 rounded-full border border-cyan-500/30 relative transition-colors focus:outline-none focus:ring-2 focus:ring-pink-500">
|
|
<span id="inspect-toggle-dot" class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-slate-400 transition-transform"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Animation Speed Control -->
|
|
<div class="flex items-center justify-between p-3 bg-slate-900/50 rounded-lg border border-pink-500/20">
|
|
<div>
|
|
<div class="font-bold text-slate-200">Grid Perspective Anim</div>
|
|
<div class="text-xs text-slate-400">Toggle retro grid motion</div>
|
|
</div>
|
|
<button id="anim-toggle-btn" onclick="toggleGridAnimation()" aria-pressed="true" class="px-3 py-1 rounded bg-pink-500/20 text-pink-300 border border-pink-500/40 text-xs font-mono font-bold hover:bg-pink-500/30">
|
|
ACTIVE
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- REAL-TIME ANNOUNCER LOG TERMINAL -->
|
|
<div class="glass-panel p-5 flex flex-col flex-grow min-h-[300px]">
|
|
<div class="flex items-center justify-between border-b border-cyan-500/20 pb-2 mb-3">
|
|
<h2 class="text-md font-orbitron font-bold text-pink-400 flex items-center gap-2">
|
|
<i class="fa-solid fa-terminal text-cyan-400"></i> LiveAnnouncer Stream
|
|
</h2>
|
|
<button onclick="clearAnnouncerLogs()" class="text-xs text-slate-400 hover:text-cyan-300 font-mono">Clear</button>
|
|
</div>
|
|
|
|
<!-- Log Stream Container -->
|
|
<div id="announcer-log-stream" class="flex-grow bg-slate-950/90 rounded-lg p-3 font-mono text-xs overflow-y-auto max-h-[260px] space-y-2 border border-cyan-500/20" role="log" aria-label="Accessibility Announcement Log">
|
|
<div class="text-slate-500 italic">[00:00:00] LiveAnnouncer initialized. Ready for ARIA events.</div>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<!-- RIGHT COLUMN: INTERACTIVE COMPONENTS SHOWCASE (8 Cols) -->
|
|
<section class="lg:col-span-8 flex flex-col gap-6">
|
|
|
|
<!-- 1. LIVE SYNTHWAVE METRIC CHART CARD -->
|
|
<div class="glass-panel p-6 relative overflow-hidden">
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-4">
|
|
<div>
|
|
<h2 class="text-xl font-orbitron font-bold neon-text-secondary">REAL-TIME DATA STREAM</h2>
|
|
<p class="text-xs text-slate-400 font-mono">SVG Reactive Chart with Pulsing Nodes & Screen Reader Readout</p>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button id="chart-pulse-btn" onclick="toggleChartStream()" class="cyber-btn px-4 py-1.5 rounded-lg text-xs font-bold">
|
|
<i class="fa-solid fa-play mr-1"></i> LIVE STREAM
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Interactive SVG Chart -->
|
|
<div class="relative w-full h-48 bg-slate-950/80 rounded-xl p-2 border border-cyan-500/30">
|
|
<svg id="synth-chart" class="w-full h-full overflow-visible" viewBox="0 0 500 150" aria-label="Real-time performance metric line chart" role="img">
|
|
<defs>
|
|
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="var(--secondary-color)" stop-opacity="0.4"/>
|
|
<stop offset="100%" stop-color="var(--primary-color)" stop-opacity="0.0"/>
|
|
</linearGradient>
|
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
<feGaussianBlur stdDeviation="4" result="blur" />
|
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
</filter>
|
|
</defs>
|
|
<!-- Grid Lines -->
|
|
<line x1="0" y1="30" x2="500" y2="30" stroke="rgba(0, 240, 255, 0.1)" stroke-dasharray="4"/>
|
|
<line x1="0" y1="75" x2="500" y2="75" stroke="rgba(0, 240, 255, 0.1)" stroke-dasharray="4"/>
|
|
<line x1="0" y1="120" x2="500" y2="120" stroke="rgba(0, 240, 255, 0.1)" stroke-dasharray="4"/>
|
|
|
|
<!-- Area Gradient -->
|
|
<polygon id="chart-area" points="0,150 0,75 100,40 200,90 300,30 400,80 500,45 500,150" fill="url(#chartGradient)" />
|
|
|
|
<!-- Glowing Path -->
|
|
<path id="chart-path" d="M 0 75 L 100 40 L 200 90 L 300 30 L 400 80 L 500 45" fill="none" stroke="var(--secondary-color)" stroke-width="3" filter="url(#glow)"/>
|
|
|
|
<!-- Active Data Points -->
|
|
<g id="chart-points">
|
|
<circle cx="0" cy="75" r="5" fill="var(--primary-color)" class="animate-pulse"/>
|
|
<circle cx="100" cy="40" r="5" fill="var(--secondary-color)"/>
|
|
<circle cx="200" cy="90" r="5" fill="var(--primary-color)"/>
|
|
<circle cx="300" cy="30" r="5" fill="var(--secondary-color)"/>
|
|
<circle cx="400" cy="80" r="5" fill="var(--primary-color)"/>
|
|
<circle cx="500" cy="45" r="6" fill="var(--highlight-color)" stroke="#ffffff" stroke-width="2"/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="mt-3 flex justify-between items-center text-xs font-mono text-slate-400">
|
|
<span>Current Frequency: <strong id="chart-val" class="text-cyan-300">142.8 MHz</strong></span>
|
|
<span class="text-pink-400"><i class="fa-solid fa-wifi animate-pulse mr-1"></i> Synthwave Telemetry Connected</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. ACCESSIBLE SYNTHWAVE ARIA COMPONENTS GRID -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
<!-- Custom Accessible Dropdown / Aria Select Component -->
|
|
<div class="glass-panel p-5 flex flex-col justify-between">
|
|
<div>
|
|
<h3 class="text-md font-orbitron font-bold text-cyan-300 mb-1">ACCESSIBLE ARIA SELECT</h3>
|
|
<p class="text-xs text-slate-400 mb-4">Full Keyboard Navigation (Up/Down/Esc/Enter)</p>
|
|
|
|
<!-- Custom Select Widget -->
|
|
<div class="relative">
|
|
<label id="synth-select-label" class="block text-xs font-mono text-pink-400 mb-1">Select Synth Preset Profile:</label>
|
|
|
|
<button id="synth-select-btn"
|
|
type="button"
|
|
aria-haspopup="listbox"
|
|
aria-expanded="false"
|
|
aria-labelledby="synth-select-label synth-select-btn"
|
|
onclick="toggleCustomSelect()"
|
|
onkeydown="handleSelectKeydown(event)"
|
|
class="w-full bg-slate-950/80 border border-cyan-500/40 rounded-xl px-4 py-2.5 text-left font-rajdhani text-sm flex items-center justify-between text-slate-200 focus:outline-none focus:ring-2 focus:ring-pink-500 hover:border-cyan-400 transition-all">
|
|
<span id="select-current-val" class="font-semibold text-cyan-300">Outrun Cyberpunk '84</span>
|
|
<i class="fa-solid fa-chevron-down text-pink-500 transition-transform duration-200" id="select-arrow"></i>
|
|
</button>
|
|
|
|
<!-- Dropdown Listbox -->
|
|
<ul id="synth-select-listbox"
|
|
role="listbox"
|
|
aria-labelledby="synth-select-label"
|
|
tabindex="-1"
|
|
class="hidden absolute z-30 w-full mt-2 bg-slate-950/95 border border-pink-500/40 rounded-xl shadow-2xl py-2 backdrop-blur-2xl focus:outline-none space-y-1">
|
|
|
|
<li id="opt-1" role="option" aria-selected="true" onclick="selectOption('Outrun Cyberpunk \'84', 'opt-1')" class="px-4 py-2 text-sm font-rajdhani cursor-pointer bg-pink-500/20 text-pink-300 flex items-center justify-between hover:bg-pink-500/30">
|
|
<span>Outrun Cyberpunk '84</span>
|
|
<i class="fa-solid fa-check text-xs"></i>
|
|
</li>
|
|
<li id="opt-2" role="option" aria-selected="false" onclick="selectOption('Neon Grid Driver', 'opt-2')" class="px-4 py-2 text-sm font-rajdhani cursor-pointer text-slate-300 flex items-center justify-between hover:bg-cyan-500/20 hover:text-cyan-300">
|
|
<span>Neon Grid Driver</span>
|
|
</li>
|
|
<li id="opt-3" role="option" aria-selected="false" onclick="selectOption('Hyper-Vaporwave Sunset', 'opt-3')" class="px-4 py-2 text-sm font-rajdhani cursor-pointer text-slate-300 flex items-center justify-between hover:bg-cyan-500/20 hover:text-cyan-300">
|
|
<span>Hyper-Vaporwave Sunset</span>
|
|
</li>
|
|
<li id="opt-4" role="option" aria-selected="false" onclick="selectOption('Dark Synth Retrowave', 'opt-4')" class="px-4 py-2 text-sm font-rajdhani cursor-pointer text-slate-300 flex items-center justify-between hover:bg-cyan-500/20 hover:text-cyan-300">
|
|
<span>Dark Synth Retrowave</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-t border-slate-800/80 text-[11px] font-mono text-slate-400">
|
|
Keyboard support: <span class="text-pink-400">↑/↓</span> navigate, <span class="text-pink-400">Enter</span> select, <span class="text-pink-400">Esc</span> close
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Accessible Glass Modal Component Trigger -->
|
|
<div class="glass-panel p-5 flex flex-col justify-between">
|
|
<div>
|
|
<h3 class="text-md font-orbitron font-bold text-pink-400 mb-1">ACCESSIBLE GLASS DIALOG</h3>
|
|
<p class="text-xs text-slate-400 mb-4">CDK FocusTrap & Escape Key Dismiss Simulation</p>
|
|
|
|
<div class="p-4 bg-slate-950/60 rounded-xl border border-pink-500/20 text-center">
|
|
<p class="text-xs text-slate-300 mb-3 font-rajdhani">Opens a modal overlay with isolated focus management and screen-reader accessibility.</p>
|
|
<button onclick="openGlassModal()" class="cyber-btn w-full py-2.5 rounded-xl font-bold text-xs shadow-lg">
|
|
<i class="fa-solid fa-window-restore mr-2"></i> LAUNCH GLASS DIALOG
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-t border-slate-800/80 text-[11px] font-mono text-slate-400 flex items-center justify-between">
|
|
<span>FocusTrap: <strong class="text-emerald-400">ACTIVE</strong></span>
|
|
<span>Aria-Modal: <strong class="text-cyan-400">TRUE</strong></span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- 3. INTERACTIVE ACCESSIBLE TABS SHOWCASE -->
|
|
<div class="glass-panel p-6">
|
|
<h3 class="text-md font-orbitron font-bold text-cyan-300 mb-3">ARIA TABS DESIGN SYSTEM PATTERN</h3>
|
|
|
|
<!-- Tab List -->
|
|
<div role="tablist" aria-label="Synthwave Components Specs" class="flex border-b border-cyan-500/30 space-x-2">
|
|
<button id="tab-1" role="tab" aria-selected="true" aria-controls="panel-1" onclick="switchTab(1)" class="px-4 py-2 text-sm font-orbitron font-bold border-b-2 border-pink-500 text-pink-400 focus:outline-none focus:text-pink-300">
|
|
DESIGN TOKENS
|
|
</button>
|
|
<button id="tab-2" role="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1" onclick="switchTab(2)" class="px-4 py-2 text-sm font-orbitron font-bold border-b-2 border-transparent text-slate-400 hover:text-cyan-300 focus:outline-none">
|
|
ACCESSIBILITY RULES
|
|
</button>
|
|
<button id="tab-3" role="tab" aria-selected="false" aria-controls="panel-3" tabindex="-1" onclick="switchTab(3)" class="px-4 py-2 text-sm font-orbitron font-bold border-b-2 border-transparent text-slate-400 hover:text-cyan-300 focus:outline-none">
|
|
CDK INTEGRATION
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab Panels -->
|
|
<div class="mt-4">
|
|
<!-- Panel 1 -->
|
|
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1" class="text-sm font-rajdhani space-y-3">
|
|
<p class="text-slate-300">The Angular Aria Synthwave Design System relies on high-contrast neon variables blended with frosted glass aesthetics.</p>
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 font-mono text-xs">
|
|
<div class="p-2.5 bg-slate-950/80 rounded-lg border border-pink-500/40">
|
|
<div class="w-full h-3 rounded bg-[#ff007f] mb-1"></div>
|
|
<span class="text-slate-400">Primary Pink</span>
|
|
<div class="text-pink-400 font-bold">#ff007f</div>
|
|
</div>
|
|
<div class="p-2.5 bg-slate-950/80 rounded-lg border border-cyan-500/40">
|
|
<div class="w-full h-3 rounded bg-[#00f0ff] mb-1"></div>
|
|
<span class="text-slate-400">Secondary Cyan</span>
|
|
<div class="text-cyan-400 font-bold">#00f0ff</div>
|
|
</div>
|
|
<div class="p-2.5 bg-slate-950/80 rounded-lg border border-purple-500/40">
|
|
<div class="w-full h-3 rounded bg-[#7928ca] mb-1"></div>
|
|
<span class="text-slate-400">Accent Purple</span>
|
|
<div class="text-purple-400 font-bold">#7928ca</div>
|
|
</div>
|
|
<div class="p-2.5 bg-slate-950/80 rounded-lg border border-yellow-500/40">
|
|
<div class="w-full h-3 rounded bg-[#ffbe0b] mb-1"></div>
|
|
<span class="text-slate-400">Glow Yellow</span>
|
|
<div class="text-yellow-400 font-bold">#ffbe0b</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panel 2 -->
|
|
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2" class="hidden text-sm font-rajdhani space-y-2 text-slate-300">
|
|
<ul class="list-disc list-inside space-y-1.5 font-mono text-xs text-cyan-200">
|
|
<li>All color combinations maintain WCAG 2.1 AA 4.5:1 minimum contrast ratio.</li>
|
|
<li>Focus indicators feature dynamic high-visibility outlines (3px offset).</li>
|
|
<li>Screen readers receive real-time announcements via Angular CDK LiveAnnouncer.</li>
|
|
<li>Full keyboard navigation supported on all interactive custom controls.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Panel 3 -->
|
|
<div id="panel-3" role="tabpanel" aria-labelledby="tab-3" class="hidden text-sm font-rajdhani text-slate-300">
|
|
<pre class="bg-slate-950 p-3 rounded-lg border border-cyan-500/30 text-xs font-mono text-cyan-300 overflow-x-auto"><code>import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
|
|
@Component({
|
|
selector: 'synth-aria-button',
|
|
template: `<button (click)="announce()">Cyber Action</button>`
|
|
})
|
|
export class SynthButtonComponent {
|
|
constructor(private announcer: LiveAnnouncer) {}
|
|
|
|
announce() {
|
|
this.announcer.announce('Synthwave telemetry synchronized!', 'polite');
|
|
}
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="p-4 border-t border-cyan-500/20 glass-panel rounded-none border-b-0 border-x-0 mt-6 text-center text-xs font-mono text-slate-400">
|
|
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-2">
|
|
<div>ANGULAR ARIA SYNTHWAVE DESIGN SYSTEM © 2026</div>
|
|
<div class="flex items-center space-x-4">
|
|
<span class="text-cyan-400"><i class="fa-solid fa-universal-access mr-1"></i> A11Y CONFORMANT</span>
|
|
<span class="text-pink-400"><i class="fa-solid fa-bolt mr-1"></i> HIGH PERF CSS3D</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
<!-- ACCESSIBLE GLASS MODAL DIALOG (HIDDEN BY DEFAULT) -->
|
|
<div id="glass-modal-overlay" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md transition-opacity" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
|
|
<div class="glass-panel max-w-lg w-full p-6 relative border-2 border-cyan-400 shadow-[0_0_50px_rgba(0,240,255,0.4)] animate-in fade-in zoom-in duration-200">
|
|
|
|
<button onclick="closeGlassModal()" aria-label="Close dialog" class="absolute top-4 right-4 text-slate-400 hover:text-pink-400 focus:outline-none focus:ring-2 focus:ring-pink-500 rounded-lg p-1">
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
</button>
|
|
|
|
<div class="flex items-center space-x-3 mb-4">
|
|
<div class="w-10 h-10 rounded-full bg-pink-500/20 border border-pink-500 flex items-center justify-center text-pink-400 text-lg font-bold">
|
|
<i class="fa-solid fa-shield-halved"></i>
|
|
</div>
|
|
<div>
|
|
<h3 id="modal-title" class="text-lg font-orbitron font-bold text-slate-100">ACCESSIBLE GLASS DIALOG</h3>
|
|
<p id="modal-desc" class="text-xs text-cyan-300 font-mono">Focus trapped within modal overlay window</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 font-rajdhani text-sm text-slate-300 mb-6">
|
|
<p>This dialog modal simulates Angular CDK <code class="text-pink-400 font-mono">FocusTrap</code> functionality. Tabbing will loop strictly within this glass dialog.</p>
|
|
<div class="p-3 bg-slate-950/80 rounded-lg border border-cyan-500/20 text-xs font-mono">
|
|
<div class="text-slate-400">Current Focus Target:</div>
|
|
<div id="modal-focus-indicator" class="text-emerald-400 font-bold">#modal-input-1</div>
|
|
</div>
|
|
<input id="modal-input-1" type="text" placeholder="Type inside accessible modal..." class="w-full bg-slate-900 border border-cyan-500/40 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-pink-500 font-mono">
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button onclick="closeGlassModal()" class="px-4 py-2 rounded-lg text-xs font-mono text-slate-400 hover:text-slate-200 focus:outline-none focus:ring-2 focus:ring-slate-500">
|
|
CANCEL [ESC]
|
|
</button>
|
|
<button onclick="confirmModalAction()" class="cyber-btn px-5 py-2 rounded-lg text-xs font-bold">
|
|
CONFIRM ACTION
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JAVASCRIPT INTERACTIVE LOGIC -->
|
|
<script>
|
|
// --- STATE MANAGEMENT ---
|
|
let currentTheme = 'theme-cyber-magenta';
|
|
let isHighContrast = false;
|
|
let isAriaInspectorActive = false;
|
|
let isGridAnimating = true;
|
|
let isChartStreaming = true;
|
|
let chartInterval = null;
|
|
let lastFocusedElement = null;
|
|
|
|
// --- INITIALIZATION ---
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
initFpsCounter();
|
|
startChartStream();
|
|
logAnnouncerEvent('System ready. Angular Aria Synthwave components mounted.', 'polite');
|
|
|
|
// Keyboard accessibility listener for Modal ESC & Inspector
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeGlassModal();
|
|
closeCustomSelect();
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- ANNOUNCER LOG SYSTEM (Simulating @angular/cdk/a11y LiveAnnouncer) ---
|
|
function logAnnouncerEvent(message, mode = 'polite') {
|
|
const time = new Date().toLocaleTimeString();
|
|
const logContainer = document.getElementById('announcer-log-stream');
|
|
|
|
// Screen reader live region update
|
|
const liveRegion = document.getElementById(`aria-live-${mode}`);
|
|
if (liveRegion) liveRegion.innerText = message;
|
|
|
|
// Terminal UI update
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = 'text-xs border-l-2 pl-2 py-0.5 ' + (mode === 'assertive' ? 'border-pink-500 text-pink-300' : 'border-cyan-400 text-cyan-200');
|
|
logEntry.innerHTML = `<span class="text-slate-500 font-mono">[${time}]</span> <span class="uppercase font-bold text-[10px] ${mode === 'assertive' ? 'text-pink-400' : 'text-cyan-400'}">[${mode}]</span> ${message}`;
|
|
|
|
logContainer.appendChild(logEntry);
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
}
|
|
|
|
function clearAnnouncerLogs() {
|
|
const logContainer = document.getElementById('announcer-log-stream');
|
|
logContainer.innerHTML = '<div class="text-slate-500 italic">[Logs cleared]</div>';
|
|
}
|
|
|
|
// --- THEME & CONTRAST CONTROLS ---
|
|
function changeTheme(themeName) {
|
|
document.body.classList.remove('theme-cyber-magenta', 'theme-neon-cyan', 'theme-sunset-orange', 'theme-matrix-green');
|
|
document.body.classList.add(themeName);
|
|
currentTheme = themeName;
|
|
logAnnouncerEvent(`Theme changed to ${themeName.replace('theme-', '').replace('-', ' ')}`, 'polite');
|
|
}
|
|
|
|
function toggleHighContrast() {
|
|
isHighContrast = !isHighContrast;
|
|
const btn = document.getElementById('hc-btn');
|
|
if (isHighContrast) {
|
|
document.body.classList.add('high-contrast');
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
btn.classList.add('bg-yellow-400', 'text-slate-950');
|
|
logAnnouncerEvent('High Contrast Mode Enabled', 'assertive');
|
|
} else {
|
|
document.body.classList.remove('high-contrast');
|
|
btn.setAttribute('aria-pressed', 'false');
|
|
btn.classList.remove('bg-yellow-400', 'text-slate-950');
|
|
logAnnouncerEvent('High Contrast Mode Disabled', 'polite');
|
|
}
|
|
}
|
|
|
|
function updateBlur(val) {
|
|
document.documentElement.style.setProperty('--blur-quality', `${val}px`);
|
|
document.getElementById('blur-val').innerText = `${val}px`;
|
|
}
|
|
|
|
function toggleAriaInspector() {
|
|
isAriaInspectorActive = !isAriaInspectorActive;
|
|
const btn = document.getElementById('inspect-toggle-btn');
|
|
const dot = document.getElementById('inspect-toggle-dot');
|
|
|
|
btn.setAttribute('aria-pressed', isAriaInspectorActive.toString());
|
|
if (isAriaInspectorActive) {
|
|
dot.style.transform = 'translateX(24px)';
|
|
btn.classList.add('bg-pink-500');
|
|
document.querySelectorAll('[role], [aria-label], [aria-expanded]').forEach(el => {
|
|
el.classList.add('aria-inspect-active');
|
|
});
|
|
logAnnouncerEvent('ARIA Inspector highlights enabled', 'polite');
|
|
} else {
|
|
dot.style.transform = 'translateX(0px)';
|
|
btn.classList.remove('bg-pink-500');
|
|
document.querySelectorAll('.aria-inspect-active').forEach(el => {
|
|
el.classList.remove('aria-inspect-active');
|
|
});
|
|
logAnnouncerEvent('ARIA Inspector highlights disabled', 'polite');
|
|
}
|
|
}
|
|
|
|
function toggleGridAnimation() {
|
|
isGridAnimating = !isGridAnimating;
|
|
const gridPlane = document.querySelector('.grid-plane');
|
|
const btn = document.getElementById('anim-toggle-btn');
|
|
if (isGridAnimating) {
|
|
gridPlane.style.animationPlayState = 'running';
|
|
btn.innerText = 'ACTIVE';
|
|
btn.className = 'px-3 py-1 rounded bg-pink-500/20 text-pink-300 border border-pink-500/40 text-xs font-mono font-bold hover:bg-pink-500/30';
|
|
} else {
|
|
gridPlane.style.animationPlayState = 'paused';
|
|
btn.innerText = 'PAUSED';
|
|
btn.className = 'px-3 py-1 rounded bg-slate-800 text-slate-400 border border-slate-700 text-xs font-mono font-bold';
|
|
}
|
|
}
|
|
|
|
// --- CUSTOM ACCESSIBLE SELECT COMPONENT ---
|
|
function toggleCustomSelect() {
|
|
const btn = document.getElementById('synth-select-btn');
|
|
const listbox = document.getElementById('synth-select-listbox');
|
|
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
|
|
|
|
if (isExpanded) {
|
|
closeCustomSelect();
|
|
} else {
|
|
btn.setAttribute('aria-expanded', 'true');
|
|
listbox.classList.remove('hidden');
|
|
document.getElementById('select-arrow').style.transform = 'rotate(180deg)';
|
|
logAnnouncerEvent('Select listbox expanded. Use arrow keys to navigate options.', 'polite');
|
|
}
|
|
}
|
|
|
|
function closeCustomSelect() {
|
|
const btn = document.getElementById('synth-select-btn');
|
|
const listbox = document.getElementById('synth-select-listbox');
|
|
btn.setAttribute('aria-expanded', 'false');
|
|
listbox.classList.add('hidden');
|
|
document.getElementById('select-arrow').style.transform = 'rotate(0deg)';
|
|
}
|
|
|
|
function selectOption(val, optId) {
|
|
document.getElementById('select-current-val').innerText = val;
|
|
document.querySelectorAll('#synth-select-listbox li').forEach(li => {
|
|
li.setAttribute('aria-selected', 'false');
|
|
li.className = 'px-4 py-2 text-sm font-rajdhani cursor-pointer text-slate-300 flex items-center justify-between hover:bg-cyan-500/20 hover:text-cyan-300';
|
|
});
|
|
const selectedLi = document.getElementById(optId);
|
|
selectedLi.setAttribute('aria-selected', 'true');
|
|
selectedLi.className = 'px-4 py-2 text-sm font-rajdhani cursor-pointer bg-pink-500/20 text-pink-300 flex items-center justify-between hover:bg-pink-500/30';
|
|
|
|
closeCustomSelect();
|
|
logAnnouncerEvent(`Selected synth profile: ${val}`, 'assertive');
|
|
}
|
|
|
|
function handleSelectKeydown(e) {
|
|
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
toggleCustomSelect();
|
|
}
|
|
}
|
|
|
|
// --- GLASS MODAL ACCESSIBILITY ---
|
|
function openGlassModal() {
|
|
lastFocusedElement = document.activeElement;
|
|
const modal = document.getElementById('glass-modal-overlay');
|
|
modal.classList.remove('hidden');
|
|
document.getElementById('modal-input-1').focus();
|
|
logAnnouncerEvent('Glass Modal Dialog Opened. Focus trapped inside modal.', 'assertive');
|
|
}
|
|
|
|
function closeGlassModal() {
|
|
const modal = document.getElementById('glass-modal-overlay');
|
|
if (!modal.classList.contains('hidden')) {
|
|
modal.classList.add('hidden');
|
|
if (lastFocusedElement) lastFocusedElement.focus();
|
|
logAnnouncerEvent('Glass Modal Dialog Closed. Focus restored to trigger button.', 'polite');
|
|
}
|
|
}
|
|
|
|
function confirmModalAction() {
|
|
alert('Synthwave telemetry sequence initiated!');
|
|
closeGlassModal();
|
|
}
|
|
|
|
// --- TABS SYSTEM ---
|
|
function switchTab(tabIdx) {
|
|
for (let i = 1; i <= 3; i++) {
|
|
const tab = document.getElementById(`tab-${i}`);
|
|
const panel = document.getElementById(`panel-${i}`);
|
|
if (i === tabIdx) {
|
|
tab.setAttribute('aria-selected', 'true');
|
|
tab.setAttribute('tabindex', '0');
|
|
tab.className = 'px-4 py-2 text-sm font-orbitron font-bold border-b-2 border-pink-500 text-pink-400 focus:outline-none';
|
|
panel.classList.remove('hidden');
|
|
} else {
|
|
tab.setAttribute('aria-selected', 'false');
|
|
tab.setAttribute('tabindex', '-1');
|
|
tab.className = 'px-4 py-2 text-sm font-orbitron font-bold border-b-2 border-transparent text-slate-400 hover:text-cyan-300 focus:outline-none';
|
|
panel.classList.add('hidden');
|
|
}
|
|
}
|
|
logAnnouncerEvent(`Switched to Tab ${tabIdx}`, 'polite');
|
|
}
|
|
|
|
// --- REAL-TIME CHART ANIMATION ---
|
|
function startChartStream() {
|
|
if (chartInterval) clearInterval(chartInterval);
|
|
chartInterval = setInterval(() => {
|
|
if (!isChartStreaming) return;
|
|
const path = document.getElementById('chart-path');
|
|
const area = document.getElementById('chart-area');
|
|
|
|
// Randomize chart Y points slightly
|
|
const p1 = Math.floor(Math.random() * 60) + 30;
|
|
const p2 = Math.floor(Math.random() * 70) + 20;
|
|
const p3 = Math.floor(Math.random() * 80) + 20;
|
|
const p4 = Math.floor(Math.random() * 60) + 30;
|
|
|
|
const newD = `M 0 75 L 100 ${p1} L 200 ${p2} L 300 ${p3} L 400 ${p4} L 500 45`;
|
|
path.setAttribute('d', newD);
|
|
area.setAttribute('points', `0,150 0,75 100,${p1} 200,${p2} 300,${p3} 400,${p4} 500,45 500,150`);
|
|
|
|
const freq = (140 + Math.random() * 10).toFixed(1);
|
|
document.getElementById('chart-val').innerText = `${freq} MHz`;
|
|
}, 1200);
|
|
}
|
|
|
|
function toggleChartStream() {
|
|
isChartStreaming = !isChartStreaming;
|
|
const btn = document.getElementById('chart-pulse-btn');
|
|
if (isChartStreaming) {
|
|
btn.innerHTML = '<i class="fa-solid fa-pause mr-1"></i> PAUSE STREAM';
|
|
logAnnouncerEvent('Real-time chart data stream resumed', 'polite');
|
|
} else {
|
|
btn.innerHTML = '<i class="fa-solid fa-play mr-1"></i> RESUME STREAM';
|
|
logAnnouncerEvent('Real-time chart data stream paused', 'polite');
|
|
}
|
|
}
|
|
|
|
// --- FPS COUNTER ---
|
|
function initFpsCounter() {
|
|
let frameCount = 0;
|
|
let lastTime = performance.now();
|
|
const fpsElem = document.getElementById('fps-counter');
|
|
|
|
function updateFPS() {
|
|
const now = performance.now();
|
|
frameCount++;
|
|
if (now - lastTime >= 1000) {
|
|
fpsElem.innerText = frameCount;
|
|
frameCount = 0;
|
|
lastTime = now;
|
|
}
|
|
requestAnimationFrame(updateFPS);
|
|
}
|
|
requestAnimationFrame(updateFPS);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |