Seven rules join the four in the module guide, as rules 5 to 11: no
explanatory text in a tile's gear and no label over three words; server-side
settings in configSchema only; a tile setting changes that tile alone;
hide-toggles ungrouped when there are four or fewer and in a folded "Show"
group when there are five or more; title-bar controls are icons with
tooltips, never names; anything needing words belongs in the gear; and one
control per action, either place but not both.
Two passages said the opposite and now say the same thing — "keep labels
short and put the explanation there" is scoped to configSchema, and "icon
buttons, short labels" becomes icons with no labels at all. CLAUDE.md
carries the summary.
Then the modules. Nothing here changes a setting key or a default, so saved
layouts, named layouts and every tile already on a wall keep working; group
names, labels, help and fold state are presentation and are not stored.
- pco-plan: sixteen labels cut to three words or fewer ("Tint items by type
(song / media / other)" is now "Tint by type"), "Show / hide" renamed
"Show" and folded. Its one remaining help line stays by request: the top
bar's template needs its separator syntax spelled out, which the variable
chips beside it cannot convey, so rule 5 carves out template fields.
- propresenter-timers: the "Timers ▾" menu was the only named control left
in any title bar. It is a stopwatch icon now, with the count riding
alongside it while some timers are hidden — setLabel() writes over the
button's contents, so the count needed ProdCom's badge treatment rather
than a label. "Show" folds; the Colours help goes.
- propresenter-now-next: "Show" folds, in the manifest and in the Now
entry's own copy.
- prodcom-transcript: "Show channel icons" loses the redundant verb.
slack and clock were already compliant and are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
394 lines
13 KiB
CSS
394 lines
13 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;
|
|
}
|
|
|
|
/* the count beside the title-bar timers icon, while some are hidden */
|
|
[data-module="propresenter-timers"] .tm-count {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
[data-module="propresenter-timers"] .tm-count[hidden] { display: none; }
|