mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 03:52:34 +00:00
Real-time's receiver strip gains an RX LED beside the station state, borrowed from the radio's own front panel. It lights for 140 ms on each live reception, and a burst keeps it lit instead of strobing. It is dark when nothing is arriving, which honestly answers "is anything coming in?" on a quiet station. It uses the connected lamp's green because it means receiving, sits behind aria-hidden next to "Last heard", which carries the same information in words, and stays dark under prefers-reduced-motion. Replayed history doesn't light it; it's one class toggle per packet, with no polling. The dashboard's New nodes tile names the newest arrival from the last day under its count, e.g. "newest: N7JMV-BEACON-RP · 7 hops · 1h ago", so the number has a neighbor behind it. Long names truncate so the tile keeps its height. The snapshot computes it with one indexed query alongside the other contact counts, so nothing new is polled.
527 lines
12 KiB
CSS
527 lines
12 KiB
CSS
/* Dashboard (landing page) styles.
|
|
*
|
|
* Only the theme tokens declared in base.html are used (--bg-*, --text-*,
|
|
* --border-color, --card-*, --shadow-color), and every rule that hard-codes a
|
|
* colour has a matching [data-theme="dark"] override — Bootstrap 5.1.3 has no
|
|
* native dark theme here, so nothing inherits one for free.
|
|
*/
|
|
|
|
/* ── Health strip ──────────────────────────────────────────────────────── */
|
|
|
|
.health-strip {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.25rem 1.5rem;
|
|
min-height: 56px;
|
|
padding: 0.5rem 1rem;
|
|
margin-bottom: 1.25rem;
|
|
background-color: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 10px;
|
|
box-shadow: 0 1px 3px var(--shadow-color);
|
|
}
|
|
|
|
.health-strip__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
font-size: 0.875rem;
|
|
color: var(--text-color);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.health-strip__label {
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.health-strip__spacer {
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.health-strip .btn-refresh {
|
|
padding: 0.1rem 0.4rem;
|
|
line-height: 1;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.health-strip .btn-refresh:hover,
|
|
.health-strip .btn-refresh:focus {
|
|
color: #0d6efd;
|
|
}
|
|
|
|
.health-strip .btn-refresh[disabled] {
|
|
opacity: 0.5;
|
|
cursor: progress;
|
|
}
|
|
|
|
/* ── First run ─────────────────────────────────────────────────────────── */
|
|
/* Shown instead of the grid until the station has heard anything; one panel
|
|
in the station's own voice rather than fifteen "No data." boxes. */
|
|
|
|
.first-run {
|
|
max-width: 46rem;
|
|
padding: 1.25rem 1.5rem;
|
|
background-color: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 10px;
|
|
box-shadow: 0 1px 3px var(--shadow-color);
|
|
}
|
|
|
|
.first-run__title {
|
|
margin: 0 0 0.35rem;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.first-run__state {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.first-run__state .status-indicator {
|
|
flex: none;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.first-run__lead,
|
|
.first-run__note {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.first-run__lead {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.first-run__steps {
|
|
margin: 0 0 1rem;
|
|
padding-left: 1.25rem;
|
|
}
|
|
|
|
.first-run__steps li {
|
|
margin-bottom: 0.4rem;
|
|
padding-left: 0.25rem;
|
|
}
|
|
|
|
.first-run__steps li::marker {
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.first-run__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* ── Section headings ──────────────────────────────────────────────────── */
|
|
|
|
.dashboard-section-title {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.6rem;
|
|
margin: 1.75rem 0 0.75rem;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.dashboard-section-title::after {
|
|
content: "";
|
|
flex: 1 1 auto;
|
|
height: 1px;
|
|
background-color: var(--border-color);
|
|
}
|
|
|
|
/* ── Stat tiles ────────────────────────────────────────────────────────── */
|
|
|
|
.stat-tile {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
padding: 0.75rem 0.9rem 0.5rem;
|
|
background-color: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 10px;
|
|
box-shadow: 0 1px 3px var(--shadow-color);
|
|
}
|
|
|
|
.stat-tile__label {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.2rem;
|
|
}
|
|
|
|
.stat-tile__row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stat-tile__value {
|
|
font-size: 1.6rem;
|
|
font-weight: 700;
|
|
line-height: 1.1;
|
|
color: var(--text-color);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.stat-tile__value--muted {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.stat-tile__sub {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* The newest arrival's name, truncated so the tile keeps its height */
|
|
.stat-tile__newest {
|
|
display: flex;
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.stat-tile__newest-facts {
|
|
flex: none;
|
|
margin-left: 0.25em;
|
|
}
|
|
|
|
.stat-tile__newest-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
min-width: 0;
|
|
flex: 0 1 auto;
|
|
margin-left: 0.25em;
|
|
color: var(--text-color);
|
|
font-weight: 600;
|
|
}
|
|
|
|
|
|
.stat-tile__spark {
|
|
position: relative;
|
|
height: 48px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
/* Sparkline canvases are sized by their container, not by their attributes. */
|
|
.stat-tile__spark canvas {
|
|
width: 100% !important;
|
|
height: 48px !important;
|
|
}
|
|
|
|
/* ── Delta chips ───────────────────────────────────────────────────────── */
|
|
|
|
.delta-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
padding: 0.05rem 0.35rem;
|
|
border-radius: 999px;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.delta-chip--up {
|
|
background-color: rgba(25, 135, 84, 0.12);
|
|
color: #146c43;
|
|
}
|
|
|
|
.delta-chip--down {
|
|
background-color: rgba(220, 53, 69, 0.12);
|
|
color: #b02a37;
|
|
}
|
|
|
|
[data-theme="dark"] .delta-chip--up {
|
|
background-color: rgba(25, 135, 84, 0.22);
|
|
color: #75b798;
|
|
}
|
|
|
|
[data-theme="dark"] .delta-chip--down {
|
|
background-color: rgba(220, 53, 69, 0.22);
|
|
color: #ea868f;
|
|
}
|
|
|
|
/* ── Stacked route-mix bar ─────────────────────────────────────────────── */
|
|
|
|
.stacked-bar {
|
|
display: flex;
|
|
height: 26px;
|
|
width: 100%;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.stacked-bar__segment {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.stacked-bar__legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem 1rem;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.78rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.stacked-bar__swatch {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 2px;
|
|
margin-right: 0.35rem;
|
|
vertical-align: baseline;
|
|
}
|
|
|
|
/* ── Horizontal mix rows (role / device) ───────────────────────────────── */
|
|
|
|
.mix-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(5.5rem, 8rem) 1fr auto;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
margin-bottom: 0.35rem;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.mix-row__name {
|
|
color: var(--text-color);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mix-row__track {
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
background-color: var(--bg-tertiary);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mix-row__fill {
|
|
height: 100%;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.mix-row__count {
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* ── Direct-neighbour signal rows ──────────────────────────────────────── */
|
|
|
|
.neighbor-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(6rem, 11rem) 1fr auto;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
padding: 0.2rem 0;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.neighbor-row + .neighbor-row {
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.neighbor-row__name {
|
|
color: var(--text-color);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Fixed -12..+14 dB scale, so a bar means the same thing on every refresh. */
|
|
.neighbor-row__track {
|
|
height: 10px;
|
|
min-width: 3rem;
|
|
border-radius: 5px;
|
|
background-color: var(--bg-tertiary);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.neighbor-row__fill {
|
|
height: 100%;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
/* Hatched, not empty: "we have no reading" reads differently from "zero". */
|
|
.neighbor-row__track--unknown {
|
|
background-image: repeating-linear-gradient(
|
|
45deg,
|
|
var(--bg-tertiary),
|
|
var(--bg-tertiary) 3px,
|
|
transparent 3px,
|
|
transparent 6px
|
|
);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.neighbor-row__values {
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.neighbor-row__snr {
|
|
color: var(--text-color);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Leaderboard rows ──────────────────────────────────────────────────── */
|
|
|
|
.top-list-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.25rem 0;
|
|
}
|
|
|
|
.top-list-row + .top-list-row {
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.top-list-row__name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.top-list-row__rank {
|
|
flex: 0 0 1.5rem;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
text-align: right;
|
|
}
|
|
|
|
.top-list-row .tag-chip {
|
|
flex-shrink: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* ── Chart wrappers ────────────────────────────────────────────────────── */
|
|
|
|
.chart-box {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 200px;
|
|
}
|
|
|
|
.chart-box--tall {
|
|
height: 240px;
|
|
}
|
|
|
|
/* Cards in an equal-height row: let the chart or list take the slack instead
|
|
* of leaving it as dead space under a fixed-height canvas. Chart.js needs a
|
|
* positioned parent with a real height, which flex-basis: 0 + flex-grow gives
|
|
* it — plain height: 100% resolves against an auto-height parent and collapses.
|
|
*/
|
|
.card-body--fill {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-box--fill {
|
|
flex: 1 1 0;
|
|
min-height: 200px;
|
|
height: auto;
|
|
}
|
|
|
|
.mix-fill {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-evenly;
|
|
}
|
|
|
|
.doughnut-box {
|
|
position: relative;
|
|
max-width: 190px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.dashboard-note {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.dashboard-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
min-height: 100px;
|
|
color: var(--text-muted);
|
|
font-size: 0.85rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Info affordance shared by tooltip triggers in card headers. */
|
|
.dashboard-info-btn {
|
|
padding: 0;
|
|
border: 0;
|
|
background: none;
|
|
color: var(--text-muted);
|
|
line-height: 1;
|
|
}
|
|
|
|
.dashboard-info-btn:hover,
|
|
.dashboard-info-btn:focus {
|
|
color: #0dcaf0;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.tooltip.dashboard-hint .tooltip-inner {
|
|
max-width: min(22rem, 92vw);
|
|
text-align: left;
|
|
}
|
|
|
|
@media (max-width: 575.98px) {
|
|
.stat-tile__value {
|
|
font-size: 1.35rem;
|
|
}
|
|
|
|
.health-strip {
|
|
gap: 0.25rem 1rem;
|
|
}
|
|
}
|
|
|
|
/* Touch screens: the bare icon buttons get a 36px hit area. Padding with a
|
|
matching negative margin grows the target without moving the icon. */
|
|
@media (pointer: coarse) {
|
|
.dashboard-info-btn,
|
|
.health-strip .btn-refresh {
|
|
padding: 0.7rem;
|
|
margin: -0.7rem;
|
|
}
|
|
}
|