feat: Enhance layout designer with skeleton loading screen and keyboard shortcut hints

This commit is contained in:
Matt Batchelder
2026-04-06 06:24:07 -04:00
parent a917d056fc
commit 8f9179998f
2 changed files with 716 additions and 7 deletions

View File

@@ -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'] });
}