Off, every timer sits in one grid filled row by row — a wrapping flex row rather than a CSS grid, so the last row's cards widen to use the whole width and nothing is left empty; a source in trouble is named in one line above the cards, and its own cards dim. On, the sections per source come back, and "Source headings" appears (showWhen) to hide their titles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
386 lines
12 KiB
CSS
386 lines
12 KiB
CSS
/* Timers module — scoped to its tiles, shell variables only.
|
|
A card's colour IS its state: running / warning / overrun / idle. The four
|
|
tones are custom properties on the tile root: they default to the shell's
|
|
palette (so a dashboard theme reaches them) and the tile's own colour
|
|
settings, when changed, are set inline on .tm-wrap and win — the
|
|
documented mechanism. Overrun must be unmistakable across the room:
|
|
danger fill + pulse. */
|
|
|
|
[data-module="propresenter-timers"] .tm-wrap {
|
|
--tm-ok: var(--accent);
|
|
--tm-warn: var(--warn);
|
|
--tm-danger: var(--danger);
|
|
--tm-muted: var(--muted);
|
|
--tm-meta-size: 11px;
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
/* ── degraded-state banner (the ProdDash feed itself is down) ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-banner {
|
|
flex: none;
|
|
margin: 6px 8px 0;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
background: color-mix(in srgb, var(--danger) 14%, transparent);
|
|
color: var(--danger);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-banner[hidden] { display: none; }
|
|
|
|
/* Values still on screen while their source is down are stale — say so
|
|
visually, per group: the LTC card keeps ticking when only ProPresenter
|
|
is unreachable. When the ProdDash feed is down everything is old. */
|
|
[data-module="propresenter-timers"] .tm-group.is-stale .tm-card,
|
|
[data-module="propresenter-timers"] .tm-card.is-stale,
|
|
[data-module="propresenter-timers"] .tm-wrap.is-feed-down .tm-card { opacity: 0.45; }
|
|
|
|
[data-module="propresenter-timers"] .tm-hint {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 12px 16px;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-hint[hidden] { display: none; }
|
|
|
|
/* ── source groups: heading + card grid (column count set by fit()) ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-groups {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--tm-gap, 8px);
|
|
padding: var(--tm-pad, 10px);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-groups[hidden] { display: none; }
|
|
|
|
[data-module="propresenter-timers"] .tm-group {
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1 1 0;
|
|
}
|
|
|
|
[data-module="propresenter-timers"] .tm-head {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
margin: 0 2px 4px;
|
|
font-size: 11px;
|
|
line-height: 16px;
|
|
color: var(--muted);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-head[hidden] { display: none; }
|
|
|
|
[data-module="propresenter-timers"] .tm-title {
|
|
flex: none;
|
|
font-weight: 800;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* The source's own word while it isn't OK ("Unreachable at …"). */
|
|
[data-module="propresenter-timers"] .tm-note {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-group.is-down .tm-note { color: var(--tm-warn); }
|
|
[data-module="propresenter-timers"] .tm-group.is-stale .tm-note { color: var(--tm-danger); }
|
|
|
|
/* Headings off: a source in trouble, or with nothing to show, still says so. */
|
|
[data-module="propresenter-timers"] .tm-empty {
|
|
flex: none;
|
|
padding: 2px 4px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-empty[hidden] { display: none; }
|
|
[data-module="propresenter-timers"] .tm-group.is-down .tm-empty { color: var(--tm-warn); }
|
|
|
|
[data-module="propresenter-timers"] .tm-grid {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-auto-rows: 1fr;
|
|
gap: var(--tm-gap, 8px);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-grid:empty { display: none; }
|
|
|
|
/* Grouping off: one grid for every timer, filled row by row. A wrapping
|
|
flex row instead of a CSS grid, so the last row's cards widen to use the
|
|
whole width — nothing is left empty. Rows share the height equally. */
|
|
[data-module="propresenter-timers"] .tm-grid.is-flow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-content: stretch;
|
|
align-items: stretch;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-grid.is-flow .tm-card {
|
|
flex: 1 1 calc((100% - var(--tm-gap, 8px) * (var(--tm-cols, 1) - 1)) / var(--tm-cols, 1));
|
|
min-width: 0;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-empty[data-tone="warn"] { color: var(--tm-warn); }
|
|
|
|
/* Compact cards (short tiles) and "Status lines" off: the lines under the
|
|
digits go, the colour stays. A note that explains a missing value (no
|
|
LTC input, a solo timer no longer offered) must never be hidden. */
|
|
[data-module="propresenter-timers"] .tm-compact .tm-status:not(.is-note),
|
|
[data-module="propresenter-timers"] .tm-compact .tm-detail,
|
|
[data-module="propresenter-timers"] .tm-nostatus .tm-status:not(.is-note),
|
|
[data-module="propresenter-timers"] .tm-nodetail .tm-detail { display: none; }
|
|
/* "Names" off hides every title; the clock never carries one — its digits say what it is */
|
|
[data-module="propresenter-timers"] .tm-nonames .tm-name,
|
|
[data-module="propresenter-timers"] .tm-card-clock .tm-name { display: none; }
|
|
|
|
/* ── one timer card ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-card {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 2%;
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
background: color-mix(in srgb, var(--bg) 60%, transparent);
|
|
overflow: hidden;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
[data-module="propresenter-timers"] .tm-name {
|
|
flex: none;
|
|
max-width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
font-size: var(--tm-meta-size);
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The label is its own box so a long name ends in an ellipsis (a flex
|
|
container's bare text won't). */
|
|
[data-module="propresenter-timers"] .tm-label {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-module="propresenter-timers"] .tm-time {
|
|
flex: none;
|
|
font-size: 32px; /* fit() sets the real size per card */
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.02em;
|
|
line-height: 1.05;
|
|
color: var(--text);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Admin "Timer font: Monospace" — steadier digits as the numbers change. */
|
|
[data-module="propresenter-timers"] .tm-mono .tm-time {
|
|
font-family: ui-monospace, "SF Mono", "Menlo", "Cascadia Mono", "Consolas", monospace;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
[data-module="propresenter-timers"] .tm-status,
|
|
[data-module="propresenter-timers"] .tm-detail {
|
|
flex: none;
|
|
max-width: 100%;
|
|
font-size: calc(var(--tm-meta-size) * 0.8); /* smaller than the title; fit() budgets the same share */
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-status[hidden],
|
|
[data-module="propresenter-timers"] .tm-detail[hidden] { display: none; }
|
|
|
|
[data-module="propresenter-timers"] .tm-detail {
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: none;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* A provider's status line carries its own tone ("ahead 1:20" in green). */
|
|
[data-module="propresenter-timers"] .tm-status[data-tone="ok"] { color: var(--tm-ok); }
|
|
[data-module="propresenter-timers"] .tm-status[data-tone="warn"] { color: var(--tm-warn); }
|
|
[data-module="propresenter-timers"] .tm-status[data-tone="danger"] { color: var(--tm-danger); }
|
|
[data-module="propresenter-timers"] .tm-status[data-tone="muted"] { color: var(--tm-muted); }
|
|
|
|
/* The clock's date: one line when it fits, else the day on its own line
|
|
(renderDate() sizes it to the longer half). */
|
|
[data-module="propresenter-timers"] .tm-status.is-date {
|
|
white-space: normal;
|
|
overflow: visible;
|
|
text-overflow: clip;
|
|
text-align: center;
|
|
line-height: 1.15;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-status.is-date span { white-space: nowrap; }
|
|
[data-module="propresenter-timers"] .tm-status.is-date.is-wrap span { display: block; }
|
|
|
|
/* Explanatory notes read as sentences, not badges. */
|
|
[data-module="propresenter-timers"] .tm-status.is-note {
|
|
text-transform: none;
|
|
letter-spacing: 0.02em;
|
|
white-space: normal;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── state looks (feedbacks) ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-card.is-ok {
|
|
border-color: color-mix(in srgb, var(--tm-ok) 55%, var(--border));
|
|
background: color-mix(in srgb, var(--tm-ok) 10%, transparent);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-card.is-ok .tm-time { color: var(--tm-ok); }
|
|
|
|
[data-module="propresenter-timers"] .tm-card.is-warn {
|
|
border-color: color-mix(in srgb, var(--tm-warn) 55%, var(--border));
|
|
background: color-mix(in srgb, var(--tm-warn) 10%, transparent);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-card.is-warn .tm-time { color: var(--tm-warn); }
|
|
|
|
[data-module="propresenter-timers"] .tm-card.is-muted .tm-time { color: var(--tm-muted); }
|
|
|
|
/* The wall clock is information, not a state — plain text colour. */
|
|
[data-module="propresenter-timers"] .tm-card.is-plain .tm-time { color: var(--text); }
|
|
|
|
/* Overrun: readable across the room even on a wall display. */
|
|
[data-module="propresenter-timers"] .tm-card.is-danger {
|
|
border-color: var(--tm-danger);
|
|
background: color-mix(in srgb, var(--tm-danger) 22%, transparent);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-card.is-danger .tm-time { color: var(--tm-danger); }
|
|
[data-module="propresenter-timers"] .tm-card.is-overrun {
|
|
animation: tm-overrun-pulse 1.2s ease-in-out infinite;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-card.is-overrun .tm-status { font-weight: 800; }
|
|
|
|
@keyframes tm-overrun-pulse {
|
|
50% { background: color-mix(in srgb, var(--tm-danger) 38%, transparent); }
|
|
}
|
|
|
|
/* A solo tile whose timer is no longer offered. */
|
|
[data-module="propresenter-timers"] .tm-card.is-missing { border-style: dashed; }
|
|
|
|
/* ── the LTC card: visually its own thing ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-card-ltc { border-style: dashed; }
|
|
|
|
[data-module="propresenter-timers"] .tm-ltc-badge {
|
|
flex: none;
|
|
padding: 0.15em 0.55em;
|
|
border-radius: 5px;
|
|
background: color-mix(in srgb, var(--tm-ok) 24%, transparent);
|
|
color: var(--text);
|
|
font-size: 0.95em;
|
|
letter-spacing: 0.12em;
|
|
}
|
|
|
|
/* No signal: muted digits, a blinking warning line. */
|
|
[data-module="propresenter-timers"] .tm-card.is-nosignal {
|
|
border-color: color-mix(in srgb, var(--tm-warn) 55%, var(--border));
|
|
}
|
|
[data-module="propresenter-timers"] .tm-card.is-nosignal .tm-status {
|
|
animation: tm-nosignal-blink 1.6s steps(2) infinite;
|
|
}
|
|
|
|
@keyframes tm-nosignal-blink { 50% { opacity: 0.35; } }
|
|
|
|
/* ── the "Timers ▾" menu (container styled/positioned by the shell) ── */
|
|
|
|
[data-module="propresenter-timers"] .tm-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
width: 100%;
|
|
padding: 6px 9px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
font-family: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-menu-item:hover { background: var(--border); }
|
|
[data-module="propresenter-timers"] .tm-menu-item .name { font-weight: 600; }
|
|
[data-module="propresenter-timers"] .tm-menu-item:not(.on) .name { color: var(--muted); }
|
|
|
|
/* A small switch: off = hollow pill, on = filled with the accent. (The menu
|
|
hangs off the tile's title bar, outside .tm-wrap, so the tile's own colour
|
|
properties don't reach it — the shell's palette does.) */
|
|
[data-module="propresenter-timers"] .tm-switch {
|
|
flex: none;
|
|
position: relative;
|
|
width: 26px;
|
|
height: 14px;
|
|
border-radius: 7px;
|
|
border: 1px solid var(--border);
|
|
background: color-mix(in srgb, var(--muted) 25%, transparent);
|
|
transition: background 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 1px;
|
|
left: 1px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: var(--text);
|
|
transition: transform 0.15s ease;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-menu-item.on .tm-switch {
|
|
border-color: var(--accent);
|
|
background: var(--accent);
|
|
}
|
|
[data-module="propresenter-timers"] .tm-menu-item.on .tm-switch::after {
|
|
transform: translateX(12px);
|
|
background: var(--bg);
|
|
}
|
|
|
|
[data-module="propresenter-timers"] .tm-menu-note {
|
|
padding: 6px 9px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|