feat: initial Signage 0.1.0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user