feat: Enhance layout designer with skeleton loading screen and keyboard shortcut hints
This commit is contained in:
@@ -8625,3 +8625,593 @@ select.form-control,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* ============================================================================
|
||||
LAYOUT DESIGNER — UX ENHANCEMENTS
|
||||
Phase 4: Skeleton loading screen + canvas dot-grid
|
||||
============================================================================ */
|
||||
|
||||
/* --- Skeleton loading screen --------------------------------------------- */
|
||||
|
||||
#ots-editor-skeleton {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--editor-body-bg);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
#ots-editor-skeleton.ots-skeleton-done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Top bar strip */
|
||||
.ots-skeleton-topbar {
|
||||
height: 46px;
|
||||
background: var(--editor-modal-header-bg);
|
||||
border-bottom: 1px solid var(--editor-border);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ots-skeleton-topbar-left,
|
||||
.ots-skeleton-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Body row */
|
||||
.ots-skeleton-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Left icon rail */
|
||||
.ots-skeleton-left-rail {
|
||||
width: 52px;
|
||||
background: var(--editor-toolbar-bg);
|
||||
border-right: 1px solid var(--editor-border);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.ots-skeleton-icon-dot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Canvas area with dot-grid */
|
||||
.ots-skeleton-canvas-area {
|
||||
flex: 1;
|
||||
background: var(--editor-body-bg);
|
||||
background-image: radial-gradient(circle, var(--editor-border) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* The layout preview placeholder box */
|
||||
.ots-skeleton-canvas-box {
|
||||
width: 62%;
|
||||
max-width: 680px;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--editor-widget-bg);
|
||||
border: 1px solid var(--editor-border);
|
||||
border-radius: 4px;
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Properties panel */
|
||||
.ots-skeleton-props {
|
||||
width: 272px;
|
||||
min-width: 272px;
|
||||
background: var(--editor-properties-bg);
|
||||
border-left: 1px solid var(--editor-border);
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ots-skeleton-prop-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.ots-skeleton-prop-label {
|
||||
height: 10px;
|
||||
width: 55%;
|
||||
background: var(--editor-border);
|
||||
border-radius: 3px;
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ots-skeleton-prop-field {
|
||||
height: 34px;
|
||||
background: var(--editor-border);
|
||||
border-radius: 5px;
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite 0.08s;
|
||||
}
|
||||
|
||||
.ots-skeleton-prop-swatch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ots-skeleton-prop-swatch {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
background: var(--editor-border);
|
||||
border-radius: 5px;
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Bottom bar */
|
||||
.ots-skeleton-bottombar {
|
||||
height: 44px;
|
||||
background: var(--editor-footer-controls-bg);
|
||||
border-top: 1px solid var(--editor-border);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
/* Inline chip (for topbar breadcrumb placeholders) */
|
||||
.ots-skeleton-chip {
|
||||
height: 16px;
|
||||
width: 100px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
border-radius: 4px;
|
||||
animation: ots-shimmer 1.7s ease-in-out infinite 0.12s;
|
||||
}
|
||||
|
||||
/* Shimmer keyframe */
|
||||
@keyframes ots-shimmer {
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 0.70; }
|
||||
}
|
||||
|
||||
/* --- Canvas dead-space dot-grid pattern ---------------------------------- */
|
||||
|
||||
/* Applied to the area that surrounds the actual layout preview viewport */
|
||||
#layout-editor #layout-viewer,
|
||||
#layout-editor .main-panel-wrapper {
|
||||
background-image: radial-gradient(circle, var(--editor-border) 1px, transparent 1px) !important;
|
||||
background-size: 24px 24px !important;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================================
|
||||
LAYOUT DESIGNER — UX ENHANCEMENTS
|
||||
Phase 5: Resize handles, add-region button, micro-transitions
|
||||
============================================================================ */
|
||||
|
||||
/* --- Region resize handles (jQuery UI) ----------------------------------- */
|
||||
|
||||
#layout-editor .designer-region .ui-resizable-handle,
|
||||
#layout-editor .selected .ui-resizable-handle {
|
||||
background-color: var(--color-primary) !important;
|
||||
border-radius: 2px !important;
|
||||
opacity: 0.45;
|
||||
transition: opacity 0.15s ease !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
#layout-editor .designer-region:hover .ui-resizable-handle,
|
||||
#layout-editor .designer-region.selected .ui-resizable-handle {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
#layout-editor .ui-resizable-se { cursor: se-resize !important; }
|
||||
#layout-editor .ui-resizable-sw { cursor: sw-resize !important; }
|
||||
#layout-editor .ui-resizable-ne { cursor: ne-resize !important; }
|
||||
#layout-editor .ui-resizable-nw { cursor: nw-resize !important; }
|
||||
#layout-editor .ui-resizable-n,
|
||||
#layout-editor .ui-resizable-s { cursor: n-resize !important; }
|
||||
#layout-editor .ui-resizable-e,
|
||||
#layout-editor .ui-resizable-w { cursor: e-resize !important; }
|
||||
|
||||
/* Corner handles: slightly larger for easier grab */
|
||||
#layout-editor .ui-resizable-se,
|
||||
#layout-editor .ui-resizable-sw,
|
||||
#layout-editor .ui-resizable-ne,
|
||||
#layout-editor .ui-resizable-nw {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
/* --- Canvas "+" add-region button ---------------------------------------- */
|
||||
|
||||
/* Xibo renders a prominent add button at the top of the viewer panel */
|
||||
#layout-editor .add-btn,
|
||||
#layout-editor .viewer-add-btn,
|
||||
#layout-editor .button-add-new,
|
||||
#layout-editor [class*="add-region-btn"] {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
border-radius: 50% !important;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
min-width: 36px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 3px 10px rgba(var(--ots-primary-rgb), 0.45) !important;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#layout-editor .add-btn:hover,
|
||||
#layout-editor .viewer-add-btn:hover,
|
||||
#layout-editor .button-add-new:hover,
|
||||
#layout-editor [class*="add-region-btn"]:hover {
|
||||
transform: scale(1.12) !important;
|
||||
box-shadow: 0 5px 16px rgba(var(--ots-primary-rgb), 0.65) !important;
|
||||
}
|
||||
|
||||
/* --- Micro-transitions for key interactive elements ---------------------- */
|
||||
|
||||
/* Timeline widget cards */
|
||||
#playlist-timeline .playlist-widget {
|
||||
transition: background-color 0.14s ease, outline-color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* Toolbar card rows */
|
||||
.editor-toolbar .toolbar-card,
|
||||
.editor-toolbar nav tr.toolbar-card {
|
||||
transition: background-color 0.14s ease, color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* Toolbar nav buttons */
|
||||
.editor-toolbar nav .toolbar-menu-btn {
|
||||
transition: background-color 0.14s ease, color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* Properties panel nav tabs */
|
||||
.properties-panel-container .nav-link,
|
||||
#properties-panel .nav .nav-link {
|
||||
transition: color 0.14s ease, border-bottom-color 0.14s ease,
|
||||
background-color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* Bottom-bar buttons */
|
||||
.editor-bottom-bar nav .btn,
|
||||
.editor-bottom-bar .viewer-navbar-info .btn {
|
||||
transition: color 0.14s ease, background-color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* Context menu items */
|
||||
.context-menu-btn {
|
||||
transition: background-color 0.12s ease, color 0.12s ease !important;
|
||||
}
|
||||
|
||||
/* Properties panel action buttons */
|
||||
#properties-panel .btn {
|
||||
transition: background-color 0.14s ease, color 0.14s ease,
|
||||
border-color 0.14s ease, box-shadow 0.14s ease !important;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================================
|
||||
LAYOUT DESIGNER — UX ENHANCEMENTS
|
||||
Phase 6: Custom scrollbars, focus rings, empty timeline state
|
||||
============================================================================ */
|
||||
|
||||
/* --- Custom scrollbars (all editor scrollable areas) --------------------- */
|
||||
|
||||
/* Firefox */
|
||||
#properties-panel,
|
||||
#properties-panel-form-container,
|
||||
.editor-toolbar .toolbar-pane,
|
||||
.toolbar-menu-content,
|
||||
#playlist-editor,
|
||||
#layout-editor {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--editor-scrollbar-thumb) var(--editor-scrollbar-track);
|
||||
}
|
||||
|
||||
/* WebKit */
|
||||
#properties-panel ::-webkit-scrollbar,
|
||||
#properties-panel-form-container ::-webkit-scrollbar,
|
||||
.editor-toolbar .toolbar-pane ::-webkit-scrollbar,
|
||||
.toolbar-menu-content ::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#properties-panel ::-webkit-scrollbar-track,
|
||||
#properties-panel-form-container ::-webkit-scrollbar-track,
|
||||
.editor-toolbar .toolbar-pane ::-webkit-scrollbar-track,
|
||||
.toolbar-menu-content ::-webkit-scrollbar-track {
|
||||
background: var(--editor-scrollbar-track);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#properties-panel ::-webkit-scrollbar-thumb,
|
||||
#properties-panel-form-container ::-webkit-scrollbar-thumb,
|
||||
.editor-toolbar .toolbar-pane ::-webkit-scrollbar-thumb,
|
||||
.toolbar-menu-content ::-webkit-scrollbar-thumb {
|
||||
background: var(--editor-scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#properties-panel ::-webkit-scrollbar-thumb:hover,
|
||||
#properties-panel-form-container ::-webkit-scrollbar-thumb:hover,
|
||||
.editor-toolbar .toolbar-pane ::-webkit-scrollbar-thumb:hover,
|
||||
.toolbar-menu-content ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--editor-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* --- Accessible focus ring indicators ------------------------------------ */
|
||||
|
||||
#layout-editor *:focus-visible,
|
||||
#playlist-editor *:focus-visible,
|
||||
.editor-modal *:focus-visible,
|
||||
#properties-panel *:focus-visible {
|
||||
outline: 2px solid var(--color-primary) !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 4px rgba(var(--ots-primary-rgb), 0.15) !important;
|
||||
}
|
||||
|
||||
/* --- Empty timeline state (no regions yet) -------------------------------- */
|
||||
|
||||
/* Xibo renders an empty-state message inside the timeline container */
|
||||
#playlist-timeline .empty-timeline,
|
||||
#playlist-timeline .timeline-empty-message,
|
||||
#playlist-timeline .empty-message,
|
||||
#layout-editor .timeline-no-content,
|
||||
#layout-editor .empty-canvas-hint {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-height: 72px !important;
|
||||
color: var(--editor-text-secondary) !important;
|
||||
font-size: 13px !important;
|
||||
text-align: center !important;
|
||||
padding: 12px 20px !important;
|
||||
border: 2px dashed var(--editor-border) !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 10px !important;
|
||||
cursor: pointer !important;
|
||||
transition: border-color 0.15s ease, color 0.15s ease !important;
|
||||
}
|
||||
|
||||
#playlist-timeline .empty-timeline:hover,
|
||||
#playlist-timeline .timeline-empty-message:hover,
|
||||
#playlist-timeline .empty-message:hover,
|
||||
#layout-editor .timeline-no-content:hover,
|
||||
#layout-editor .empty-canvas-hint:hover {
|
||||
border-color: var(--color-primary) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================================
|
||||
LAYOUT DESIGNER — UX ENHANCEMENTS
|
||||
Phase 7: Interactive Mode toggle, bottom bar refinement
|
||||
============================================================================ */
|
||||
|
||||
/* --- Interactive Mode toggle button -------------------------------------- */
|
||||
|
||||
/* Xibo renders this as a button in .editor-top-bar; it shows
|
||||
"Interactive Mode OFF/ON". We can't know the exact dynamic class but we can
|
||||
style all top-bar outline/light buttons that aren't the back-button. */
|
||||
|
||||
body.editor-opened .editor-top-bar .btn:not(.back-button):not(.btn-primary) {
|
||||
border-radius: 6px !important;
|
||||
font-size: 12px !important;
|
||||
transition: background-color 0.14s ease, border-color 0.14s ease,
|
||||
color 0.14s ease !important;
|
||||
}
|
||||
|
||||
/* When toggled "ON" — Xibo adds .active or .btn-success */
|
||||
body.editor-opened .editor-top-bar .btn.active:not(.back-button),
|
||||
body.editor-opened .editor-top-bar .btn-success:not(.back-button) {
|
||||
background-color: var(--color-primary) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* The layout name / info text */
|
||||
body.editor-opened .editor-top-bar .layout-info,
|
||||
body.editor-opened .editor-top-bar .navbar-text {
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
color: var(--editor-text) !important;
|
||||
letter-spacing: 0.01em !important;
|
||||
}
|
||||
|
||||
/* --- Bottom bar refinement ----------------------------------------------- */
|
||||
|
||||
/* Slim down the orange info strip slightly and add a top border accent */
|
||||
.editor-bottom-bar .viewer-navbar-info {
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0.015em !important;
|
||||
border-top: 2px solid rgba(255,255,255,0.15) !important;
|
||||
}
|
||||
|
||||
/* Slightly tighter padding on bottom-bar nav */
|
||||
.editor-bottom-bar nav {
|
||||
min-height: 40px !important;
|
||||
}
|
||||
|
||||
.editor-bottom-bar nav .btn {
|
||||
font-size: 12px !important;
|
||||
padding: 4px 10px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* Active/selected button in the bottom bar */
|
||||
.editor-bottom-bar nav .btn.active,
|
||||
.editor-bottom-bar nav .btn:focus {
|
||||
background-color: rgba(var(--ots-primary-rgb), 0.12) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
/* Preview "play" button accent */
|
||||
.editor-bottom-bar .btn-play-preview,
|
||||
.editor-bottom-bar [class*="play-preview"] {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
border-radius: 50% !important;
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease !important;
|
||||
}
|
||||
|
||||
.editor-bottom-bar .btn-play-preview:hover,
|
||||
.editor-bottom-bar [class*="play-preview"]:hover {
|
||||
transform: scale(1.1) !important;
|
||||
box-shadow: 0 3px 10px rgba(var(--ots-primary-rgb), 0.5) !important;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================================
|
||||
LAYOUT DESIGNER — UX ENHANCEMENTS
|
||||
Phase 8: Compact embed mode + light-mode propagation fix
|
||||
============================================================================ */
|
||||
|
||||
/* --- Compact mode for narrow embed viewports ----------------------------- */
|
||||
|
||||
/* At 1280px the properties panel starts crowding the canvas */
|
||||
@media (max-width: 1280px) {
|
||||
.ots-embed-mode #properties-panel,
|
||||
.ots-embed-mode .properties-panel-container {
|
||||
min-width: 230px !important;
|
||||
max-width: 230px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.ots-embed-mode #properties-panel,
|
||||
.ots-embed-mode .properties-panel-container {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.ots-embed-mode #properties-panel .form-control,
|
||||
.ots-embed-mode .properties-panel-container .form-control {
|
||||
height: 30px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.ots-embed-mode #properties-panel label,
|
||||
.ots-embed-mode .properties-panel-container label {
|
||||
font-size: 11px !important;
|
||||
margin-bottom: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Light-mode propagation: body.ots-light-mode editor tokens ----------- */
|
||||
|
||||
/* The theme applies ots-light-mode to both <html> and <body>.
|
||||
CSS custom properties cascade from html.ots-light-mode already, but
|
||||
we reinforce key structural backgrounds here to beat Xibo's bundled CSS. */
|
||||
|
||||
html.ots-light-mode #layout-editor,
|
||||
html.ots-light-mode #playlist-editor,
|
||||
html.ots-light-mode .layout-editor,
|
||||
html.ots-light-mode .playlist-editor {
|
||||
background-color: var(--editor-body-bg) !important;
|
||||
color: var(--editor-text) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #layout-editor #layout-viewer,
|
||||
html.ots-light-mode #layout-editor .main-panel-wrapper {
|
||||
background-color: var(--editor-left-margin-bg) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode .editor-toolbar,
|
||||
html.ots-light-mode .editor-toolbar nav {
|
||||
background-color: var(--editor-toolbar-bg) !important;
|
||||
color: var(--editor-toolbar-text) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode .editor-top-bar nav,
|
||||
html.ots-light-mode body.editor-opened .header-side {
|
||||
background-color: var(--editor-modal-header-bg) !important;
|
||||
color: var(--editor-text) !important;
|
||||
border-bottom: 1px solid var(--editor-border) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode .editor-bottom-bar nav {
|
||||
background-color: var(--editor-modal-content-bg) !important;
|
||||
color: var(--editor-text) !important;
|
||||
border-top: 1px solid var(--editor-border) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #playlist-editor .left-margin {
|
||||
background-color: var(--editor-left-margin-bg) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #playlist-editor .time-grid {
|
||||
color: var(--editor-timegrid-text) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #playlist-editor .time-grid .time-grid-step {
|
||||
background-color: var(--editor-timegrid-line) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #playlist-timeline {
|
||||
background-color: var(--editor-timeline-bg) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode #playlist-timeline .playlist-widget {
|
||||
background-color: var(--editor-widget-bg) !important;
|
||||
color: var(--editor-widget-text) !important;
|
||||
outline-color: var(--editor-widget-border) !important;
|
||||
}
|
||||
|
||||
html.ots-light-mode .properties-panel-container,
|
||||
html.ots-light-mode #properties-panel,
|
||||
html.ots-light-mode #properties-panel-form-container {
|
||||
background-color: var(--editor-properties-bg) !important;
|
||||
color: var(--editor-text) !important;
|
||||
}
|
||||
|
||||
/* Panel header tabs stay distinguishable in light mode */
|
||||
html.ots-light-mode #properties-panel .nav,
|
||||
html.ots-light-mode #properties-panel .nav-tabs {
|
||||
background-color: var(--editor-header-bg) !important;
|
||||
border-bottom-color: var(--editor-border) !important;
|
||||
}
|
||||
|
||||
/* Skeleton adapts to light mode too (variables are already resolved) */
|
||||
html.ots-light-mode .ots-skeleton-icon-dot {
|
||||
background: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
html.ots-light-mode .ots-skeleton-chip {
|
||||
background: rgba(0,0,0,0.07);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,8 +109,52 @@
|
||||
<!-- Editor structure -->
|
||||
<div id="layout-editor" data-published-layout-id="{{ publishedLayoutId }}" data-layout-id="{{ layout.layoutId }}" data-layout-help={{ help }}></div>
|
||||
|
||||
<div class="loading-overlay">
|
||||
<i class="fa fa-spinner fa-spin loading-icon"></i>
|
||||
{# Skeleton loading screen — replaced by editor once editor-opened class fires on body #}
|
||||
<div id="ots-editor-skeleton" aria-hidden="true">
|
||||
<div class="ots-skeleton-topbar">
|
||||
<div class="ots-skeleton-topbar-left">
|
||||
<div class="ots-skeleton-chip"></div>
|
||||
<div class="ots-skeleton-chip" style="width:80px"></div>
|
||||
</div>
|
||||
<div class="ots-skeleton-topbar-right">
|
||||
<div class="ots-skeleton-chip" style="width:150px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ots-skeleton-body">
|
||||
<div class="ots-skeleton-left-rail">
|
||||
<div class="ots-skeleton-icon-dot"></div>
|
||||
<div class="ots-skeleton-icon-dot"></div>
|
||||
<div class="ots-skeleton-icon-dot"></div>
|
||||
<div class="ots-skeleton-icon-dot"></div>
|
||||
</div>
|
||||
<div class="ots-skeleton-canvas-area">
|
||||
<div class="ots-skeleton-canvas-box"></div>
|
||||
</div>
|
||||
<div class="ots-skeleton-props">
|
||||
<div class="ots-skeleton-prop-group">
|
||||
<div class="ots-skeleton-prop-label"></div>
|
||||
<div class="ots-skeleton-prop-swatch-row">
|
||||
<div class="ots-skeleton-prop-swatch"></div>
|
||||
<div class="ots-skeleton-prop-field"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ots-skeleton-prop-group">
|
||||
<div class="ots-skeleton-prop-label"></div>
|
||||
<div class="ots-skeleton-prop-field" style="height:70px"></div>
|
||||
</div>
|
||||
<div class="ots-skeleton-prop-group">
|
||||
<div class="ots-skeleton-prop-label"></div>
|
||||
<div class="ots-skeleton-prop-field"></div>
|
||||
</div>
|
||||
<div class="ots-skeleton-prop-group">
|
||||
<div class="ots-skeleton-prop-label" style="width:35%"></div>
|
||||
<div class="ots-skeleton-prop-field"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ots-skeleton-bottombar">
|
||||
<div class="ots-skeleton-chip" style="width:120px; opacity:.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -458,6 +502,57 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{# ── Skeleton dismiss + keyboard shortcut hints ──────────── #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ── Skeleton dismiss ─────────────────────────────────────
|
||||
var skeleton = document.getElementById('ots-editor-skeleton');
|
||||
if (skeleton) {
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
skeleton.parentNode.removeChild(skeleton);
|
||||
} else {
|
||||
var skeletonObs = new MutationObserver(function() {
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
skeleton.classList.add('ots-skeleton-done');
|
||||
setTimeout(function() {
|
||||
if (skeleton.parentNode) skeleton.parentNode.removeChild(skeleton);
|
||||
}, 380);
|
||||
skeletonObs.disconnect();
|
||||
}
|
||||
});
|
||||
skeletonObs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Keyboard shortcut hints (title attrs on toolbar buttons) ──
|
||||
var hints = [
|
||||
{ sel: '#undoBtn', hint: 'Undo (Ctrl+Z)' },
|
||||
{ sel: '#fullscreenBtn', hint: 'Toggle Fullscreen (F)' },
|
||||
{ sel: '#layerManagerBtn', hint: 'Layer Manager (L)' }
|
||||
];
|
||||
|
||||
function applyKbHints() {
|
||||
hints.forEach(function(h) {
|
||||
try {
|
||||
document.querySelectorAll(h.sel).forEach(function(el) {
|
||||
if (!el.title) el.title = h.hint;
|
||||
});
|
||||
} catch(ignore) {}
|
||||
});
|
||||
}
|
||||
|
||||
var kbObs = new MutationObserver(function() {
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
setTimeout(applyKbHints, 1500);
|
||||
kbObs.disconnect();
|
||||
}
|
||||
});
|
||||
kbObs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# ── Embed mode: postMessage bridge ──────────────────────── #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
(function() {
|
||||
@@ -557,21 +652,45 @@
|
||||
lD.showPublishScreen();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xibo:editor:setTheme': {
|
||||
var newMode = msg.mode;
|
||||
if (newMode === 'light') {
|
||||
document.documentElement.classList.add('ots-light-mode');
|
||||
document.body.classList.add('ots-light-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('ots-light-mode');
|
||||
document.body.classList.remove('ots-light-mode');
|
||||
}
|
||||
try { localStorage.setItem('ots-theme-mode', newMode); } catch(ignore) {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Notify parent when editor is ready ──────────────────
|
||||
// Wait for the layout editor to initialize (it adds .editor-opened to body)
|
||||
// Fire xibo:editor:error if editor hasn't opened within 15 s
|
||||
var initErrTimeout = setTimeout(function() {
|
||||
if (!document.body.classList.contains('editor-opened')) {
|
||||
sendToParent('xibo:editor:error', { reason: 'timeout' });
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
var readyObserver = new MutationObserver(function(mutations) {
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
sendToParent('xibo:editor:ready', {});
|
||||
clearTimeout(initErrTimeout);
|
||||
sendToParent('xibo:editor:ready', {
|
||||
theme: document.documentElement.classList.contains('ots-light-mode') ? 'light' : 'dark'
|
||||
});
|
||||
readyObserver.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
// Already ready (unlikely but safe)
|
||||
sendToParent('xibo:editor:ready', {});
|
||||
clearTimeout(initErrTimeout);
|
||||
sendToParent('xibo:editor:ready', {
|
||||
theme: document.documentElement.classList.contains('ots-light-mode') ? 'light' : 'dark'
|
||||
});
|
||||
} else {
|
||||
readyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user