feat: initial Signage 0.1.0

This commit is contained in:
ekklesia
2026-08-28 16:41:54 -05:00
parent 20756f11af
commit 92bef4c7aa
107 changed files with 24397 additions and 0 deletions
+522
View File
@@ -0,0 +1,522 @@
/*
* The Signage management UI's own stylesheet.
*
* Compiled into the declared browser artifact as text and installed by the host into this artifact's
* isolated root at mount (ADR 0040), so these selectors reach nothing outside Signage and nothing
* outside Signage reaches in.
*
* Every value is either a registered Ekklesia token or a private `--signage-*` alias derived from
* one. Signage declares no `--ekklesia-*` property: those belong to the platform's global theme, and
* the build rejects a stylesheet that invents one (ADR 0039). Nothing here recolours a provider's
* output or an uploaded image — the Dashboard's theme is the Dashboard's, and a television's content
* belongs to whoever made it.
*/
/*
* The box model for the tree Signage owns.
*
* An isolated root inherits nothing from the page around it, and the platform's stylesheet declares
* no reset of its own — so under the initial `content-box` a `.ek-control`, which is `width: 100%`,
* is laid out wider than the field containing it by its own padding and border, and slides under
* whatever sits beside it. Setting the box model once here fixes that everywhere, rather than
* subtracting padding by hand in each panel.
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
:host {
--signage-gap: var(--ekklesia-spacing-lg);
--signage-gap-tight: var(--ekklesia-spacing-sm);
--signage-panel-radius: var(--ekklesia-radius-lg);
--signage-thumbnail-width: 220px;
}
.signage-desktop {
display: none;
}
.signage-narrow-notice {
background: var(--ekklesia-background-surface);
border: var(--ekklesia-border-width) solid var(--ekklesia-border-base);
border-radius: var(--signage-panel-radius);
color: var(--ekklesia-text-primary);
display: grid;
font-family: var(--ekklesia-typography-family-sans);
gap: var(--signage-gap-tight);
padding: var(--ekklesia-spacing-xl);
}
.signage-narrow-notice__title {
font-size: var(--ekklesia-typography-size-heading-3);
font-weight: var(--ekklesia-typography-weight-bold);
line-height: var(--ekklesia-typography-line-height-tight);
margin: 0;
}
.signage-narrow-notice__body {
color: var(--ekklesia-text-secondary);
line-height: var(--ekklesia-typography-line-height-normal);
margin: 0;
}
@media (min-width: 1024px) {
.signage-narrow-notice {
display: none;
}
.signage-desktop {
display: grid;
gap: var(--signage-gap);
}
}
.signage-columns {
display: grid;
gap: var(--signage-gap);
}
.signage-inline-form {
align-items: end;
display: flex;
flex-wrap: wrap;
gap: var(--signage-gap-tight);
}
/*
* A labelled control beside the button that submits it. The field takes the width its control asks
* for and is allowed to shrink below it, so a narrow header squeezes the control rather than sliding
* the button on top of it.
*/
.signage-inline-form > .ek-field {
min-width: 0;
position: relative;
}
.signage-inline-form .ek-control {
min-width: 0;
}
/*
* On one line, a field's hint hangs below the row rather than making that one field taller. The row
* is bottom-aligned so the controls share a line with the button, and a hint left in flow would push
* its own control above the others.
*/
.signage-inline-form > .ek-field > .ek-field__hint {
inset-block-start: 100%;
inset-inline-start: 0;
padding-block-start: var(--ekklesia-spacing-2xs);
position: absolute;
}
.signage-inline-form:has(.ek-field__hint) {
padding-block-end: var(--ekklesia-spacing-lg);
}
.signage-failure {
color: var(--ekklesia-text-danger);
font-size: var(--ekklesia-typography-size-sm);
margin: var(--signage-gap-tight) 0 0;
}
/*
* A list row that carries a name beside its metadata. The platform's list item is a plain block, so
* without this the two run together into one unreadable word — and the row's own children already
* assume a flex line.
*/
.signage-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: var(--ekklesia-spacing-sm);
}
.signage-row__name {
flex: 1 1 auto;
}
/* ---------------------------------------------------------------------------------------------
* Images.
* ------------------------------------------------------------------------------------------- */
.signage-upload-trigger {
cursor: pointer;
display: inline-flex;
}
.signage-upload-input {
height: 1px;
opacity: 0;
position: absolute;
width: 1px;
}
.signage-uploads {
display: grid;
gap: var(--signage-gap-tight);
margin-bottom: var(--signage-gap);
}
.signage-uploads__message {
color: var(--ekklesia-text-secondary);
font-size: var(--ekklesia-typography-size-sm);
}
.signage-image-grid {
display: grid;
gap: var(--signage-gap);
grid-template-columns: repeat(auto-fill, minmax(var(--signage-thumbnail-width), 1fr));
list-style: none;
margin: 0;
padding: 0;
}
.signage-image {
/* Rows sized to their content, with the slack from a taller neighbour left at the bottom rather
than spread between the thumbnail and the name. */
align-content: start;
background: var(--ekklesia-background-canvas);
border: var(--ekklesia-border-width) solid var(--ekklesia-border-divider);
border-radius: var(--ekklesia-radius-md);
display: grid;
gap: var(--ekklesia-spacing-xs);
overflow: hidden;
padding: var(--ekklesia-spacing-sm);
}
/* One box, whatever shape the image is: a library of mixed aspect ratios still reads as a grid, and
a tall image is letterboxed here rather than reflowing the card it sits in. */
.signage-image__thumbnail {
aspect-ratio: 16 / 9;
background: var(--ekklesia-background-inset);
border-radius: var(--ekklesia-radius-sm);
display: block;
height: auto;
max-width: 100%;
object-fit: contain;
width: 100%;
}
.signage-image__missing {
align-items: center;
aspect-ratio: 16 / 9;
background: var(--ekklesia-background-inset);
border-radius: var(--ekklesia-radius-sm);
color: var(--ekklesia-text-muted);
display: flex;
font-size: var(--ekklesia-typography-size-sm);
justify-content: center;
}
.signage-image__meta {
display: grid;
gap: var(--ekklesia-spacing-2xs);
}
/* A badge is sized by what it says, not by the column it lands in. */
.signage-image__expiry {
justify-self: start;
}
.signage-image__name {
font-weight: var(--ekklesia-typography-weight-medium);
margin: 0;
overflow-wrap: anywhere;
}
.signage-image__detail {
color: var(--ekklesia-text-secondary);
font-size: var(--ekklesia-typography-size-sm);
margin: 0;
}
.signage-image__actions {
display: flex;
flex-wrap: wrap;
gap: var(--ekklesia-spacing-2xs);
}
/* ---------------------------------------------------------------------------------------------
* Timelines.
* ------------------------------------------------------------------------------------------- */
.signage-editor {
display: grid;
gap: var(--signage-gap);
}
.signage-editor__title {
font-size: var(--ekklesia-typography-size-heading-3);
font-weight: var(--ekklesia-typography-weight-bold);
margin: 0;
}
.signage-editor__subtitle {
color: var(--ekklesia-text-secondary);
font-size: var(--ekklesia-typography-size-sm);
font-weight: var(--ekklesia-typography-weight-medium);
margin: 0;
text-transform: uppercase;
}
.signage-editor__add,
.signage-editor__sources,
.signage-editor__footer {
display: flex;
flex-wrap: wrap;
gap: var(--signage-gap-tight);
}
.signage-editor__sources {
display: grid;
gap: var(--ekklesia-spacing-xs);
}
.signage-editor__source {
align-items: center;
display: flex;
gap: var(--signage-gap-tight);
}
.signage-editor__order {
display: flex;
gap: var(--ekklesia-spacing-2xs);
}
/* ---------------------------------------------------------------------------------------------
* Playback.
* ------------------------------------------------------------------------------------------- */
.signage-playback {
display: grid;
gap: var(--signage-gap);
}
.signage-playback__status,
.signage-playback__controls,
.signage-playback__activate {
align-items: end;
display: flex;
flex-wrap: wrap;
gap: var(--signage-gap-tight);
}
.signage-playback__current {
column-gap: var(--signage-gap);
display: grid;
grid-template-columns: max-content 1fr;
margin: 0;
row-gap: var(--ekklesia-spacing-2xs);
}
.signage-playback__current dt {
color: var(--ekklesia-text-secondary);
font-size: var(--ekklesia-typography-size-sm);
}
.signage-playback__current dd {
font-variant-numeric: tabular-nums;
margin: 0;
}
.signage-playback__history-meta {
color: var(--ekklesia-text-secondary);
font-size: var(--ekklesia-typography-size-sm);
}
/* ---------------------------------------------------------------------------------------------
* Displays and providers.
* ------------------------------------------------------------------------------------------- */
.signage-groups {
display: grid;
gap: var(--signage-gap);
}
.signage-group {
border: var(--ekklesia-border-width) solid var(--ekklesia-border-divider);
border-radius: var(--ekklesia-radius-md);
display: grid;
gap: var(--signage-gap-tight);
padding: var(--ekklesia-spacing-md);
}
.signage-group__header {
align-items: center;
display: flex;
gap: var(--signage-gap-tight);
}
.signage-provider__id {
font-weight: var(--ekklesia-typography-weight-medium);
}
.signage-provider__version {
color: var(--ekklesia-text-muted);
font-size: var(--ekklesia-typography-size-sm);
/* Far enough that the version reads as a second word, not the tail of the identifier. */
margin-left: var(--ekklesia-spacing-sm);
}
/* ---------------------------------------------------------------------------------------------
* Live preview.
*
* The frame is a 16:9 box with a black ground, because that is what a television is. Inside it the
* display's own scene layers render exactly as they do on a wall — this panel supplies a size and
* nothing else.
* ------------------------------------------------------------------------------------------- */
.signage-preview {
display: grid;
gap: var(--signage-gap-tight);
}
.signage-preview__frame {
aspect-ratio: 16 / 9;
background: #000000;
border: var(--ekklesia-border-width) solid var(--ekklesia-border-strong);
border-radius: var(--ekklesia-radius-md);
overflow: hidden;
position: relative;
width: 100%;
}
.signage-preview .signage-stage {
height: 100%;
inset: 0;
min-height: 0;
position: absolute;
width: 100%;
}
.signage-preview .signage-layer {
height: 100%;
inset: 0;
opacity: 0;
position: absolute;
transition-duration: 0ms;
transition-property: opacity;
transition-timing-function: linear;
width: 100%;
}
.signage-preview .signage-layer__background {
background: #000000;
inset: 0;
position: absolute;
}
.signage-preview .signage-layer__background-image {
display: none;
height: 100%;
inset: 0;
object-fit: cover;
position: absolute;
transform: scale(1.08);
width: 100%;
}
.signage-preview .signage-layer__darken {
background: #000000;
inset: 0;
opacity: 0;
position: absolute;
}
.signage-preview .signage-layer__foreground {
height: 100%;
inset: 0;
object-fit: contain;
position: absolute;
width: 100%;
}
.signage-preview .signage-layer__renderer {
inset: 0;
position: absolute;
}
.signage-preview .signage-blackout {
background: #000000;
inset: 0;
opacity: 0;
pointer-events: none;
position: absolute;
transition: opacity 200ms linear;
z-index: 20;
}
.signage-preview [data-blackout="true"] > .signage-blackout {
opacity: 1;
}
.signage-preview .signage-slate-host {
inset: 0;
opacity: 0;
pointer-events: none;
position: absolute;
z-index: 10;
}
.signage-preview [data-slate="true"] > .signage-slate-host {
opacity: 1;
}
.signage-preview .signage-slate {
align-items: stretch;
display: flex;
flex-direction: column;
height: 100%;
inset: 0;
position: absolute;
width: 100%;
}
.signage-preview .signage-slate__bars {
display: grid;
flex: 1 1 auto;
grid-template-columns: repeat(6, 1fr);
}
.signage-preview .signage-slate__bar {
display: block;
height: 100%;
}
.signage-preview .signage-slate__bar--one {
background: #6f6f6f;
}
.signage-preview .signage-slate__bar--two {
background: #6f6f2f;
}
.signage-preview .signage-slate__bar--three {
background: #2f6f6f;
}
.signage-preview .signage-slate__bar--four {
background: #2f6f2f;
}
.signage-preview .signage-slate__bar--five {
background: #6f2f6f;
}
.signage-preview .signage-slate__bar--six {
background: #6f2f2f;
}
.signage-preview .signage-slate__message {
background: #000000;
color: #ffffff;
flex: 0 0 auto;
font-size: 2vh;
letter-spacing: 0.08em;
margin: 0;
padding: 1.5vh 0;
text-align: center;
text-transform: uppercase;
}