Optimisations and updated readme

This commit is contained in:
2026-07-14 12:50:52 +01:00
parent 9709457b73
commit ff23474a54
33 changed files with 3073 additions and 2426 deletions
-356
View File
@@ -1,356 +0,0 @@
.slide-editor-top {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 16px;
align-items: start;
margin-bottom: 16px;
}
.slide-image-file {
margin-top: 8px;
}
.slide-preview-card {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.slide-preview-header {
margin-bottom: 0;
padding: 16px 16px 8px;
}
.slide-preview-header h3 {
margin: 0;
}
.slide-preview-stage {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: 0;
background: linear-gradient(135deg, #111827, #1f2937);
}
.slide-preview-scroll {
min-height: 0;
padding: 0 16px 16px;
margin-top: 0;
}
.slide-preview-scroll button {
margin-bottom: 8px;
}
.slide-preview-background {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.slide-preview-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
color: #cbd5e1;
background: rgba(17, 24, 39, 0.35);
z-index: 1;
}
.slide-preview-overlay {
position: absolute;
inset: 0;
z-index: 2;
}
.slide-preview-region {
position: absolute;
box-sizing: border-box;
padding: 0;
overflow: hidden;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(15, 23, 42, 0.2);
backdrop-filter: blur(1px);
}
.slide-preview-region h1,
.slide-preview-region h2,
.slide-preview-region h3,
.slide-preview-region h4,
.slide-preview-region h5,
.slide-preview-region h6,
.slide-preview-region p,
.slide-preview-region blockquote,
.slide-preview-region ul,
.slide-preview-region ol,
.slide-preview-region pre {
margin: 0;
}
.slide-preview-region ul,
.slide-preview-region ol {
padding-left: 18px;
}
.slide-preview-region code {
white-space: pre-wrap;
}
.slide-preview-text-content {
width: 100%;
height: 100%;
}
.slide-preview-image-region {
background: rgba(15, 23, 42, 0.25);
}
.slide-preview-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.slide-preview-webpage-region {
background: rgba(15, 23, 42, 0.35);
overflow: hidden;
}
.slide-preview-webpage-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
background: #fff;
overflow: hidden;
}
.slide-preview-html-region {
background: rgba(15, 23, 42, 0.35);
overflow: hidden;
}
.slide-preview-html-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
background: transparent;
overflow: hidden;
}
.slide-schedule-dialog {
width: min(900px, calc(100vw - 32px));
max-width: 900px;
border: none;
border-radius: 12px;
padding: 16px;
background: var(--surface);
color: var(--text);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
}
.slide-schedule-dialog::backdrop {
background: rgba(15, 23, 42, 0.55);
}
.slide-schedule-dialog iframe {
width: 100%;
height: auto;
min-height: 120px;
border: 0;
margin-top: 0;
background: var(--surface);
}
.slide-schedule-dialog-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.slide-schedule-dialog button {
width: auto;
margin: 0;
}
.slide-schedule-dialog button.secondary {
box-shadow: none;
}
.slide-schedule-dialog button.secondary:hover {
filter: brightness(0.92);
}
.slide-schedule-dialog form {
display: grid;
gap: 16px;
}
.slide-schedule-dialog .card {
display: grid;
gap: 12px;
}
.schedule-type-field {
display: block;
margin-bottom: 20px;
}
.schedule-day-option {
margin-top: 0;
justify-content: flex-start;
width: 100%;
gap: 14px;
}
.schedule-day-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: 8px;
margin-top: 16px;
}
.schedule-day-label {
font-size: 13px;
font-weight: 700;
color: var(--text);
line-height: 1.1;
}
.schedule-day-option .switch-ui {
flex: none;
}
.schedule-day-option input:focus-visible + .switch-ui {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
.schedule-day-option input:checked + .switch-ui {
background: linear-gradient(180deg, var(--primary), var(--primary-dark));
border-color: rgba(37, 99, 235, 0.55);
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.08), 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.schedule-save-actions {
margin-top: 16px;
}
.schedule-save-actions button {
margin-top: 0;
}
.slide-preview-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #cbd5e1;
font-size: 12px;
}
.slide-preview-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
background: none;
}
.slide-preview-table tbody tr {
background: none;
border: default;
}
.slide-preview-table th,
.slide-preview-table td {
border: 1px solid rgba(255, 255, 255, 0.35);
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
.slide-preview-table th {
font-weight: 700;
}
.rich-editor {
display: grid;
gap: 8px;
}
.rich-toolbar {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.rich-toolbar button {
width: auto;
margin-top: 0;
padding: 8px 10px;
}
.rich-surface {
min-height: 240px;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff;
color: var(--text);
line-height: 1.4;
overflow: auto;
}
.rich-surface:focus {
outline: 2px solid rgba(37, 99, 235, 0.3);
border-color: var(--primary);
}
.editorjs-holder {
min-height: 240px;
border: 1px solid var(--border-strong);
border-radius: 8px;
padding: 16px 12px 12px;
background: var(--editor-paper);
color: var(--editor-text);
margin-top: 8px;
}
.editorjs-holder .codex-editor,
.editorjs-holder .codex-editor__redactor,
.editorjs-holder .ce-block,
.editorjs-holder .ce-paragraph,
.editorjs-holder .ce-header,
.editorjs-holder .ce-list,
.editorjs-holder .ce-list__item,
.editorjs-holder [contenteditable],
.editorjs-holder [contenteditable] * {
color: var(--editor-text);
}
.editorjs-holder [contenteditable] {
caret-color: var(--editor-text);
}
.editorjs-holder .codex-editor__redactor [contenteditable]:empty:after {
color: rgba(0, 0, 0, 0.45);
}
@media (max-width: 1100px) {
.slide-editor-top {
grid-template-columns: 1fr;
}
}
+614
View File
@@ -110,6 +110,7 @@ a {
color: var(--primary);
}
/* Shared admin shell and global UI used across most admin pages. */
.admin-shell-body {
overflow-x: hidden;
}
@@ -517,6 +518,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
padding: 28px;
}
/* Dashboard and list-style admin pages. */
.page-shell {
display: grid;
gap: 18px;
@@ -608,6 +610,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
margin-bottom: 0;
}
/* Authentication pages. */
.auth-shell-body {
background: var(--auth-body-bg);
}
@@ -676,6 +679,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
line-height: 1.5;
}
/* Account and user management pages. */
.account-card {
max-width: 720px;
}
@@ -765,6 +769,25 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
white-space: nowrap;
}
/* Text region controls. */
.template-field-head {
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.template-text-style-row {
margin-top: 0;
align-items: flex-end;
}
.template-text-style-row input[type="color"] {
height: 44px;
padding: 4px;
}
.users-table th:last-child {
width: 220px;
@@ -798,6 +821,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
align-items: start;
}
/* Playlist, canvas size, template, and screen edit pages. */
.user-inline-form .button-link,
.user-password-form .button-link {
width: fit-content;
@@ -883,6 +907,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
margin-top: 0;
}
/* Shared table, form, and widget styling used across admin pages. */
.card h3 {
margin: 0 0 14px;
font-size: 18px;
@@ -1049,6 +1074,7 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
justify-content: flex-end;
}
/* Shared form controls. */
label {
display: block;
font-size: 14px;
@@ -1408,6 +1434,7 @@ tbody tr:last-child td {
}
}
/* Responsive shell behavior. */
@media (max-width: 1100px) {
.app-shell {
grid-template-columns: 1fr;
@@ -1885,3 +1912,590 @@ table td .actions form {
margin-left: 6px;
}
/* Slide editor pages. */
.slide-editor-top {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 16px;
align-items: start;
margin-bottom: 16px;
}
.slide-image-file {
margin-top: 8px;
}
.slide-preview-card {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.slide-preview-header {
margin-bottom: 0;
padding: 16px 16px 8px;
}
.slide-preview-header h3 {
margin: 0;
}
.slide-preview-stage {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: 0;
background: linear-gradient(135deg, #111827, #1f2937);
}
.slide-preview-scroll {
min-height: 0;
padding: 0 16px 16px;
margin-top: 0;
}
.slide-preview-scroll button {
margin-bottom: 8px;
}
.slide-preview-background {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.slide-preview-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
color: #cbd5e1;
background: rgba(17, 24, 39, 0.35);
z-index: 1;
}
.slide-preview-overlay {
position: absolute;
inset: 0;
z-index: 2;
}
.slide-preview-region {
position: absolute;
box-sizing: border-box;
padding: 0;
overflow: hidden;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(15, 23, 42, 0.2);
backdrop-filter: blur(1px);
}
.slide-preview-region h1,
.slide-preview-region h2,
.slide-preview-region h3,
.slide-preview-region h4,
.slide-preview-region h5,
.slide-preview-region h6,
.slide-preview-region p,
.slide-preview-region blockquote,
.slide-preview-region ul,
.slide-preview-region ol,
.slide-preview-region pre {
margin: 0;
}
.slide-preview-region ul,
.slide-preview-region ol {
padding-left: 18px;
}
.slide-preview-region code {
white-space: pre-wrap;
}
.slide-preview-text-content {
width: 100%;
height: 100%;
}
.slide-preview-image-region {
background: rgba(15, 23, 42, 0.25);
}
.slide-preview-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.slide-preview-webpage-region {
background: rgba(15, 23, 42, 0.35);
overflow: hidden;
}
.slide-preview-webpage-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
background: #fff;
overflow: hidden;
}
.slide-preview-html-region {
background: rgba(15, 23, 42, 0.35);
overflow: hidden;
}
.slide-preview-html-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
background: transparent;
overflow: hidden;
}
.slide-schedule-dialog {
width: min(900px, calc(100vw - 32px));
max-width: 900px;
border: none;
border-radius: 12px;
padding: 16px;
background: var(--surface);
color: var(--text);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
}
.slide-schedule-dialog::backdrop {
background: rgba(15, 23, 42, 0.55);
}
.slide-schedule-dialog iframe {
width: 100%;
height: auto;
min-height: 120px;
border: 0;
margin-top: 0;
background: var(--surface);
}
.slide-schedule-dialog-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.slide-schedule-dialog button {
width: auto;
margin: 0;
}
.slide-schedule-dialog button.secondary {
box-shadow: none;
}
.slide-schedule-dialog button.secondary:hover {
filter: brightness(0.92);
}
.slide-schedule-dialog form {
display: grid;
gap: 16px;
}
.slide-schedule-dialog .card {
display: grid;
gap: 12px;
}
.schedule-type-field {
display: block;
margin-bottom: 20px;
}
.schedule-day-option {
margin-top: 0;
justify-content: flex-start;
width: 100%;
gap: 14px;
}
.schedule-day-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: 8px;
margin-top: 16px;
}
.schedule-day-label {
font-size: 13px;
font-weight: 700;
color: var(--text);
line-height: 1.1;
}
.schedule-day-option .switch-ui {
flex: none;
}
.schedule-day-option input:focus-visible + .switch-ui {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
.schedule-day-option input:checked + .switch-ui {
background: linear-gradient(180deg, var(--primary), var(--primary-dark));
border-color: rgba(37, 99, 235, 0.55);
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.08), 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.schedule-save-actions {
margin-top: 16px;
}
.schedule-save-actions button {
margin-top: 0;
}
.slide-preview-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #cbd5e1;
font-size: 12px;
}
.slide-preview-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
background: none;
}
.slide-preview-table tbody tr {
background: none;
border: 0;
}
.slide-preview-table th,
.slide-preview-table td {
border: 1px solid rgba(255, 255, 255, 0.35);
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
.slide-preview-table th {
font-weight: 700;
}
.rich-editor {
display: grid;
gap: 8px;
}
.rich-toolbar {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.rich-toolbar button {
width: auto;
margin-top: 0;
padding: 8px 10px;
}
.rich-surface {
min-height: 240px;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff;
color: var(--text);
line-height: 1.4;
overflow: auto;
}
.rich-surface:focus {
outline: 2px solid rgba(37, 99, 235, 0.3);
border-color: var(--primary);
}
.editorjs-holder {
min-height: 240px;
border: 1px solid var(--border-strong);
border-radius: 8px;
padding: 16px 12px 12px;
background: var(--editor-paper);
color: var(--editor-text);
margin-top: 8px;
}
.editorjs-holder .codex-editor,
.editorjs-holder .codex-editor__redactor,
.editorjs-holder .ce-block,
.editorjs-holder .ce-paragraph,
.editorjs-holder .ce-header,
.editorjs-holder .ce-list,
.editorjs-holder .ce-list__item,
.editorjs-holder [contenteditable],
.editorjs-holder [contenteditable] * {
color: var(--editor-text);
}
.editorjs-holder [contenteditable] {
caret-color: var(--editor-text);
}
.editorjs-holder .codex-editor__redactor [contenteditable]:empty:after {
color: rgba(0, 0, 0, 0.45);
}
@media (max-width: 1100px) {
.slide-editor-top {
grid-template-columns: 1fr;
}
}
/* Template designer pages. */
.template-designer-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
gap: 12px;
align-items: start;
}
.template-designer-main {
display: grid;
gap: 8px;
}
.template-meta-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
.template-meta-row > label {
flex: 1 1 0;
margin-top: 0;
}
.template-designer-sidebar {
display: grid;
gap: 8px;
}
.template-designer-actions {
display: flex;
flex-wrap: nowrap;
gap: 8px;
margin: 0 0 8px 0;
}
#template-form .template-designer-actions button {
flex: 1 1 0;
width: auto;
margin-top: 0;
}
.region-select {
width: 100%;
margin-bottom: 8px;
}
#canvas-size-select:disabled {
opacity: 0.55;
cursor: not-allowed;
}
#canvas-size-summary.is-locked {
color: #94a3b8;
}
.designer-stage {
position: relative;
width: 100%;
background: #0f172a;
border-radius: 0;
overflow: hidden;
border: 1px solid #1f2937;
cursor: crosshair;
user-select: none;
box-sizing: border-box;
}
.designer-stage img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
pointer-events: none;
}
.designer-stage .empty-bg {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 14px;
pointer-events: none;
}
.designer-overlay {
position: absolute;
inset: 0;
}
.designer-rect {
position: absolute;
border: 2px solid #22c55e;
background: rgba(34, 197, 94, 0.14);
color: #fff;
font-size: 12px;
overflow: hidden;
box-sizing: border-box;
}
.designer-rect.selected {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.18);
}
.designer-rect-label {
position: absolute;
left: 0;
top: 0;
padding: 2px 6px;
background: rgba(15, 23, 42, 0.8);
font-size: 11px;
pointer-events: none;
}
.designer-rect .resize-handle {
position: absolute;
width: 12px;
height: 12px;
background: var(--surface);
border: 1px solid var(--border-strong);
box-sizing: border-box;
}
.designer-rect .resize-handle.nw {
left: -7px;
top: -7px;
cursor: nwse-resize;
}
.designer-rect .resize-handle.ne {
right: -7px;
top: -7px;
cursor: nesw-resize;
}
.designer-rect .resize-handle.sw {
left: -7px;
bottom: -7px;
cursor: nesw-resize;
}
.designer-rect .resize-handle.se {
right: -7px;
bottom: -7px;
cursor: nwse-resize;
}
.designer-draft {
border-style: dashed;
background: rgba(59, 130, 246, 0.12);
border-color: #60a5fa;
}
.region-list {
display: grid;
gap: 8px;
}
#template-form {
display: grid;
}
#template-form label {
margin-top: 8px;
}
#template-form input,
#template-form textarea,
#template-form select,
#template-form button {
margin-top: 4px;
}
#template-form .card {
padding: 12px;
}
.template-field-card + .template-field-card {
margin-top: 12px;
}
.template-designer-sidebar .row.template-designer-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.template-save-button {
margin-bottom: 0;
}
@media (max-width: 720px) {
.template-designer-sidebar .row.template-designer-actions {
grid-template-columns: 1fr;
}
}
.image-filename {
display: block;
margin-top: 8px;
}
.region-item {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px;
display: grid;
gap: 8px;
}
.region-item.selected {
border-color: #f59e0b;
box-shadow: 0 0 0 1px #f59e0b inset;
}
.region-item .row {
align-items: end;
gap: 6px;
}
.region-item[hidden] {
display: none !important;
}
-245
View File
@@ -1,245 +0,0 @@
.template-designer-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
gap: 12px;
align-items: start;
}
.template-designer-main {
display: grid;
gap: 8px;
}
.template-meta-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
.template-meta-row > label {
flex: 1 1 0;
margin-top: 0;
}
.template-designer-sidebar {
display: grid;
gap: 8px;
}
.template-designer-actions {
display: flex;
flex-wrap: nowrap;
gap: 8px;
margin: 0 0 8px 0;
}
#template-form .template-designer-actions button {
flex: 1 1 0;
width: auto;
margin-top: 0;
}
.region-select {
width: 100%;
margin-bottom: 8px;
}
#canvas-size-select:disabled {
opacity: 0.55;
cursor: not-allowed;
}
#canvas-size-summary.is-locked {
color: #94a3b8;
}
.designer-stage {
position: relative;
width: 100%;
background: #0f172a;
border-radius: 0;
overflow: hidden;
border: 1px solid #1f2937;
cursor: crosshair;
user-select: none;
box-sizing: border-box;
}
.designer-stage img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
pointer-events: none;
}
.designer-stage .empty-bg {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 14px;
pointer-events: none;
}
.designer-overlay {
position: absolute;
inset: 0;
}
.designer-rect {
position: absolute;
border: 2px solid #22c55e;
background: rgba(34, 197, 94, 0.14);
color: #fff;
font-size: 12px;
overflow: hidden;
box-sizing: border-box;
}
.designer-rect.selected {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.18);
}
.designer-rect-label {
position: absolute;
left: 0;
top: 0;
padding: 2px 6px;
background: rgba(15, 23, 42, 0.8);
font-size: 11px;
pointer-events: none;
}
.designer-rect .resize-handle {
position: absolute;
width: 12px;
height: 12px;
background: var(--surface);
border: 1px solid var(--border-strong);
box-sizing: border-box;
}
.designer-rect .resize-handle.nw {
left: -7px;
top: -7px;
cursor: nwse-resize;
}
.designer-rect .resize-handle.ne {
right: -7px;
top: -7px;
cursor: nesw-resize;
}
.designer-rect .resize-handle.sw {
left: -7px;
bottom: -7px;
cursor: nesw-resize;
}
.designer-rect .resize-handle.se {
right: -7px;
bottom: -7px;
cursor: nwse-resize;
}
.designer-draft {
border-style: dashed;
background: rgba(59, 130, 246, 0.12);
border-color: #60a5fa;
}
.region-list {
display: grid;
gap: 8px;
}
#template-form {
display: grid;
}
#template-form label {
margin-top: 8px;
}
#template-form input,
#template-form textarea,
#template-form select,
#template-form button {
margin-top: 4px;
}
#template-form .card {
padding: 12px;
}
.template-field-card + .template-field-card {
margin-top: 12px;
}
.template-designer-sidebar .row.template-designer-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.template-save-button {
margin-bottom: 0;
}
.template-field-head {
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.template-text-style-row {
margin-top: 0;
align-items: flex-end;
}
.template-text-style-row input[type="color"] {
height: 36px;
padding: 4px;
}
@media (max-width: 720px) {
.template-designer-sidebar .row.template-designer-actions {
grid-template-columns: 1fr;
}
}
.image-filename {
display: block;
margin-top: 8px;
}
.region-item {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px;
display: grid;
gap: 8px;
}
.region-item.selected {
border-color: #f59e0b;
box-shadow: 0 0 0 1px #f59e0b inset;
}
.region-item .row {
align-items: end;
gap: 6px;
}
.region-item[hidden] {
display: none !important;
}
+12 -469
View File
@@ -1,5 +1,4 @@
(function () {
var THEME_STORAGE_KEY = 'webui-theme';
function escapeHtml(value) {
return String(value ?? '')
@@ -32,196 +31,6 @@
return String(client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
}
function getTableHeaderText(headerCell) {
return String(headerCell && headerCell.textContent ? headerCell.textContent : '').trim().toLowerCase();
}
function cellHasButtonContent(cell) {
return Boolean(cell && cell.querySelector && cell.querySelector('button, form, input, select, textarea'));
}
function getCellSortValue(cell) {
if (!cell) {
return '';
}
var sortValue = cell.getAttribute && cell.getAttribute('data-sort-value');
if (sortValue !== null && sortValue !== undefined && sortValue !== '') {
return String(sortValue).trim();
}
return String(cell.textContent || '').replace(/\s+/g, ' ').trim();
}
function getComparableSortValue(rawValue) {
var value = String(rawValue || '').trim();
if (!value) {
return { type: 'empty', value: '' };
}
var numericValue = Number(value.replace(/,/g, ''));
if (!Number.isNaN(numericValue) && value !== '') {
return { type: 'number', value: numericValue };
}
var dateValue = Date.parse(value);
if (!Number.isNaN(dateValue)) {
return { type: 'date', value: dateValue };
}
return { type: 'string', value: value.toLowerCase() };
}
function compareSortValues(leftValue, rightValue) {
if (leftValue.type === 'empty' && rightValue.type === 'empty') {
return 0;
}
if (leftValue.type === 'empty') {
return 1;
}
if (rightValue.type === 'empty') {
return -1;
}
if (leftValue.type === rightValue.type) {
if (leftValue.value < rightValue.value) {
return -1;
}
if (leftValue.value > rightValue.value) {
return 1;
}
return 0;
}
return String(leftValue.value).localeCompare(String(rightValue.value), undefined, { numeric: true, sensitivity: 'base' });
}
function getSortableTableState(table) {
if (!table._sortableState) {
table._sortableState = {
columnIndex: null,
direction: 'asc'
};
}
return table._sortableState;
}
function updateSortableHeaderState(table) {
var state = getSortableTableState(table);
var headerCells = table.tHead && table.tHead.rows && table.tHead.rows.length ? Array.prototype.slice.call(table.tHead.rows[0].cells || []) : [];
headerCells.forEach(function (headerCell, index) {
if (!headerCell) {
return;
}
headerCell.classList.remove('sort-asc', 'sort-desc', 'sortable', 'unsortable');
headerCell.removeAttribute('aria-sort');
headerCell.removeAttribute('role');
headerCell.removeAttribute('tabindex');
if (isSortableTableColumn(table, index)) {
headerCell.classList.add('sortable');
headerCell.setAttribute('role', 'button');
headerCell.setAttribute('tabindex', '0');
if (state.columnIndex === index) {
headerCell.classList.add(state.direction === 'desc' ? 'sort-desc' : 'sort-asc');
headerCell.setAttribute('aria-sort', state.direction === 'desc' ? 'descending' : 'ascending');
} else {
headerCell.setAttribute('aria-sort', 'none');
}
} else {
headerCell.classList.add('unsortable');
}
});
}
function isSortableTableColumn(table, columnIndex) {
var headerCell = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0].cells[columnIndex] : null;
if (!headerCell) {
return false;
}
if (/actions?|buttons?/i.test(getTableHeaderText(headerCell))) {
return false;
}
var bodies = table.tBodies ? Array.prototype.slice.call(table.tBodies) : [];
for (var i = 0; i < bodies.length; i += 1) {
var rows = Array.prototype.slice.call(bodies[i].rows || []);
for (var j = 0; j < rows.length; j += 1) {
var cell = rows[j].cells ? rows[j].cells[columnIndex] : null;
if (cell && cellHasButtonContent(cell)) {
return false;
}
}
}
return true;
}
function sortTable(table, columnIndex, direction) {
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
if (!tbody) {
return;
}
var rows = Array.prototype.slice.call(tbody.rows || []);
if (!rows.length) {
return;
}
var multiplier = direction === 'desc' ? -1 : 1;
rows.sort(function (leftRow, rightRow) {
var leftCell = leftRow.cells ? leftRow.cells[columnIndex] : null;
var rightCell = rightRow.cells ? rightRow.cells[columnIndex] : null;
var leftComparable = getComparableSortValue(getCellSortValue(leftCell));
var rightComparable = getComparableSortValue(getCellSortValue(rightCell));
return compareSortValues(leftComparable, rightComparable) * multiplier;
});
rows.forEach(function (row) {
tbody.appendChild(row);
});
}
function applyTableSort(table) {
var state = getSortableTableState(table);
if (state.columnIndex === null || state.columnIndex === undefined) {
return;
}
sortTable(table, state.columnIndex, state.direction);
updateSortableHeaderState(table);
}
function initSortableTables() {
var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
tables.forEach(function (table) {
var headerRow = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0] : null;
if (!headerRow) {
return;
}
Array.prototype.forEach.call(headerRow.cells, function (headerCell, index) {
if (!isSortableTableColumn(table, index)) {
return;
}
headerCell.addEventListener('click', function () {
var state = getSortableTableState(table);
var nextDirection = state.columnIndex === index && state.direction === 'asc' ? 'desc' : 'asc';
state.columnIndex = index;
state.direction = nextDirection;
sortTable(table, index, nextDirection);
updateSortableHeaderState(table);
});
headerCell.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
headerCell.click();
}
});
});
updateSortableHeaderState(table);
});
}
function renderClientActionCell(client) {
var paused = Boolean(client.paused);
var pauseButtonClass = 'button-link list-action' + (paused ? ' is-paused' : '');
@@ -230,7 +39,7 @@
var blackoutButtonClass = 'button-link list-action' + (blackout ? ' is-blackout' : '');
var blackoutButtonLabel = blackout ? 'Restore' : 'Blackout';
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
var blackoutCommandValue = blackout ? '0' : '1';
var blackoutCommandValue = blackout ? 'false' : 'true';
return '<div class="actions"><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action is-danger" data-action="reload">Reload</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action list-action--nav" data-action="previous" aria-label="Previous slide">◀</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action list-action--nav" data-action="next" aria-label="Next slide">▶</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause">' + pauseButtonLabel + '</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout">' + blackoutButtonLabel + '</button></form></div>';
}
@@ -296,7 +105,7 @@
}
var blackoutStateInput = blackoutForm.querySelector('input[name="blackout"]');
if (blackoutStateInput) {
blackoutStateInput.value = blackout ? '0' : '1';
blackoutStateInput.value = blackout ? 'false' : 'true';
}
var blackoutConnectionInput = blackoutForm.querySelector('input[name="connectionId"]');
if (blackoutConnectionInput) {
@@ -458,7 +267,7 @@
tbody.removeChild(tbody.lastElementChild);
}
applyTableSort(document.getElementById('dashboard-clients-table'));
window.applyTableSort(document.getElementById('dashboard-clients-table'));
}
function updateScreenTable(state) {
@@ -475,7 +284,7 @@
return;
}
tbody.innerHTML = state.screens.map(renderScreenRow).join('');
applyTableSort(table);
window.applyTableSort(table);
}
function updateDashboardQuickActions(state) {
@@ -495,7 +304,7 @@
blackoutButton.textContent = label;
blackoutButton.className = 'button-link list-action' + (allBlackout ? ' is-blackout' : '');
if (blackoutInput) {
blackoutInput.value = allBlackout ? '0' : '1';
blackoutInput.value = allBlackout ? 'false' : 'true';
}
if (blackoutForm) {
blackoutForm.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients?' : 'Blackout all connected clients?');
@@ -511,171 +320,9 @@
updateScreenTable(state);
updateClientTable(state);
updateDashboardQuickActions(state);
updateSidebarStatus(Boolean(state.playerServiceConnected), 'Live player feed');
}
function updateSidebarStatus(status, label) {
var dot = document.getElementById('sidebar-status-dot');
var text = document.getElementById('sidebar-status-text');
var pill = document.getElementById('sidebar-status-pill');
if (!dot && !text && !pill) {
return;
}
var normalizedStatus = status === 'online' || status === 'offline' || status === 'unknown'
? status
: (status ? 'online' : 'offline');
var statusLabel = normalizedStatus === 'online'
? (label || 'Connected to player feed')
: normalizedStatus === 'offline'
? 'Disconnected from player feed'
: (label || 'Connecting to player feed');
var pillLabel = normalizedStatus === 'online' ? 'Connected' : normalizedStatus === 'offline' ? 'Disconnected' : 'Connecting';
if (dot) {
dot.classList.remove('status-dot--unknown', 'status-dot--online', 'status-dot--offline');
dot.classList.add(normalizedStatus === 'online' ? 'status-dot--online' : normalizedStatus === 'offline' ? 'status-dot--offline' : 'status-dot--unknown');
dot.setAttribute('aria-label', statusLabel);
dot.setAttribute('title', statusLabel);
}
if (text) {
text.textContent = statusLabel;
}
if (pill) {
pill.classList.remove('status-pill--unknown', 'status-pill--online', 'status-pill--offline');
pill.classList.add(normalizedStatus === 'online' ? 'status-pill--online' : normalizedStatus === 'offline' ? 'status-pill--offline' : 'status-pill--unknown');
pill.textContent = pillLabel;
}
}
function connectDashboardSocket() {
if (!window.WebSocket) {
return;
}
var socket = null;
var reconnectTimer = null;
function scheduleReconnect() {
if (reconnectTimer) {
return;
}
reconnectTimer = window.setTimeout(function () {
reconnectTimer = null;
connect();
}, 5000);
}
function connect() {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
socket = new WebSocket(protocol + '//' + window.location.host + '/ws/admin/dashboard');
updateSidebarStatus('unknown', 'Connecting to player feed');
socket.onmessage = function (event) {
try {
var payload = JSON.parse(String(event.data || '{}'));
if (payload && payload.type === 'dashboard-state') {
handleDashboardState(payload.state);
}
} catch (_error) {
// Ignore malformed dashboard payloads.
}
};
socket.onclose = function () {
updateSidebarStatus('offline');
scheduleReconnect();
};
socket.onerror = function () {
updateSidebarStatus('offline');
try {
socket.close();
} catch (_error) {
// ignore close errors
}
};
socket.onopen = function () {
updateSidebarStatus('unknown', 'Connecting to player feed');
};
}
connect();
}
function dismissToast(toast) {
toast.classList.add('toast-hide');
window.setTimeout(function () {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 220);
}
function showToast(message) {
var text = String(message || '').trim();
if (!text) {
return;
}
var existingToast = document.getElementById('app-toast');
var existingBody = existingToast ? existingToast.querySelector('.toast-body') : null;
if (existingToast && existingBody) {
existingBody.textContent = text;
existingToast.classList.remove('toast-hide');
window.clearTimeout(existingToast._dismissTimer);
existingToast._dismissTimer = window.setTimeout(function () {
dismissToast(existingToast);
}, 4000);
return;
}
var toast = document.createElement('div');
toast.className = 'toast';
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = '<div class="toast-body"></div><button type="button" class="toast-close" aria-label="Dismiss notification">×</button>';
toast.querySelector('.toast-body').textContent = text;
var closeButton = toast.querySelector('.toast-close');
closeButton.addEventListener('click', function () {
dismissToast(toast);
});
document.body.appendChild(toast);
toast._dismissTimer = window.setTimeout(function () {
dismissToast(toast);
}, 4000);
}
function initToast() {
var toast = document.getElementById('app-toast');
var closeButton = document.getElementById('app-toast-close');
if (!toast || !closeButton) {
return;
}
try {
var url = new URL(window.location.href);
if (url.searchParams.has('message')) {
url.searchParams.delete('message');
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
}
} catch (_error) {
// ignore URL cleanup failures
}
closeButton.addEventListener('click', function () {
dismissToast(toast);
});
window.setTimeout(function () {
dismissToast(toast);
}, 4000);
}
window.webuiHandleDashboardState = handleDashboardState;
function initConfirmForms() {
document.addEventListener('submit', function (event) {
@@ -790,6 +437,10 @@
throw new Error(text || 'Unable to save changes.');
});
}
if (form.hasAttribute && form.hasAttribute('data-async-save-reload')) {
window.location.replace(response.url || window.location.href);
return;
}
return response.text().then(function (text) {
var savedMessage = '';
try {
@@ -823,120 +474,12 @@
});
}
function initSidebarToggle() {
var body = document.body;
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-sidebar-toggle]'));
var backdrop = document.querySelector('[data-sidebar-backdrop]');
if (!toggleButtons.length || !backdrop) {
return;
}
function setSidebarOpen(isOpen) {
body.classList.toggle('is-sidebar-open', isOpen);
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
}
function toggleSidebar() {
setSidebarOpen(!body.classList.contains('is-sidebar-open'));
}
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.addEventListener('click', function () {
toggleSidebar();
});
});
backdrop.addEventListener('click', function () {
setSidebarOpen(false);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
setSidebarOpen(false);
}
});
if (window.initSortableTables) {
window.initSortableTables();
}
function getPreferredTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function getStoredTheme() {
try {
var storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
if (storedTheme === 'dark' || storedTheme === 'light') {
return storedTheme;
}
} catch (error) {
return null;
}
return null;
}
function setStoredTheme(theme) {
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch (error) {
return;
}
}
function applyTheme(theme) {
var normalizedTheme = theme === 'dark' ? 'dark' : 'light';
var nextTheme = normalizedTheme === 'dark' ? 'light' : 'dark';
var nextThemeLabel = nextTheme === 'dark' ? 'Dark mode' : 'Light mode';
document.documentElement.dataset.theme = normalizedTheme;
document.documentElement.style.colorScheme = normalizedTheme;
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-toggle]'), function (toggleButton) {
var icon = toggleButton.querySelector('.theme-toggle__icon');
toggleButton.setAttribute('aria-pressed', normalizedTheme === 'dark' ? 'true' : 'false');
toggleButton.setAttribute('aria-label', 'Switch to ' + nextThemeLabel.toLowerCase());
if (icon) {
icon.classList.remove('theme-toggle__icon--moon', 'theme-toggle__icon--sun');
icon.classList.add(normalizedTheme === 'dark' ? 'theme-toggle__icon--sun' : 'theme-toggle__icon--moon');
}
});
return normalizedTheme;
}
function initThemeToggle() {
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-theme-toggle]'));
var storedTheme = getStoredTheme();
var theme = storedTheme || getPreferredTheme();
applyTheme(theme);
if (!toggleButtons.length) {
return;
}
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.addEventListener('click', function () {
var nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
setStoredTheme(nextTheme);
applyTheme(nextTheme);
});
});
}
initToast();
initConfirmForms();
initAsyncCommandForms();
initAsyncSaveForms();
initSubmitOnChange();
initThemeToggle();
initSidebarToggle();
initSortableTables();
if (document.getElementById('sidebar-status-pill') || document.getElementById('sidebar-status-dot') || document.getElementById('sidebar-status-text')) {
connectDashboardSocket();
}
}());
-48
View File
@@ -25,54 +25,6 @@
var editorInstances = new Map();
var submitting = false;
function dismissToast(toast) {
if (!toast) {
return;
}
toast.classList.add('toast-hide');
window.setTimeout(function () {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 220);
}
function showToast(message) {
var text = String(message || '').trim();
if (!text) {
return;
}
var existingToast = document.getElementById('app-toast');
var existingBody = existingToast ? existingToast.querySelector('.toast-body') : null;
if (existingToast && existingBody) {
existingBody.textContent = text;
existingToast.classList.remove('toast-hide');
window.clearTimeout(existingToast._dismissTimer);
existingToast._dismissTimer = window.setTimeout(function () {
dismissToast(existingToast);
}, 4000);
return;
}
var toast = document.createElement('div');
toast.className = 'toast';
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = '<div class="toast-body"></div><button type="button" class="toast-close" aria-label="Dismiss notification">×</button>';
toast.querySelector('.toast-body').textContent = text;
var closeButton = toast.querySelector('.toast-close');
closeButton.addEventListener('click', function () {
dismissToast(toast);
});
document.body.appendChild(toast);
toast._dismissTimer = window.setTimeout(function () {
dismissToast(toast);
}, 4000);
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
+99
View File
@@ -0,0 +1,99 @@
(function () {
function updateSidebarStatus(status, label) {
var dot = document.getElementById('sidebar-status-dot');
var text = document.getElementById('sidebar-status-text');
var pill = document.getElementById('sidebar-status-pill');
if (!dot && !text && !pill) {
return;
}
var normalizedStatus = status === 'online' || status === 'offline' || status === 'unknown'
? status
: (status ? 'online' : 'offline');
var statusLabel = normalizedStatus === 'online'
? (label || 'Connected to player feed')
: normalizedStatus === 'offline'
? 'Disconnected from player feed'
: (label || 'Connecting to player feed');
var pillLabel = normalizedStatus === 'online' ? 'Connected' : normalizedStatus === 'offline' ? 'Disconnected' : 'Connecting';
dot.classList.remove('status-dot--unknown', 'status-dot--online', 'status-dot--offline');
dot.classList.add(normalizedStatus === 'online' ? 'status-dot--online' : normalizedStatus === 'offline' ? 'status-dot--offline' : 'status-dot--unknown');
dot.setAttribute('aria-label', statusLabel);
dot.setAttribute('title', statusLabel);
if (text) {
text.textContent = statusLabel;
}
if (pill) {
pill.classList.remove('status-pill--unknown', 'status-pill--online', 'status-pill--offline');
pill.classList.add(normalizedStatus === 'online' ? 'status-pill--online' : normalizedStatus === 'offline' ? 'status-pill--offline' : 'status-pill--unknown');
pill.textContent = pillLabel;
}
}
function connectDashboardSocket() {
if (!window.WebSocket) {
return;
}
var hasStatusIndicators = document.getElementById('sidebar-status-dot') || document.getElementById('sidebar-status-text') || document.getElementById('sidebar-status-pill');
if (!hasStatusIndicators) {
return;
}
var socket = null;
var reconnectTimer = null;
function scheduleReconnect() {
if (reconnectTimer) {
return;
}
reconnectTimer = window.setTimeout(function () {
reconnectTimer = null;
connect();
}, 5000);
}
function connect() {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
socket = new WebSocket(protocol + '//' + window.location.host + '/ws/admin/dashboard');
updateSidebarStatus('unknown', 'Connecting to player feed');
socket.onmessage = function (event) {
try {
var payload = JSON.parse(String(event.data || '{}'));
if (payload && payload.type === 'dashboard-state' && typeof window.webuiHandleDashboardState === 'function') {
window.webuiHandleDashboardState(payload.state);
updateSidebarStatus(Boolean(payload.state && payload.state.playerServiceConnected), 'Live player feed');
}
} catch (_error) {
// Ignore malformed dashboard payloads.
}
};
socket.onclose = function () {
updateSidebarStatus('offline');
scheduleReconnect();
};
socket.onerror = function () {
updateSidebarStatus('offline');
try {
socket.close();
} catch (_error) {
// ignore close errors
}
};
socket.onopen = function () {
updateSidebarStatus('unknown', 'Connecting to player feed');
};
}
connect();
}
connectDashboardSocket();
}());
+194
View File
@@ -0,0 +1,194 @@
(function () {
function getTableHeaderText(headerCell) {
return String(headerCell && headerCell.textContent ? headerCell.textContent : '').trim().toLowerCase();
}
function cellHasButtonContent(cell) {
return Boolean(cell && cell.querySelector && cell.querySelector('button, form, input, select, textarea'));
}
function getCellSortValue(cell) {
if (!cell) {
return '';
}
var sortValue = cell.getAttribute && cell.getAttribute('data-sort-value');
if (sortValue !== null && sortValue !== undefined && sortValue !== '') {
return String(sortValue).trim();
}
return String(cell.textContent || '').replace(/\s+/g, ' ').trim();
}
function getComparableSortValue(rawValue) {
var value = String(rawValue || '').trim();
if (!value) {
return { type: 'empty', value: '' };
}
var numericValue = Number(value.replace(/,/g, ''));
if (!Number.isNaN(numericValue) && value !== '') {
return { type: 'number', value: numericValue };
}
var dateValue = Date.parse(value);
if (!Number.isNaN(dateValue)) {
return { type: 'date', value: dateValue };
}
return { type: 'string', value: value.toLowerCase() };
}
function compareSortValues(leftValue, rightValue) {
if (leftValue.type === 'empty' && rightValue.type === 'empty') {
return 0;
}
if (leftValue.type === 'empty') {
return 1;
}
if (rightValue.type === 'empty') {
return -1;
}
if (leftValue.type === rightValue.type) {
if (leftValue.value < rightValue.value) {
return -1;
}
if (leftValue.value > rightValue.value) {
return 1;
}
return 0;
}
return String(leftValue.value).localeCompare(String(rightValue.value), undefined, { numeric: true, sensitivity: 'base' });
}
function getSortableTableState(table) {
if (!table._sortableState) {
table._sortableState = {
columnIndex: null,
direction: 'asc'
};
}
return table._sortableState;
}
function isSortableTableColumn(table, columnIndex) {
var headerCell = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0].cells[columnIndex] : null;
if (!headerCell) {
return false;
}
if (/actions?|buttons?/i.test(getTableHeaderText(headerCell))) {
return false;
}
var bodies = table.tBodies ? Array.prototype.slice.call(table.tBodies) : [];
for (var i = 0; i < bodies.length; i += 1) {
var rows = Array.prototype.slice.call(bodies[i].rows || []);
for (var j = 0; j < rows.length; j += 1) {
var cell = rows[j].cells ? rows[j].cells[columnIndex] : null;
if (cell && cellHasButtonContent(cell)) {
return false;
}
}
}
return true;
}
function updateSortableHeaderState(table) {
var state = getSortableTableState(table);
var headerCells = table.tHead && table.tHead.rows && table.tHead.rows.length ? Array.prototype.slice.call(table.tHead.rows[0].cells || []) : [];
headerCells.forEach(function (headerCell, index) {
if (!headerCell) {
return;
}
headerCell.classList.remove('sort-asc', 'sort-desc', 'sortable', 'unsortable');
headerCell.removeAttribute('aria-sort');
headerCell.removeAttribute('role');
headerCell.removeAttribute('tabindex');
if (isSortableTableColumn(table, index)) {
headerCell.classList.add('sortable');
headerCell.setAttribute('role', 'button');
headerCell.setAttribute('tabindex', '0');
if (state.columnIndex === index) {
headerCell.classList.add(state.direction === 'desc' ? 'sort-desc' : 'sort-asc');
headerCell.setAttribute('aria-sort', state.direction === 'desc' ? 'descending' : 'ascending');
} else {
headerCell.setAttribute('aria-sort', 'none');
}
} else {
headerCell.classList.add('unsortable');
}
});
}
function sortTable(table, columnIndex, direction) {
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
if (!tbody) {
return;
}
var rows = Array.prototype.slice.call(tbody.rows || []);
if (!rows.length) {
return;
}
var multiplier = direction === 'desc' ? -1 : 1;
rows.sort(function (leftRow, rightRow) {
var leftCell = leftRow.cells ? leftRow.cells[columnIndex] : null;
var rightCell = rightRow.cells ? rightRow.cells[columnIndex] : null;
var leftComparable = getComparableSortValue(getCellSortValue(leftCell));
var rightComparable = getComparableSortValue(getCellSortValue(rightCell));
return compareSortValues(leftComparable, rightComparable) * multiplier;
});
rows.forEach(function (row) {
tbody.appendChild(row);
});
}
function applyTableSort(table) {
var state = getSortableTableState(table);
if (state.columnIndex === null || state.columnIndex === undefined) {
return;
}
sortTable(table, state.columnIndex, state.direction);
updateSortableHeaderState(table);
}
function initSortableTables() {
var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
tables.forEach(function (table) {
var headerRow = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0] : null;
if (!headerRow) {
return;
}
Array.prototype.forEach.call(headerRow.cells, function (headerCell, index) {
if (!isSortableTableColumn(table, index)) {
return;
}
headerCell.addEventListener('click', function () {
var state = getSortableTableState(table);
var nextDirection = state.columnIndex === index && state.direction === 'asc' ? 'desc' : 'asc';
state.columnIndex = index;
state.direction = nextDirection;
sortTable(table, index, nextDirection);
updateSortableHeaderState(table);
});
headerCell.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
headerCell.click();
}
});
});
updateSortableHeaderState(table);
});
}
window.applyTableSort = applyTableSort;
window.initSortableTables = initSortableTables;
}());
+20
View File
@@ -0,0 +1,20 @@
(function () {
var storageKey = 'webui-theme';
var theme = 'light';
try {
var storedTheme = window.localStorage.getItem(storageKey);
if (storedTheme === 'dark' || storedTheme === 'light') {
theme = storedTheme;
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
} catch (error) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
}
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
}());
+118
View File
@@ -0,0 +1,118 @@
(function () {
var THEME_STORAGE_KEY = 'webui-theme';
function getPreferredTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function getStoredTheme() {
try {
var storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
if (storedTheme === 'dark' || storedTheme === 'light') {
return storedTheme;
}
} catch (error) {
return null;
}
return null;
}
function setStoredTheme(theme) {
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch (error) {
return;
}
}
function applyTheme(theme) {
var normalizedTheme = theme === 'dark' ? 'dark' : 'light';
var nextTheme = normalizedTheme === 'dark' ? 'light' : 'dark';
var nextThemeLabel = nextTheme === 'dark' ? 'Dark mode' : 'Light mode';
document.documentElement.dataset.theme = normalizedTheme;
document.documentElement.style.colorScheme = normalizedTheme;
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-toggle]'), function (toggleButton) {
var icon = toggleButton.querySelector('.theme-toggle__icon');
toggleButton.setAttribute('aria-pressed', normalizedTheme === 'dark' ? 'true' : 'false');
toggleButton.setAttribute('aria-label', 'Switch to ' + nextThemeLabel.toLowerCase());
if (icon) {
icon.classList.remove('theme-toggle__icon--moon', 'theme-toggle__icon--sun');
icon.classList.add(normalizedTheme === 'dark' ? 'theme-toggle__icon--sun' : 'theme-toggle__icon--moon');
}
});
return normalizedTheme;
}
function initThemeToggle() {
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-theme-toggle]'));
var storedTheme = getStoredTheme();
var theme = storedTheme || getPreferredTheme();
applyTheme(theme);
if (!toggleButtons.length) {
return;
}
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.addEventListener('click', function () {
var nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
setStoredTheme(nextTheme);
applyTheme(nextTheme);
});
});
}
function initSidebarToggle() {
var body = document.body;
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-sidebar-toggle]'));
var backdrop = document.querySelector('[data-sidebar-backdrop]');
if (!toggleButtons.length || !backdrop) {
return;
}
function setSidebarOpen(isOpen) {
body.classList.toggle('is-sidebar-open', isOpen);
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
}
function toggleSidebar() {
setSidebarOpen(!body.classList.contains('is-sidebar-open'));
}
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
toggleButton.addEventListener('click', function () {
toggleSidebar();
});
});
backdrop.addEventListener('click', function () {
setSidebarOpen(false);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
setSidebarOpen(false);
}
});
}
window.getPreferredTheme = getPreferredTheme;
window.getStoredTheme = getStoredTheme;
window.setStoredTheme = setStoredTheme;
window.applyTheme = applyTheme;
window.initThemeToggle = initThemeToggle;
window.initSidebarToggle = initSidebarToggle;
initThemeToggle();
initSidebarToggle();
}());
+82
View File
@@ -0,0 +1,82 @@
(function () {
function dismissToast(toast) {
if (!toast) {
return;
}
toast.classList.add('toast-hide');
window.setTimeout(function () {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 220);
}
function showToast(message) {
var text = String(message || '').trim();
if (!text) {
return;
}
var existingToast = document.getElementById('app-toast');
var existingBody = existingToast ? existingToast.querySelector('.toast-body') : null;
if (existingToast && existingBody) {
existingBody.textContent = text;
existingToast.classList.remove('toast-hide');
window.clearTimeout(existingToast._dismissTimer);
existingToast._dismissTimer = window.setTimeout(function () {
dismissToast(existingToast);
}, 4000);
return;
}
var toast = document.createElement('div');
toast.className = 'toast';
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = '<div class="toast-body"></div><button type="button" class="toast-close" aria-label="Dismiss notification">×</button>';
toast.querySelector('.toast-body').textContent = text;
var closeButton = toast.querySelector('.toast-close');
closeButton.addEventListener('click', function () {
dismissToast(toast);
});
document.body.appendChild(toast);
toast._dismissTimer = window.setTimeout(function () {
dismissToast(toast);
}, 4000);
}
function initToast() {
var toast = document.getElementById('app-toast');
var closeButton = document.getElementById('app-toast-close');
if (!toast || !closeButton) {
return;
}
try {
var url = new URL(window.location.href);
if (url.searchParams.has('message')) {
url.searchParams.delete('message');
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
}
} catch (_error) {
// ignore URL cleanup failures
}
closeButton.addEventListener('click', function () {
dismissToast(toast);
});
window.setTimeout(function () {
dismissToast(toast);
}, 4000);
}
window.dismissToast = dismissToast;
window.showToast = showToast;
window.initToast = initToast;
initToast();
}());
-1
View File
@@ -110,7 +110,6 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
selectableSlides: selectableSlides,
playlistCanvasSignature: playlistCanvasSignature,
playlistCanvasMismatch: playlistCanvasMismatch,
stylesheets: ['css/admin-editor.css'],
scripts: ['js/playlist-schedule.js']
});
};
@@ -59,7 +59,6 @@ module.exports = function renderPlaylistSlideConfigPage(playlist, playlistSlide,
...day,
checked: selectedDays.has(day.value)
})),
stylesheets: ['css/admin-editor.css'],
scripts: ['js/playlist-schedule.js']
});
};
+1 -2
View File
@@ -21,7 +21,6 @@ module.exports = function renderSlideFormPage(data, mode, slide, message, curren
existingTemplateId: slide && slide.template_id ? slide.template_id : null,
existingContent: slide && slide.content ? slide.content : {}
},
currentUser: currentUser || null,
stylesheets: ['css/admin-editor.css']
currentUser: currentUser || null
});
};
-1
View File
@@ -59,7 +59,6 @@ module.exports = function renderTemplateFormPage(template, mode, message, canvas
currentUser: currentUser || null,
template: current,
canvasSizes: canvasSizes || [],
stylesheets: ['css/template-designer.css'],
scripts: ['js/template-designer.js']
});
};
+2 -2
View File
@@ -9,7 +9,7 @@
<section class="stat"><strong id="dashboard-playlist-count">{{playlists.length}}</strong><span>Playlists</span></section>
<section class="stat"><strong id="dashboard-slide-count">{{slides.length}}</strong><span>Slides</span></section>
<section class="stat"><strong id="dashboard-screen-count">{{screens.length}}</strong><span>Screens</span></section>
<section class="stat"><strong id="dashboard-client-count">{{connectedClientsCount}}</strong><span>Connected clients</span></section>
<section class="stat"><strong id="dashboard-client-count">{{connectedClientsCount}}</strong><span>Clients</span></section>
</div>
<div class="card">
@@ -21,7 +21,7 @@
</form>
<form method="post" action="/admin/commands" class="inline-form" data-confirm-message="Blackout all connected clients?" data-async-command>
<input type="hidden" name="command" value="blackout" />
<input type="hidden" name="blackout" value="1" />
<input type="hidden" name="blackout" value="true" />
<button type="submit" class="button-link list-action" id="dashboard-blackout-all-button">Blackout all clients</button>
</form>
</div>
+3 -21
View File
@@ -4,27 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{title}} - Signage</title>
<script>
(function () {
var storageKey = 'webui-theme';
var theme = 'light';
try {
var storedTheme = window.localStorage.getItem(storageKey);
if (storedTheme === 'dark' || storedTheme === 'light') {
theme = storedTheme;
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
} catch (error) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
}
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
}());
</script>
<script src="/assets/js/theme-init.js"></script>
<link rel="stylesheet" href="/assets/css/admin.css" />
{{#if stylesheets.length}}
{{#each stylesheets}}
@@ -55,6 +35,8 @@
</main>
</div>
</div>
<script src="/assets/js/theme-sidebar.js"></script>
<script src="/assets/js/toast.js"></script>
<script src="/assets/js/admin-page.js"></script>
{{#if scripts.length}}
{{#each scripts}}
+5 -21
View File
@@ -4,27 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>{{title}} - Signage</title>
<script>
(function () {
var storageKey = 'webui-theme';
var theme = 'light';
try {
var storedTheme = window.localStorage.getItem(storageKey);
if (storedTheme === 'dark' || storedTheme === 'light') {
theme = storedTheme;
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
} catch (error) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
}
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
}());
</script>
<script src="/assets/js/theme-init.js"></script>
<link rel="stylesheet" href="/assets/css/admin.css" />
{{#if stylesheets.length}}
{{#each stylesheets}}
@@ -116,7 +96,11 @@
</main>
</div>
</div>
<script src="/assets/js/theme-sidebar.js"></script>
<script src="/assets/js/toast.js"></script>
<script src="/assets/js/table-sort.js"></script>
<script src="/assets/js/admin-page.js"></script>
<script src="/assets/js/system-status.js"></script>
{{/if}}
{{#if scripts.length}}
{{#each scripts}}
+1 -1
View File
@@ -6,7 +6,7 @@
<a class="button-link" href="/admin/playlists">Back to playlists</a>
</div>
<form id="playlist-edit-form" method="post" action="/admin/playlists/{{playlist.id}}" data-async-save>
<form id="playlist-edit-form" method="post" action="/admin/playlists/{{playlist.id}}" data-async-save data-async-save-reload>
<div class="card playlist-name-card">
<div class="row playlist-edit-header">
<label class="playlist-name-field">Name