From c97422431221e9d7880e7c605440b62432a0f03f Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sat, 4 Apr 2026 10:11:04 +0200 Subject: [PATCH] feat: overhaul web control UI with live status and draft handling --- internal/control/ui.html | 2158 ++++++++++++++++++++++++++++---------- 1 file changed, 1631 insertions(+), 527 deletions(-) diff --git a/internal/control/ui.html b/internal/control/ui.html index 1a70e9c..8445eca 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -9,11 +9,15 @@ :root { --bg: #0a0a0c; + --bg-2: #0f1015; --surface: #111116; --surface2: #18181e; + --surface3: #1f2028; --border: #2a2a35; + --border-strong: #3a3a49; --text: #d4d4dc; - --text-dim: #6a6a78; + --text-dim: #8b8b99; + --text-muted: #666674; --accent: #ff3b30; --accent-glow: #ff3b3044; --green: #30d158; @@ -21,15 +25,20 @@ --amber: #ff9f0a; --amber-glow: #ff9f0a44; --blue: #0a84ff; + --blue-glow: #0a84ff33; --mono: 'JetBrains Mono', monospace; --display: 'Archivo Black', sans-serif; - --radius: 6px; + --radius: 8px; + --shadow: 0 10px 30px rgba(0,0,0,.25); } * { box-sizing: border-box; margin: 0; padding: 0; } - +html { color-scheme: dark; } body { - background: var(--bg); + background: + radial-gradient(circle at top right, rgba(10,132,255,.06), transparent 28%), + radial-gradient(circle at top left, rgba(255,59,48,.06), transparent 30%), + var(--bg); color: var(--text); font-family: var(--mono); font-size: 13px; @@ -38,52 +47,83 @@ body { overflow-x: hidden; } -/* Scan lines overlay */ body::before { content: ''; position: fixed; inset: 0; - background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px); - pointer-events: none; z-index: 1000; + background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px); + pointer-events: none; + z-index: 1000; } +button, input { font: inherit; } +button { user-select: none; } + .app { - max-width: 900px; + max-width: 1120px; margin: 0 auto; - padding: 16px; + padding: 18px; } -/* Header */ .header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - padding: 16px 0 24px; + gap: 18px; + padding: 8px 0 22px; border-bottom: 1px solid var(--border); - margin-bottom: 20px; + margin-bottom: 18px; +} +.header-main { + display: flex; + flex-direction: column; + gap: 8px; } - .header h1 { font-family: var(--display); - font-size: 22px; + font-size: 24px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); text-shadow: 0 0 20px var(--accent-glow), 0 0 40px var(--accent-glow); } - +.header-sub { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.badge { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 28px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255,255,255,0.02); + color: var(--text-dim); + font-size: 11px; + text-transform: uppercase; + letter-spacing: .8px; +} +.badge strong { + color: var(--text); + font-weight: 700; +} .header-status { display: flex; align-items: center; - gap: 12px; + gap: 10px; + padding-top: 6px; } -/* LED indicator */ .led { - width: 10px; height: 10px; + width: 10px; + height: 10px; border-radius: 50%; background: #333; box-shadow: none; - transition: all 0.3s; + transition: all .25s ease; + flex-shrink: 0; } .led.on-green { background: var(--green); @@ -97,779 +137,1843 @@ body::before { background: var(--amber); box-shadow: 0 0 8px var(--amber), 0 0 20px var(--amber-glow); } +.led.on-blue { + background: var(--blue); + box-shadow: 0 0 8px var(--blue), 0 0 20px var(--blue-glow); +} -/* TX control bar */ -.tx-bar { - display: flex; - gap: 10px; - align-items: center; - background: var(--surface); +.status-text { + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1.2px; +} + +.layout { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(310px, .75fr); + gap: 14px; + align-items: start; +} +.stack { display: flex; flex-direction: column; gap: 12px; } + +.card { + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border: 1px solid var(--border); border-radius: var(--radius); - padding: 12px 16px; - margin-bottom: 16px; + box-shadow: var(--shadow); +} + +.hero { + padding: 16px; + position: relative; + overflow: hidden; +} +.hero.tx-live::after { + content: ''; + position: absolute; + inset: -40%; + background: radial-gradient(circle, rgba(48,209,88,.12), transparent 55%); + animation: pulseGlow 2.8s ease-in-out infinite; + pointer-events: none; +} +.hero.tx-busy::after { + content: ''; + position: absolute; + inset: -50%; + background: conic-gradient(from 0deg, transparent, rgba(255,159,10,.12), transparent 45%); + animation: spinWash 2s linear infinite; + pointer-events: none; +} +@keyframes pulseGlow { + 0%, 100% { transform: scale(.95); opacity: .45; } + 50% { transform: scale(1.05); opacity: .8; } +} +@keyframes spinWash { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes blinkSoft { + 0%, 100% { opacity: 1; } + 50% { opacity: .55; } } -.tx-bar .freq-display { +.tx-bar { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(180px, 250px) 1fr auto; + gap: 14px; + align-items: center; +} + +.freq-display-wrap { + display: flex; + flex-direction: column; + gap: 6px; +} +.freq-display-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.4px; + color: var(--text-dim); +} +.freq-display { font-family: var(--display); - font-size: 32px; + font-size: 38px; color: var(--green); text-shadow: 0 0 15px var(--green-glow); letter-spacing: 1px; - min-width: 200px; + line-height: 1; } -.tx-bar .freq-display .unit { +.freq-display .unit { font-family: var(--mono); font-size: 14px; color: var(--text-dim); - margin-left: 4px; + margin-left: 5px; } -.tx-btn { - padding: 8px 20px; - border: 1px solid var(--border); +.tx-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.tx-btn, .ghost-btn, .apply-btn, .preset-btn, .danger-btn { + min-height: 40px; + padding: 0 18px; border-radius: var(--radius); + border: 1px solid var(--border); background: var(--surface2); color: var(--text); - font-family: var(--mono); - font-size: 12px; - font-weight: 600; cursor: pointer; + font-size: 12px; + font-weight: 700; text-transform: uppercase; letter-spacing: 1px; - transition: all 0.15s; + transition: all .16s ease; +} +.tx-btn:hover, .ghost-btn:hover, .apply-btn:hover, .preset-btn:hover, .danger-btn:hover { + transform: translateY(-1px); + border-color: var(--border-strong); +} +.tx-btn:disabled, .ghost-btn:disabled, .apply-btn:disabled, .preset-btn:disabled, .danger-btn:disabled { + opacity: .45; + cursor: not-allowed; + transform: none; } -.tx-btn:hover { border-color: var(--text-dim); } .tx-btn.start { border-color: var(--green); color: var(--green); } -.tx-btn.start:hover { background: var(--green); color: var(--bg); } +.tx-btn.start:hover:not(:disabled) { background: rgba(48,209,88,.1); } .tx-btn.stop { border-color: var(--accent); color: var(--accent); } -.tx-btn.stop:hover { background: var(--accent); color: #fff; } +.tx-btn.stop:hover:not(:disabled) { background: rgba(255,59,48,.1); } +.ghost-btn { color: var(--text-dim); } +.danger-btn { + border-color: rgba(255,59,48,.45); + color: var(--accent); + background: rgba(255,59,48,.04); +} +.danger-btn:hover:not(:disabled) { + background: rgba(255,59,48,.12); +} +.tx-state-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; +} .tx-state { font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-dim); - margin-left: auto; } -.tx-state.running { color: var(--green); } -.tx-state.idle { color: var(--text-dim); } +.tx-state.running { color: var(--green); animation: blinkSoft 2s ease-in-out infinite; } +.tx-state.idle, .tx-state.stopped { color: var(--text-dim); } +.tx-state.starting, .tx-state.stopping, .tx-state.working { color: var(--amber); animation: blinkSoft 1.1s ease-in-out infinite; } +.tx-state.error { color: var(--accent); } +.status-hint { + font-size: 10px; + color: var(--text-muted); + text-align: right; +} -/* Telemetry strip */ -.telem { - display: flex; - gap: 1px; - background: var(--border); - border-radius: var(--radius); - overflow: hidden; - margin-bottom: 16px; +.quick-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-top: 16px; } -.telem-cell { - flex: 1; - background: var(--surface); - padding: 10px 12px; - text-align: center; +.quick-item { + padding: 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-2); } -.telem-cell .label { +.quick-item .label { font-size: 9px; text-transform: uppercase; - letter-spacing: 1.5px; + letter-spacing: 1.4px; color: var(--text-dim); - margin-bottom: 4px; + margin-bottom: 6px; } -.telem-cell .value { - font-size: 16px; +.quick-item .value { + font-size: 18px; font-weight: 700; color: var(--text); } -.telem-cell .value.warn { color: var(--amber); } -.telem-cell .value.err { color: var(--accent); } +.quick-item .value.warn { color: var(--amber); } +.quick-item .value.err { color: var(--accent); } +.quick-item .value.good { color: var(--green); } -/* Section panels */ -.panel { - background: var(--surface); +.signal-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; +} +.signal-card { + padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); - margin-bottom: 12px; + background: var(--bg-2); +} +.signal-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} +.signal-title { + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1.2px; +} +.signal-value { + font-size: 11px; + color: var(--text); + font-weight: 700; +} +.meter { + width: 100%; + height: 10px; + border-radius: 999px; + background: #171821; + border: 1px solid var(--border); + overflow: hidden; +} +.meter-fill { + height: 100%; + width: 0%; + transition: width .25s ease, background-color .25s ease; + background: linear-gradient(90deg, var(--green), #5cff90); +} +.meter-fill.warn { + background: linear-gradient(90deg, var(--amber), #ffc45b); +} +.meter-fill.err { + background: linear-gradient(90deg, var(--accent), #ff6b63); +} +.spark { + width: 100%; + height: 34px; + margin-top: 10px; + border-radius: 6px; + background: rgba(255,255,255,0.01); + border: 1px solid rgba(255,255,255,0.03); +} +.spark path.line { + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.spark path.area { + opacity: .14; +} +.spark.good path.line { stroke: var(--green); } +.spark.good path.area { fill: var(--green); } +.spark.warn path.line { stroke: var(--amber); } +.spark.warn path.area { fill: var(--amber); } +.spark.err path.line { stroke: var(--accent); } +.spark.err path.area { fill: var(--accent); } + +.panel { overflow: hidden; } .panel-head { display: flex; align-items: center; gap: 8px; - padding: 10px 14px; + padding: 12px 14px; border-bottom: 1px solid var(--border); background: var(--surface2); cursor: pointer; user-select: none; } .panel-head h2 { - font-family: var(--mono); font-size: 11px; - font-weight: 600; + font-weight: 700; text-transform: uppercase; - letter-spacing: 2px; + letter-spacing: 1.6px; color: var(--text-dim); } -.panel-head .chevron { +.panel-head .meta { margin-left: auto; + margin-right: 8px; + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} +.panel-head .chevron { color: var(--text-dim); - transition: transform 0.2s; + transition: transform .2s ease; font-size: 10px; } .panel-head.collapsed .chevron { transform: rotate(-90deg); } .panel-body { padding: 14px; } .panel-body.collapsed { display: none; } -/* Form controls */ +.section-note { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 12px; +} +.shortcuts-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 12px; +} +.shortcut-line { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 0; + border-bottom: 1px solid #1a1a22; +} +.shortcut-line:last-child { border-bottom: none; } +.shortcut-line .name { font-size: 11px; color: var(--text-dim); } +.shortcut-line .keys { + display: inline-flex; + gap: 6px; + flex-wrap: wrap; +} +.kbd { + min-width: 28px; + padding: 3px 7px; + border: 1px solid var(--border); + border-bottom-width: 2px; + border-radius: 6px; + background: var(--bg-2); + font-size: 10px; + color: var(--text); + text-align: center; +} + +.preset-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} +.preset-btn { + min-height: 34px; + padding: 0 12px; + font-size: 11px; + letter-spacing: .8px; + color: var(--text-dim); +} +.preset-btn.active { + border-color: var(--blue); + color: var(--blue); + background: rgba(10,132,255,.08); +} +.preset-btn.rds { + text-transform: none; + font-weight: 600; +} + .ctrl-row { display: flex; align-items: center; gap: 12px; - padding: 6px 0; + padding: 10px 0; border-bottom: 1px solid #1a1a22; } .ctrl-row:last-child { border-bottom: none; } - +.ctrl-label-wrap { + min-width: 130px; + display: flex; + flex-direction: column; + gap: 2px; +} .ctrl-label { font-size: 11px; color: var(--text-dim); - min-width: 110px; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: .8px; +} +.ctrl-sub { + font-size: 10px; + color: var(--text-muted); } - .ctrl-input { flex: 1; display: flex; align-items: center; - gap: 8px; + gap: 10px; } input[type="range"] { -webkit-appearance: none; appearance: none; flex: 1; - height: 4px; - background: var(--border); - border-radius: 2px; + height: 6px; + background: linear-gradient(90deg, var(--border), var(--surface3)); + border-radius: 999px; outline: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; - width: 14px; height: 14px; + width: 16px; + height: 16px; border-radius: 50%; background: var(--text); border: 2px solid var(--bg); cursor: pointer; - transition: background 0.15s; + transition: background .15s ease, transform .15s ease; +} +input[type="range"]::-webkit-slider-thumb:hover { + background: var(--accent); + transform: scale(1.06); } -input[type="range"]::-webkit-slider-thumb:hover { background: var(--accent); } - input[type="number"], input[type="text"] { background: var(--bg); border: 1px solid var(--border); - border-radius: 4px; + border-radius: 6px; color: var(--text); - font-family: var(--mono); - font-size: 13px; - padding: 5px 8px; - width: 80px; + padding: 8px 10px; outline: none; - transition: border-color 0.15s; + transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; +} +input[type="number"] { + width: 92px; + text-align: right; } input[type="text"] { width: 100%; } -input:focus { border-color: var(--accent); } - +input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(255,59,48,.12); +} +input.input-dirty { + border-color: var(--amber); + box-shadow: 0 0 0 3px rgba(255,159,10,.08); +} +input.input-error { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(255,59,48,.14); + background: rgba(255,59,48,.04); +} .val-display { - font-size: 12px; - font-weight: 600; - min-width: 55px; + min-width: 64px; text-align: right; + font-size: 12px; + font-weight: 700; color: var(--text); } +.unit-label { + font-size: 11px; + color: var(--text-dim); + min-width: 44px; +} +.field-error { + display: none; + margin-top: 8px; + font-size: 11px; + color: var(--accent); +} +.field-error.show { display: block; } -/* Toggle switch */ +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 12px 0; + border-bottom: 1px solid #1a1a22; +} +.toggle-row:last-child { border-bottom: none; } +.toggle-copy { + display: flex; + flex-direction: column; + gap: 3px; +} +.toggle-copy .title { + font-size: 12px; + color: var(--text); + font-weight: 700; +} +.toggle-copy .sub { + font-size: 10px; + color: var(--text-muted); +} +.toggle-ctl { + display: flex; + align-items: center; + gap: 10px; +} .toggle { position: relative; - width: 36px; height: 20px; + width: 42px; + height: 24px; background: var(--border); - border-radius: 10px; + border-radius: 999px; cursor: pointer; - transition: background 0.2s; + transition: all .2s ease; flex-shrink: 0; } -.toggle.on { background: var(--green); } .toggle::after { content: ''; position: absolute; - top: 2px; left: 2px; - width: 16px; height: 16px; + top: 3px; + left: 3px; + width: 18px; + height: 18px; background: var(--text); border-radius: 50%; - transition: transform 0.2s; + transition: transform .2s ease; +} +.toggle.on { background: var(--green); } +.toggle.on::after { transform: translateX(18px); } +.toggle.busy { opacity: .55; pointer-events: none; } +.toggle-state { + min-width: 52px; + text-align: right; + font-size: 11px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; } -.toggle.on::after { transform: translateX(16px); } -/* RDS section */ +.rds-grid { + display: grid; + gap: 12px; +} +.rds-field { + display: flex; + flex-direction: column; + gap: 6px; +} .rds-input { width: 100%; background: var(--bg); border: 1px solid var(--border); - border-radius: 4px; + border-radius: 6px; color: var(--green); font-family: var(--mono); font-size: 15px; font-weight: 700; - padding: 8px 10px; + padding: 10px 12px; outline: none; letter-spacing: 2px; text-transform: uppercase; - transition: border-color 0.15s; } -.rds-input:focus { border-color: var(--accent); } .rds-input.rt { - font-size: 12px; - font-weight: 400; - letter-spacing: 0.5px; - text-transform: none; color: var(--text); + text-transform: none; + letter-spacing: .5px; + font-size: 12px; + font-weight: 500; } .rds-charcount { font-size: 10px; color: var(--text-dim); text-align: right; - margin-top: 2px; } -/* Apply button */ +.actions-row { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 14px; +} .apply-btn { - display: block; - width: 100%; - padding: 10px; - margin-top: 8px; background: var(--accent); - border: none; - border-radius: var(--radius); + border-color: transparent; color: #fff; - font-family: var(--mono); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 2px; - cursor: pointer; - transition: all 0.15s; - opacity: 0; - transform: translateY(-4px); - pointer-events: none; } -.apply-btn.visible { - opacity: 1; - transform: translateY(0); - pointer-events: auto; +.apply-btn.secondary { + background: var(--surface2); + color: var(--text-dim); + border-color: var(--border); } -.apply-btn:hover { filter: brightness(1.2); } -.apply-btn.sending { - opacity: 0.6; - pointer-events: none; +.apply-btn.ok { background: var(--green); color: var(--bg); } + +.sidebar-card { + padding: 14px; } -.apply-btn.ok { - background: var(--green); +.sidebar-section + .sidebar-section { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--border); } - -/* Toast notification */ -.toast { - position: fixed; - bottom: 20px; - right: 20px; - padding: 10px 16px; - border-radius: var(--radius); +.sidebar-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1.4px; + color: var(--text-dim); + margin-bottom: 10px; +} +.kv { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 12px; + align-items: start; +} +.kv .k { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} +.kv .v { font-size: 12px; - font-weight: 600; - z-index: 2000; - transform: translateY(60px); - opacity: 0; - transition: all 0.3s; + color: var(--text); + word-break: break-word; } -.toast.show { transform: translateY(0); opacity: 1; } -.toast.ok { background: var(--green); color: var(--bg); } -.toast.err { background: var(--accent); color: #fff; } +.health-line { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid #1a1a22; +} +.health-line:last-child { border-bottom: none; } +.health-line .name { + font-size: 11px; + color: var(--text-dim); +} +.health-line .val { + font-size: 11px; + color: var(--text); + text-align: right; +} +.health-line .val.good { color: var(--green); } +.health-line .val.warn { color: var(--amber); } +.health-line .val.err { color: var(--accent); } -/* Log */ .log { background: var(--bg); border: 1px solid var(--border); - border-radius: 4px; - padding: 8px 10px; + border-radius: 6px; + padding: 10px; font-size: 10px; color: var(--text-dim); - max-height: 120px; + max-height: 220px; overflow-y: auto; white-space: pre-wrap; - word-break: break-all; + word-break: break-word; } -.log .entry { padding: 1px 0; } +.log .entry { padding: 3px 0; } .log .entry.err { color: var(--accent); } .log .entry.ok { color: var(--green); } +.log .entry.warn { color: var(--amber); } +.log .entry.info { color: var(--blue); } +.empty-log { + color: var(--text-muted); +} -/* Responsive */ -@media (max-width: 600px) { - .tx-bar { flex-wrap: wrap; } - .tx-bar .freq-display { font-size: 24px; min-width: auto; } - .telem { flex-wrap: wrap; } - .telem-cell { flex: 1 1 30%; } - .ctrl-row { flex-wrap: wrap; } - .ctrl-label { min-width: auto; width: 100%; } +.toast { + position: fixed; + right: 16px; + bottom: 16px; + max-width: min(420px, calc(100vw - 24px)); + padding: 12px 15px; + border-radius: var(--radius); + font-size: 12px; + font-weight: 700; + z-index: 2000; + transform: translateY(60px); + opacity: 0; + transition: all .25s ease; + box-shadow: var(--shadow); +} +.toast.show { transform: translateY(0); opacity: 1; } +.toast.ok { background: var(--green); color: var(--bg); } +.toast.err { background: var(--accent); color: #fff; } +.toast.info { background: var(--blue); color: #fff; } +.toast.warn { background: var(--amber); color: #141414; } + +@media (max-width: 980px) { + .layout { grid-template-columns: 1fr; } + .tx-bar { + grid-template-columns: 1fr; + align-items: stretch; + } + .tx-state-wrap { + align-items: flex-start; + } + .status-hint { text-align: left; } + .quick-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .signal-grid { grid-template-columns: 1fr; } +} + +@media (max-width: 640px) { + .app { padding: 12px; } + .header { flex-direction: column; align-items: stretch; gap: 10px; } + .header h1 { font-size: 22px; } + .header-sub { gap: 6px; } + .badge { width: 100%; justify-content: space-between; } + .quick-grid { grid-template-columns: 1fr 1fr; gap: 8px; } + .quick-item { padding: 10px; } + .quick-item .value { font-size: 16px; } + .ctrl-row { flex-direction: column; align-items: stretch; } + .ctrl-label-wrap { min-width: auto; } + .ctrl-input { flex-wrap: wrap; } + input[type="number"] { width: 100%; text-align: left; } + .actions-row, .tx-actions { flex-direction: column; } + .tx-btn, .ghost-btn, .apply-btn, .preset-btn, .danger-btn { width: 100%; } + .panel-head { padding: 11px 12px; } + .panel-body, .sidebar-card { padding: 12px; } + .freq-display { font-size: 31px; } + .preset-row { flex-direction: column; } + .shortcuts-grid { grid-template-columns: 1fr; } } -
- - +
-

FM-RDS-TX

+
+

FM-RDS-TX

+
+
Backend--
+
ModeControl Plane
+
Live Config--
+
+
- connecting +
connecting
- -
-
---.--MHz
- - -
--
-
+
+
+
+
+
+
Carrier
+
---.-MHz
+
+ +
+ + + +
+ +
+
IDLE
+
Awaiting runtime data
+
+
- -
-
Chunks
--
-
Samples
--
-
Underruns
0
-
Uptime
--
-
Rate
--
-
+
+
+
Chunks
+
--
+
+
+
Samples
+
--
+
+
+
Underruns
+
--
+
+
+
Uptime
+
--
+
+
+
Rate
+
--
+
+
- -
-
-
-

Frequency

- -
-
-
- TX Freq -
- - - MHz +
+
+
+
Audio Buffer
+
--
+
+
+ +
+
+
+
Stream Health
+
--
+
+
+ +
+
+
+
TX Activity
+
--
+
+
+ +
- -
-
- -
-
-
-

Levels

- -
-
-
- Output Drive -
- - -- +
+
+
+

Frequency

+
Live-tunable
+
-
-
- Pilot Level -
- - -- +
+
Tune the RF carrier without restarting the control plane. Draft values stay local until you apply them.
+ +
+ + + + + +
+ +
+
+ TX Freq + Valid range 65–110 MHz +
+
+ + + MHz +
+
+
+
+ + +
-
- RDS Inject -
- - -- + +
+
+
+

Switches

+
Live
+ +
+
+
These switches apply immediately and show a busy state while the request is in flight.
+ +
+
+
Stereo
+
19 kHz pilot + 38 kHz DSB-SC
+
+
+
+
--
+
+
+ +
+
+
RDS
+
57 kHz subcarrier encoder
+
+
+
+
--
+
+
+ +
+
+
Limiter
+
MPX peak protection
+
+
+
+
--
+
+
-
- Limiter Ceil -
- - -- + +
+
+
+

RDS Text

+
PS + RT
+ +
+
+
Edit Program Service and RadioText without losing in-progress typing when the page refreshes itself.
+ +
+ + + + +
+ +
+
+ Program Service (PS) + +
0/8
+
+
+
+ RadioText (RT) + +
0/64
+
+
+
+
+ + +
-
-
- -
-
-
-

Switches

- -
-
-
- Stereo -
-
- -- +
+ -
- RDS -
-
- -- + +
-
- Limiter -
-
- -- + +
+
+

Shortcuts

+
keyboard
+ +
+
+
Fast control, as long as you're not typing in an input field.
+
+
+
Start TXt
+
Stop TXShiftt
+
Refreshr
+
+
+
Next Freq Preset]
+
Prev Freq Preset[
+
Apply DraftEnter
+
+
-
-
- -
-
-
-

RDS

- -
-
-
- Program Service (PS) - -
0/8
-
-
- RadioText (RT) - -
0/64
+
+
+

Danger Zone

+
tx control
+ +
+
+
Fast emergency controls. Nothing hidden here — just clearer separation from normal controls.
+
+ + +
+
- -
-
- -
-
-

Log

- -
-
-
+
+
+

Activity Log

+
recent events
+ +
+
+
+ +
+
No events yet.
+
+
-
-
- + \ No newline at end of file