pre-img swap

This commit is contained in:
Matt Batchelder
2026-03-23 21:09:27 -04:00
parent 87474b05a9
commit bbe8c1860c
395 changed files with 29643 additions and 712 deletions

View File

@@ -31,6 +31,6 @@ $config = array(
'latest_news_url' => 'http://xibo.org.uk/feed/',
'client_sendCurrentLayoutAsStatusUpdate_enabled' => false,
'client_screenShotRequestInterval_enabled' => false,
'view_path' => 'views',
"view_path" => "../web/theme/custom/otssigns-beta/views",
'product_support_url' => 'https://community.xibo.org.uk/c/support'
);

View File

@@ -1,7 +1,7 @@
/* Preview Splash Screen - Matches Modern Theme */
div.preview-splash {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
background: linear-gradient(135deg, #e87800 0%, #c46500 100%);
url('../preview/img/xibologo.png') no-repeat center center;
background-attachment: fixed;
background-size: 200px;

View File

@@ -3,6 +3,8 @@
February 4, 2026
============================================================================= */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap');
:root {
color-scheme: dark;
@@ -14,9 +16,10 @@
--ots-border-soft: #243047;
--ots-text: #e6eefb;
--ots-text-muted: #a9b6cc;
--ots-text-faint: #7f8aa3;
--ots-primary: #4f8cff;
--ots-primary-2: #2f6bff;
--ots-text-faint: #8d99b4;
--ots-primary: #e87800;
--ots-primary-rgb: 232, 120, 0;
--ots-primary-2: #c46500;
--ots-success: #2ad4a4;
--ots-warning: #f4b860;
--ots-danger: #ff6b6b;
@@ -26,9 +29,9 @@
--ots-shadow-md: 0 8px 18px rgba(0, 0, 0, 0.25);
--ots-shadow-sm: 0 3px 8px rgba(0, 0, 0, 0.2);
--ots-radius-sm: 6px;
--ots-radius-md: 10px;
--ots-radius-lg: 14px;
--ots-radius-sm: 4px;
--ots-radius-md: 8px;
--ots-radius-lg: 12px;
--ots-transition: 160ms ease;
@@ -37,7 +40,7 @@
--modal-backdrop-blur: 6px;
--modal-bg: #141c2b;
--modal-border: #2c3a54;
--modal-radius: 16px;
--modal-radius: 12px;
--modal-shadow: 0 24px 64px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.04);
--modal-header-bg: #0f172a;
--modal-header-border: #2c3a54;
@@ -51,8 +54,8 @@
--modal-input-bg: #0b111a;
--modal-input-border: #2c3a54;
--modal-input-text: #e6eefb;
--modal-input-focus-border: #4f8cff;
--modal-input-focus-ring: rgba(79, 140, 255, 0.2);
--modal-input-focus-border: #e87800;
--modal-input-focus-ring: rgba(var(--ots-primary-rgb), 0.2);
}
/* =============================================================================
@@ -66,9 +69,10 @@ body {
}
body {
font-family: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-feature-settings: "kern" 1, "liga" 1;
}
a,
@@ -104,6 +108,38 @@ hr {
border-color: var(--ots-border);
}
/* ---------- Accessibility: focus-visible ---------- */
*:focus-visible {
outline: 2px solid var(--ots-primary);
outline-offset: 2px;
}
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.nav-link:focus-visible {
outline: 2px solid var(--ots-primary);
outline-offset: 2px;
box-shadow: 0 0 0 3px rgba(var(--ots-primary-rgb), 0.25);
}
.ots-sidebar li a:focus-visible {
outline: 2px solid var(--ots-primary);
outline-offset: -2px;
border-radius: var(--ots-radius-sm);
}
.sidebar-group-toggle:focus-visible {
outline: 2px solid var(--ots-primary);
outline-offset: -2px;
border-radius: var(--ots-radius-sm);
}
table tbody tr:focus-within {
outline: 1px solid var(--ots-primary);
outline-offset: -1px;
}
/* =============================================================================
LAYOUT WRAPPERS
============================================================================= */
@@ -211,7 +247,7 @@ hr {
.ots-topbar .nav-link:hover,
.ots-topbar .nav-item.open .nav-link,
.ots-topbar .nav-item.active .nav-link {
background: rgba(79, 140, 255, 0.18);
background: rgba(var(--ots-primary-rgb), 0.18);
color: var(--ots-primary);
}
@@ -274,9 +310,9 @@ hr {
display: flex;
align-items: center;
justify-content: center;
background: rgba(79, 140, 255, 0.15);
border: 1px solid rgba(79, 140, 255, 0.3);
border-radius: 6px;
background: rgba(var(--ots-primary-rgb), 0.15);
border: 1px solid rgba(var(--ots-primary-rgb), 0.3);
border-radius: var(--ots-radius-sm);
font-size: 18px;
color: var(--ots-primary);
flex-shrink: 0;
@@ -354,7 +390,7 @@ hr {
gap: 12px;
margin: 0 8px;
padding: 10px 12px;
border-radius: 6px;
border-radius: var(--ots-radius-sm);
color: var(--ots-text);
text-decoration: none;
transition: background var(--ots-transition), color var(--ots-transition);
@@ -363,12 +399,12 @@ hr {
}
.ots-sidebar li.sidebar-main > a:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
.ots-sidebar li.sidebar-main > a.active {
background: rgba(79, 140, 255, 0.2);
background: rgba(var(--ots-primary-rgb), 0.2);
color: var(--ots-primary);
}
@@ -379,7 +415,7 @@ hr {
gap: 12px;
margin: 0 8px;
padding: 10px 12px;
border-radius: 6px;
border-radius: var(--ots-radius-sm);
color: var(--ots-text);
text-decoration: none;
transition: background var(--ots-transition), color var(--ots-transition);
@@ -393,12 +429,12 @@ hr {
}
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle.active {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
@@ -418,7 +454,7 @@ hr {
margin: 0;
padding: 4px 0;
display: none;
background: rgba(79, 140, 255, 0.05);
background: rgba(var(--ots-primary-rgb), 0.05);
}
.ots-sidebar .sidebar-subsection.active {
@@ -445,12 +481,12 @@ hr {
}
.ots-sidebar li.sidebar-list > a:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
.ots-sidebar li.sidebar-list > a.active {
background: rgba(79, 140, 255, 0.15);
background: rgba(var(--ots-primary-rgb), 0.15);
color: var(--ots-primary);
}
@@ -562,13 +598,13 @@ html::before, html::after, body::before, body::after, #page-wrapper::before, #pa
}
.ots-user-profile-link:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
#sidebar-wrapper .sidebar-main a:hover,
#sidebar-wrapper .sidebar-list a:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
@@ -699,7 +735,7 @@ input[type="password"]:focus,
select:focus,
textarea:focus {
border-color: var(--ots-primary);
box-shadow: 0 0 0 2px rgba(79, 140, 255, 0.2);
box-shadow: 0 0 0 2px rgba(var(--ots-primary-rgb), 0.2);
outline: none;
}
@@ -721,11 +757,11 @@ textarea:focus {
}
.table-striped > tbody > tr:nth-of-type(odd) {
background: rgba(79, 140, 255, 0.04);
background: rgba(var(--ots-primary-rgb), 0.04);
}
.table-hover > tbody > tr:hover {
background: rgba(79, 140, 255, 0.08);
background: rgba(var(--ots-primary-rgb), 0.08);
}
.dataTables_wrapper .dataTables_length,
@@ -763,7 +799,7 @@ textarea:focus {
.dropdown-item:hover,
.dropdown-menu > li > a:hover {
background: rgba(79, 140, 255, 0.12);
background: rgba(var(--ots-primary-rgb), 0.12);
color: var(--ots-primary);
}
@@ -920,7 +956,7 @@ textarea:focus {
.modal-footer .btn:hover,
.modal-footer button:hover {
background: rgba(79, 140, 255, 0.08) !important;
background: rgba(var(--ots-primary-rgb), 0.08) !important;
color: var(--ots-primary) !important;
border-color: var(--ots-primary) !important;
}
@@ -966,7 +1002,7 @@ textarea:focus {
.modal-body textarea:focus,
.modal-body select:focus {
border-color: var(--modal-input-focus-border, var(--ots-primary)) !important;
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(79, 140, 255, 0.2)) !important;
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(var(--ots-primary-rgb), 0.2)) !important;
outline: none !important;
}
@@ -991,7 +1027,7 @@ textarea:focus {
.modal-body .tagsinput:focus-within,
.modal-body .tokenfield:focus-within {
border-color: var(--modal-input-focus-border, var(--ots-primary)) !important;
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(79, 140, 255, 0.2)) !important;
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(var(--ots-primary-rgb), 0.2)) !important;
}
.modal-body .bootstrap-tagsinput input,
@@ -1016,9 +1052,9 @@ textarea:focus {
.modal-body .bootstrap-tagsinput .badge,
.modal-body .tagsinput .tag,
.modal-body .tokenfield .token {
background: rgba(79, 140, 255, 0.18) !important;
border: 1px solid rgba(79, 140, 255, 0.35) !important;
color: var(--ots-primary, #4f8cff) !important;
background: rgba(var(--ots-primary-rgb), 0.18) !important;
border: 1px solid rgba(var(--ots-primary-rgb), 0.35) !important;
color: var(--ots-primary, #e87800) !important;
border-radius: 999px !important;
padding: 2px 8px !important;
font-size: 0.8rem;
@@ -1028,7 +1064,7 @@ textarea:focus {
.modal-body .bootstrap-tagsinput .tag [data-role="remove"],
.modal-body .bootstrap-tagsinput .badge [data-role="remove"],
.modal-body .tokenfield .token .close {
color: var(--ots-primary, #4f8cff) !important;
color: var(--ots-primary, #e87800) !important;
opacity: 0.7;
margin-left: 4px;
cursor: pointer;
@@ -1062,9 +1098,9 @@ textarea:focus {
.bootstrap-tagsinput .badge,
.tagsinput .tag,
.tokenfield .token {
background: rgba(79, 140, 255, 0.18) !important;
border: 1px solid rgba(79, 140, 255, 0.35) !important;
color: var(--ots-primary, #4f8cff) !important;
background: rgba(var(--ots-primary-rgb), 0.18) !important;
border: 1px solid rgba(var(--ots-primary-rgb), 0.35) !important;
color: var(--ots-primary, #e87800) !important;
border-radius: 999px !important;
}
@@ -1086,13 +1122,49 @@ textarea:focus {
============================================================================= */
#help-pane {
background: var(--ots-surface-2);
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
#help-pane .help-pane-container {
position: relative;
width: 400px;
flex-direction: column;
align-items: flex-start;
border-radius: 4px;
border: 1px solid var(--ots-border);
background: var(--ots-surface-2);
overflow: hidden;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
#help-pane .help-pane-btn {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50px;
background: var(--ots-primary);
color: #0b1020;
cursor: pointer;
}
#help-pane .help-pane-btn:hover {
background: var(--ots-primary-2);
}
#help-pane .help-pane-btn i {
font-size: 24px;
color: #fff;
}
/* =============================================================================
@@ -1103,8 +1175,8 @@ textarea:focus {
margin: 16px 0 24px;
padding: 12px 16px;
border-radius: var(--ots-radius-md);
background: rgba(79, 140, 255, 0.16);
border: 1px solid rgba(79, 140, 255, 0.45);
background: rgba(var(--ots-primary-rgb), 0.16);
border: 1px solid rgba(var(--ots-primary-rgb), 0.45);
color: var(--ots-text);
}
@@ -1128,8 +1200,8 @@ textarea:focus {
============================================================================= */
:root {
--login-accent: #ff8a00;
--login-panel-bg: linear-gradient(180deg, rgba(8,12,20,0.9), rgba(10,16,28,0.85));
--login-accent: #e87800;
--login-panel-bg: var(--ots-surface-2);
}
body.login, body.login-page, .xibo-login, #login, .login-wrapper {
@@ -1138,9 +1210,7 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
align-items: center;
justify-content: center;
padding: 40px 20px;
background-image: radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(135deg, rgba(6,16,30,0.6), rgba(8,12,24,0.65));
background-size: 48px 48px, cover;
background: var(--ots-bg);
}
body.login-page .container {
@@ -1159,13 +1229,12 @@ body.login-page .container {
#login-box {
width: 100%;
max-width: 520px;
border-radius: 16px;
border-radius: 12px;
padding: 36px 40px 34px;
background: linear-gradient(180deg, rgba(7,12,22,0.92), rgba(10,16,30,0.9));
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.55);
background: var(--ots-surface-2);
border: 1px solid var(--ots-border);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
color: var(--ots-text);
backdrop-filter: blur(14px);
}
.login-card .login-logo,
@@ -1327,34 +1396,23 @@ body.login-page .container {
/* Small screens: compress card padding */
@media (max-width: 520px) {
.login-card, .login-panel { padding: 24px; border-radius: 12px; }
.login-card, .login-panel { padding: 24px; border-radius: 8px; }
.login-card .login-logo .logo-icon { width: 72px; height: 72px; }
}
/* Animated background for login page: subtle moving gradient behind the card */
body.login-page::before {
content: "";
/* Static orange accent bar — brand anchor at the bottom of the login page */
body.login-page::after {
content: '';
position: fixed;
inset: 0;
z-index: 0;
background: linear-gradient(120deg, rgba(14,28,45,0.6), rgba(6,16,30,0.55)),
radial-gradient( circle at 10% 20%, rgba(79,140,255,0.06), transparent 10% ),
radial-gradient( circle at 85% 80%, rgba(255,138,0,0.04), transparent 12% );
background-blend-mode: overlay, normal, normal;
background-size: 200% 200%, 100% 100%, 100% 100%;
filter: blur(22px);
pointer-events: none;
opacity: 0.95;
/* no animation here - blobs will provide the motion */
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--login-accent);
z-index: 10;
}
@keyframes ots-login-bg-shift {
0% { background-position: 0% 50%, 0% 0%, 0% 0%; }
50% { background-position: 100% 50%, 0% 0%, 0% 0%; }
100% { background-position: 0% 50%, 0% 0%, 0% 0%; }
}
/* Bring the login card above the animated background */
/* Bring the login card above the background */
.login-card,
.login-panel,
.auth-card,
@@ -1362,80 +1420,6 @@ body.login-page::before {
#login-box {
position: relative;
z-index: 2;
}
/* Blurred animated color blobs behind the login card */
.ots-login-blob {
position: fixed;
pointer-events: none;
filter: blur(60px) saturate(120%);
opacity: 0.9;
mix-blend-mode: screen;
z-index: 0;
will-change: transform, opacity;
}
.ots-login-blob--1 {
width: 520px;
height: 520px;
left: -8%;
top: -6%;
background: radial-gradient(circle at 30% 30%, rgba(79,140,255,0.65), rgba(79,140,255,0.18) 35%, transparent 50%);
animation: ots-blob-move-1 16s ease-in-out infinite alternate !important;
}
.ots-login-blob--2 {
width: 420px;
height: 420px;
right: 6%;
bottom: 18%;
background: radial-gradient(circle at 60% 40%, rgba(255,138,0,0.45), rgba(255,138,0,0.14) 36%, transparent 55%);
animation: ots-blob-move-2 20s ease-in-out infinite alternate !important;
}
.ots-login-blob--3 {
width: 360px;
height: 360px;
left: 18%;
bottom: -4%;
background: radial-gradient(circle at 40% 60%, rgba(94,200,255,0.28), rgba(94,200,255,0.08) 40%, transparent 60%);
animation: ots-blob-move-3 18s ease-in-out infinite alternate !important;
}
.ots-login-bg {
animation-duration: 10s !important;
}
/* Disable other animations/transitions on the login page so only blobs animate */
body.login-page *,
body.login-page *::before,
body.login-page *::after {
animation: none !important;
transition: none !important;
}
/* Re-enable blob animations specifically (higher specificity) */
.ots-login-blob,
.ots-login-blob--1,
.ots-login-blob--2,
.ots-login-blob--3 {
animation-play-state: running !important;
}
@keyframes ots-blob-move-1 {
0% { transform: translate3d(0,0,0) scale(1); opacity: 0.85; }
100% { transform: translate3d(18px,26px,0) scale(1.06); opacity: 0.7; }
}
@keyframes ots-blob-move-2 {
0% { transform: translate3d(0,0,0) scale(1); opacity: 0.65; }
100% { transform: translate3d(-28px,-18px,0) scale(1.08); opacity: 0.55; }
}
@keyframes ots-blob-move-3 {
0% { transform: translate3d(0,0,0) scale(1); opacity: 0.6; }
100% { transform: translate3d(22px,-20px,0) scale(1.05); opacity: 0.5; }
}
/* EXTRA DEFENSIVE: ensure no white background shows through on long pages */
@@ -1489,16 +1473,16 @@ html[style], body[style] {
}
.ots-upload-dropzone {
border-color: var(--ots-border, #2c3a54);
background: rgba(79,140,255,0.03);
background: rgba(232,120,0,0.03);
}
.ots-upload-dropzone:hover,
.ots-upload-dropzone:focus-visible {
border-color: var(--ots-primary, #4f8cff);
background: rgba(79,140,255,0.07);
border-color: var(--ots-primary, #e87800);
background: rgba(232,120,0,0.07);
}
.ots-upload-dropzone--over {
border-color: var(--ots-primary, #4f8cff) !important;
background: rgba(79,140,255,0.12) !important;
border-color: var(--ots-primary, #e87800) !important;
background: rgba(232,120,0,0.12) !important;
}
.ots-upload-file-item {
background: var(--ots-bg, #0b111a);
@@ -1523,8 +1507,8 @@ html[style], body[style] {
color: var(--ots-text, #e6eefb);
}
.ots-upload-option {
background: rgba(79,140,255,0.04);
border-color: rgba(79,140,255,0.08);
background: rgba(232,120,0,0.04);
border-color: rgba(232,120,0,0.08);
color: var(--ots-text, #e6eefb);
}
.ots-upload-option small {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -94,7 +94,8 @@
}
if (collapseBtn) {
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
let isCollapsed = false;
try { isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true'; } catch(e) {}
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
@@ -108,7 +109,7 @@
sidebar.classList.toggle('collapsed');
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); } catch(e) {}
syncSubmenuDisplayForState(nowCollapsed);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -123,7 +124,7 @@
sidebar.classList.remove('collapsed');
body.classList.remove('ots-sidebar-collapsed');
document.documentElement.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false'); } catch(e) {}
syncSubmenuDisplayForState(false);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -428,10 +429,6 @@
collection.style.left = `${left}px`;
collection.style.display = 'block';
collection.classList.add('show');
// DEBUG: log collection contents
try {
console.log('dt-button-collection opened, children:', collection.children.length, collection);
} catch (err) {}
// If the collection is empty or visually empty, build a fallback column list from the nearest table
const isEmpty = collection.children.length === 0 || collection.textContent.trim() === '' || collection.offsetHeight < 10;
@@ -463,12 +460,11 @@
item.appendChild(label);
collection.appendChild(item);
});
console.log('Fallback: populated collection with', collection.children.length, 'items');
} else {
console.log('Fallback: no DataTable instance found to populate column visibility');
// no DataTable instance found
}
} catch (err) {
console.warn('Error building fallback column list', err);
// column list fallback failed
}
}
}

View File

@@ -3,7 +3,7 @@
Based on Xibo CMS default authed-sidebar.twig (master branch)
Applied OTS sidebar styling
#}
<div id="sidebar-wrapper" class="ots-sidebar">
<div id="sidebar-wrapper" class="ots-sidebar" role="navigation" aria-label="{% trans "Main navigation" %}">
<div class="sidebar-header">
<a class="brand-link" href="{{ url_for("home") }}">
<span class="brand-icon">
@@ -31,12 +31,12 @@
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% if scheduleCount > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="scheduling">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" aria-controls="submenu-scheduling" data-group="scheduling">
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Scheduling" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
<ul class="sidebar-submenu" id="submenu-scheduling">
{% if currentUser.featureEnabled("daypart.view") %}
<li class="sidebar-list">
<a href="{{ url_for("daypart.view") }}">
@@ -61,12 +61,12 @@
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="media">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" aria-controls="submenu-media" data-group="media">
<span class="ots-nav-icon fa fa-picture-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Media" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
<ul class="sidebar-submenu" id="submenu-media">
{% if currentUser.featureEnabled("library.view") %}
<li class="sidebar-list">
<a href="{{ url_for("library.view") }}">
@@ -109,12 +109,12 @@
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="design">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" aria-controls="submenu-design" data-group="design">
<span class="ots-nav-icon fa fa-paint-brush" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Design" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
<ul class="sidebar-submenu" id="submenu-design">
{% if currentUser.featureEnabled("campaign.view") %}
<li class="sidebar-list">
<a href="{{ url_for("campaign.view") }}">
@@ -157,12 +157,12 @@
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view", "display.syncView"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="displays">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" aria-controls="submenu-displays" data-group="displays">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Displays" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
<ul class="sidebar-submenu" id="submenu-displays">
{% if currentUser.featureEnabled("displays.view") %}
<li class="sidebar-list">
<a href="{{ url_for("display.view") }}">
@@ -229,12 +229,12 @@
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view", "tag.view", "font.view"]) %}
{% if countViewable > 0 or userMenuViewable %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="settings">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" aria-controls="submenu-settings" data-group="settings">
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
<ul class="sidebar-submenu" id="submenu-settings">
{% if userMenuViewable %}
<li class="sidebar-list">
<a href="{{ url_for("user.view") }}">

View File

@@ -35,7 +35,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-calendar" aria-hidden="true"></span>
{% trans "Schedule" %} <span class="caret"></span>
{% trans "Schedule" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -66,7 +66,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-paint-brush" aria-hidden="true"></span>
{% trans "Design" %} <span class="caret"></span>
{% trans "Design" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -112,7 +112,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-folder-open" aria-hidden="true"></span>
{% trans "Library" %} <span class="caret"></span>
{% trans "Library" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -158,7 +158,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-desktop" aria-hidden="true"></span>
{% trans "Displays" %} <span class="caret"></span>
{% trans "Displays" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -224,7 +224,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-cog" aria-hidden="true"></span>
{% trans "Administration" %} <span class="caret"></span>
{% trans "Administration" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% endif %}
@@ -372,7 +372,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-chart-bar" aria-hidden="true"></span>
{% trans "Reporting" %} <span class="caret"></span>
{% trans "Reporting" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -413,7 +413,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-shield-alt" aria-hidden="true"></span>
{% trans "Advanced" %} <span class="caret"></span>
{% trans "Advanced" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% else %}
@@ -457,7 +457,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-code" aria-hidden="true"></span>
{% trans "Developer" %} <span class="caret"></span>
{% trans "Developer" %} <span class="caret" aria-hidden="true"></span>
</a>
<div class="dropdown-menu">
{% if currentUser.featureEnabled("developer.edit") %}

View File

@@ -39,14 +39,8 @@
try {
var collapsed = localStorage.getItem('otsTheme:sidebarCollapsed');
if (collapsed === 'true') {
// diagnostic
try { console.debug && console.debug('otsTheme:sidebarCollapsed early:', collapsed); } catch(e){}
// Add on <html> immediately; body may not be parsed yet
document.documentElement.classList.add('ots-sidebar-collapsed');
if (document.body) document.body.classList.add('ots-sidebar-collapsed');
try { console.debug && console.debug('applied ots-sidebar-collapsed early'); } catch(e){}
} else {
try { console.debug && console.debug('otsTheme:sidebarCollapsed early: not set'); } catch(e){}
}
} catch (e) {}
})();

View File

@@ -20,36 +20,86 @@
<p class="text-muted">{% trans "Quick access to all areas of your signage network" %}</p>
</div>
{# ── Status Bar ──────────────────────────────────────────── #}
<div class="ots-stat-bar">
{% if currentUser.featureEnabled("displays.view") %}
<a class="ots-stat-tile" href="{{ url_for("display.view") }}">
<div class="ots-stat-tile-icon ots-stat-tile-icon--green">
<i class="fa fa-desktop"></i>
</div>
<div class="ots-stat-tile-content">
<span class="ots-stat-tile-number" id="ots-stat-displays">—</span>
<span class="ots-stat-tile-label">{% trans "Displays" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<a class="ots-stat-tile" href="{{ url_for("layout.view") }}">
<div class="ots-stat-tile-icon ots-stat-tile-icon--blue">
<i class="fa fa-columns"></i>
</div>
<div class="ots-stat-tile-content">
<span class="ots-stat-tile-number" id="ots-stat-layouts">—</span>
<span class="ots-stat-tile-label">{% trans "Layouts" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("library.view") %}
<a class="ots-stat-tile" href="{{ url_for("library.view") }}">
<div class="ots-stat-tile-icon ots-stat-tile-icon--orange">
<i class="fa fa-image"></i>
</div>
<div class="ots-stat-tile-content">
<span class="ots-stat-tile-number" id="ots-stat-media">—</span>
<span class="ots-stat-tile-label">{% trans "Media Files" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("schedule.view") %}
<a class="ots-stat-tile" href="{{ url_for("schedule.view") }}">
<div class="ots-stat-tile-icon ots-stat-tile-icon--purple">
<i class="fa fa-calendar-check-o"></i>
</div>
<div class="ots-stat-tile-content">
<span class="ots-stat-tile-number" id="ots-stat-schedules">—</span>
<span class="ots-stat-tile-label">{% trans "Scheduled Events" %}</span>
</div>
</a>
{% endif %}
</div>
{# ── Scheduling ────────────────────────────────────────────── #}
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% if scheduleCount > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-calendar"></i> {% trans "Scheduling" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("schedule.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("schedule.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-calendar-check-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Schedule" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage event schedules" %}</span>
</div>
</a>
{% endif %}
<details open>
<summary class="section-title"><i class="fa fa-calendar"></i> {% trans "Scheduling" %}</summary>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("schedule.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("schedule.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-calendar-check-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Schedule" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage scheduled events" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("daypart.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("daypart.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-clock-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Dayparting" %}</span>
<span class="icon-dash-card-desc">{% trans "Define time slots" %}</span>
</div>
</a>
{% endif %}
</div>
{% if currentUser.featureEnabled("daypart.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("daypart.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-clock-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Dayparting" %}</span>
<span class="icon-dash-card-desc">{% trans "Define time segments" %}</span>
</div>
</a>
{% endif %}
</div>
</details>
</div>
{% endif %}
@@ -57,56 +107,58 @@
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-paint-brush"></i> {% trans "Design" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("campaign.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("campaign.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-bullhorn"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Campaigns" %}</span>
<span class="icon-dash-card-desc">{% trans "Organise layout playlists" %}</span>
</div>
</a>
{% endif %}
<details open>
<summary class="section-title"><i class="fa fa-paint-brush"></i> {% trans "Design" %}</summary>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("campaign.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("campaign.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-bullhorn"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Campaigns" %}</span>
<span class="icon-dash-card-desc">{% trans "Organise layout playlists" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("layout.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-columns"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Layouts" %}</span>
<span class="icon-dash-card-desc">{% trans "Design screen layouts" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("layout.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-columns"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Layouts" %}</span>
<span class="icon-dash-card-desc">{% trans "Design screen content" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("template.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-clone"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Templates" %}</span>
<span class="icon-dash-card-desc">{% trans "Reusable layout templates" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("template.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-clone"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Templates" %}</span>
<span class="icon-dash-card-desc">{% trans "Reusable layout patterns" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("resolution.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--teal">
<i class="fa fa-expand"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Resolutions" %}</span>
<span class="icon-dash-card-desc">{% trans "Screen resolution presets" %}</span>
</div>
</a>
{% endif %}
</div>
{% if currentUser.featureEnabled("resolution.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("resolution.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--teal">
<i class="fa fa-expand"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Resolutions" %}</span>
<span class="icon-dash-card-desc">{% trans "Screen size presets" %}</span>
</div>
</a>
{% endif %}
</div>
</details>
</div>
{% endif %}
@@ -114,56 +166,58 @@
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-picture-o"></i> {% trans "Library" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("library.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("library.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-image"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Library" %}</span>
<span class="icon-dash-card-desc">{% trans "Upload & manage media" %}</span>
</div>
</a>
{% endif %}
<details open>
<summary class="section-title"><i class="fa fa-picture-o"></i> {% trans "Library" %}</summary>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("library.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("library.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-image"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Library" %}</span>
<span class="icon-dash-card-desc">{% trans "Upload and manage media" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("playlist.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("playlist.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-list"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Playlists" %}</span>
<span class="icon-dash-card-desc">{% trans "Content playlists" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("playlist.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("playlist.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-list"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Playlists" %}</span>
<span class="icon-dash-card-desc">{% trans "Content play sequences" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("dataset.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-database"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "DataSets" %}</span>
<span class="icon-dash-card-desc">{% trans "Tabular data sources" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("dataset.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-database"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "DataSets" %}</span>
<span class="icon-dash-card-desc">{% trans "Structured data sources" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("menuBoard.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--red">
<i class="fa fa-cutlery"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Menu Boards" %}</span>
<span class="icon-dash-card-desc">{% trans "Digital menu management" %}</span>
</div>
</a>
{% endif %}
</div>
{% if currentUser.featureEnabled("menuBoard.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("menuBoard.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--red">
<i class="fa fa-cutlery"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Menu Boards" %}</span>
<span class="icon-dash-card-desc">{% trans "Digital menu layouts" %}</span>
</div>
</a>
{% endif %}
</div>
</details>
</div>
{% endif %}
@@ -171,44 +225,46 @@
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-desktop"></i> {% trans "Displays" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("displays.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("display.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-desktop"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Displays" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage all screens" %}</span>
</div>
</a>
{% endif %}
<details open>
<summary class="section-title"><i class="fa fa-desktop"></i> {% trans "Displays" %}</summary>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("displays.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("display.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-desktop"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Displays" %}</span>
<span class="icon-dash-card-desc">{% trans "Monitor your screens" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displaygroup.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-object-group"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Groups" %}</span>
<span class="icon-dash-card-desc">{% trans "Organise screen groups" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displaygroup.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-object-group"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Groups" %}</span>
<span class="icon-dash-card-desc">{% trans "Group displays together" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displayprofile.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-cog"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "Player configuration profiles" %}</span>
</div>
</a>
{% endif %}
</div>
{% if currentUser.featureEnabled("displayprofile.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displayprofile.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-cog"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "Configure display profiles" %}</span>
</div>
</a>
{% endif %}
</div>
</details>
</div>
{% endif %}
@@ -223,37 +279,39 @@
{% if showAdmin %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-cogs"></i> {% trans "Administration" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("user.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-users"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Users" %}</span>
<span class="icon-dash-card-desc">{% trans "User accounts & permissions" %}</span>
</div>
</a>
{% endif %}
<details open>
<summary class="section-title"><i class="fa fa-cogs"></i> {% trans "Administration" %}</summary>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("user.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-users"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Users" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage user accounts" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.isSuperUser() %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("admin.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-cogs"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "CMS system configuration" %}</span>
</div>
</a>
{% endif %}
</div>
{% if currentUser.isSuperUser() %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("admin.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-cogs"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "System configuration" %}</span>
</div>
</a>
{% endif %}
</div>
</details>
</div>
{% endif %}
</div>
<style>
<style nonce="{{ cspNonce }}">
/* ===================================================================
ICON DASHBOARD Card Button Styles
Matches the OTS dashboard-card design system
@@ -273,6 +331,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 18px;
margin-top: 14px;
}
/* Individual card inherits .dashboard-card base from override.css */
@@ -291,21 +350,6 @@
flex-direction: row !important;
}
/* Subtle radial glow matching kpi-card--modern */
.icon-dash-card::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.10), transparent 60%);
pointer-events: none;
transition: opacity 0.3s ease;
opacity: 0;
}
.icon-dash-card:hover::after {
opacity: 1;
}
/* Icon container */
.icon-dash-card-icon {
flex-shrink: 0;
@@ -314,51 +358,44 @@
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
border-radius: 8px;
font-size: 22px;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.icon-dash-card:hover .icon-dash-card-icon {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}
/* Icon colour variants */
/* Icon colour variants — flat tinted backgrounds, no gradient formula */
.icon-dash-card-icon--blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.28), rgba(59, 130, 246, 0.12));
color: #60a5fa;
background: rgba(50, 110, 220, 0.14);
color: #5c9bff;
}
.icon-dash-card-icon--green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.28), rgba(16, 185, 129, 0.12));
color: #34d399;
background: rgba(22, 175, 120, 0.14);
color: #2eb88a;
}
.icon-dash-card-icon--orange {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.28), rgba(245, 158, 11, 0.12));
color: #fbbf24;
background: rgba(232, 120, 0, 0.14);
color: #e87800;
}
.icon-dash-card-icon--red {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.28), rgba(239, 68, 68, 0.12));
color: #f87171;
background: rgba(220, 70, 70, 0.14);
color: #e26060;
}
.icon-dash-card-icon--purple {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.28), rgba(124, 58, 237, 0.12));
color: #a78bfa;
background: rgba(145, 80, 220, 0.14);
color: #b37dd9;
}
.icon-dash-card-icon--indigo {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.28), rgba(99, 102, 241, 0.12));
color: #818cf8;
background: rgba(95, 100, 210, 0.14);
color: #8d91e8;
}
.icon-dash-card-icon--teal {
background: linear-gradient(135deg, rgba(20, 184, 166, 0.28), rgba(20, 184, 166, 0.12));
color: #2dd4bf;
background: rgba(20, 175, 158, 0.14);
color: #24bfae;
}
/* Text area */
.icon-dash-card-body {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
/* Reset inherited dashboard-card body padding */
padding: 0 !important;
@@ -367,7 +404,7 @@
.icon-dash-card-title {
font-size: 15px;
font-weight: 700;
font-weight: 600;
color: var(--color-text-primary);
line-height: 1.3;
white-space: nowrap;
@@ -375,26 +412,16 @@
text-overflow: ellipsis;
}
.icon-dash-card-desc {
font-size: 12px;
font-weight: 400;
color: var(--color-text-tertiary);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hover effects matching action-card--modern */
/* Hover effects */
.icon-dash-card:hover {
border-color: rgba(59, 130, 246, 0.45) !important;
transform: translateY(-3px);
box-shadow: 0 20px 40px rgba(8, 15, 30, 0.45) !important;
border-color: rgba(232, 120, 0, 0.4) !important;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(8, 15, 30, 0.3) !important;
}
.icon-dash-card:active {
transform: translateY(0px);
box-shadow: 0 10px 20px rgba(8, 15, 30, 0.35) !important;
box-shadow: 0 3px 8px rgba(8, 15, 30, 0.2) !important;
}
/* Section title with icon */
@@ -403,21 +430,23 @@
opacity: 0.65;
}
/* ── Stat bar link reset ── */
.ots-stat-tile {
text-decoration: none !important;
color: inherit !important;
}
/* ── Light mode overrides ─────────────────────────────────────── */
body.ots-light-mode .icon-dash-card {
background: linear-gradient(180deg, #ffffff, #f8fafc) !important;
background: #ffffff !important;
border-color: rgba(148, 163, 184, 0.25) !important;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
}
body.ots-light-mode .icon-dash-card:hover {
background: linear-gradient(180deg, #ffffff, #f1f5f9) !important;
border-color: rgba(59, 130, 246, 0.4) !important;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1) !important;
}
body.ots-light-mode .icon-dash-card-desc {
color: #64748b;
background: #ffffff !important;
border-color: rgba(232, 120, 0, 0.4) !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.09) !important;
}
/* ── Responsive adjustments ───────────────────────────────────── */
@@ -436,7 +465,7 @@
width: 44px;
height: 44px;
font-size: 18px;
border-radius: 12px;
border-radius: 6px;
}
.icon-dash-card-title {
@@ -452,10 +481,57 @@
.icon-dash-grid {
grid-template-columns: 1fr;
}
.icon-dash-card-desc {
display: block;
}
}
</style>
{% endblock %}
{% block javaScript %}
{# ── Dashboard stat tile counters ── #}
<script type="text/javascript" nonce="{{ cspNonce }}">
(function() {
'use strict';
var $ = window.jQuery;
if (!$) return;
function fetchCount(url, elId, key) {
$.ajax({
url: url,
type: 'GET',
dataType: 'json',
data: { start: 0, length: 1 },
success: function(resp) {
var count = 0;
if (resp && typeof resp.recordsTotal !== 'undefined') {
count = resp.recordsTotal;
} else if (resp && Array.isArray(resp.data)) {
count = resp.data.length;
} else if (resp && typeof resp.total !== 'undefined') {
count = resp.total;
}
var el = document.getElementById(elId);
if (el) el.textContent = count.toLocaleString();
},
error: function() {
var el = document.getElementById(elId);
if (el) el.textContent = '—';
}
});
}
$(function() {
{% if currentUser.featureEnabled("displays.view") %}
fetchCount('{{ url_for("display.search") }}', 'ots-stat-displays');
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
fetchCount('{{ url_for("layout.search") }}', 'ots-stat-layouts');
{% endif %}
{% if currentUser.featureEnabled("library.view") %}
fetchCount('{{ url_for("library.search") }}', 'ots-stat-media');
{% endif %}
{% if currentUser.featureEnabled("schedule.view") %}
fetchCount('{{ url_for("schedule.search") }}', 'ots-stat-schedules');
{% endif %}
});
})();
</script>
{% endblock %}

View File

@@ -42,10 +42,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter DataSets" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline" onsubmit="return false">
{% set title %}{% trans "Name" %}{% endset %}

View File

@@ -41,10 +41,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Display Groups" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}

View File

@@ -25,10 +25,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Fonts" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}

File diff suppressed because one or more lines are too long

View File

@@ -547,6 +547,7 @@ window.openUploadForm = function openUploadForm(options) {
data: formData,
processData: false,
contentType: false,
timeout: 120000,
xhr: function() {
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
@@ -762,6 +763,7 @@ window.openUploadForm = function openUploadForm(options) {
url: options.url,
type: 'POST',
data: postData,
timeout: 120000,
success: function(response) {
item.status = 'done';
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');

View File

@@ -164,7 +164,7 @@
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("layout.add") %}
<button class="btn btn-sm btn-success ots-toolbar-btn layout-add-button" title="{% trans "Add a new Layout and jump to the layout editor." %}" href="{{ url_for("layout.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-info ots-toolbar-btn" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-outline-secondary ots-toolbar-btn" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>

View File

@@ -42,10 +42,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Media" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}

View File

@@ -21,23 +21,6 @@
<link href="{{ theme.uri("css/override-dark.css") }}?{{ version }}" rel="stylesheet" media="screen">
</head>
<body class="login-page">
<!-- Fallback animated background element (inline styles ensure it appears even if external CSS is cached) -->
<div class="ots-login-bg" aria-hidden="true"></div>
<!-- Animated blurred color blobs -->
<div class="ots-login-blob ots-login-blob--1" aria-hidden="true"></div>
<div class="ots-login-blob ots-login-blob--2" aria-hidden="true"></div>
<div class="ots-login-blob ots-login-blob--3" aria-hidden="true"></div>
<style>
.ots-login-bg{position:fixed;inset:0;z-index:0;pointer-events:none;filter:blur(20px);opacity:0.95;
background: linear-gradient(120deg, rgba(14,28,45,0.6), rgba(6,16,30,0.55)),
radial-gradient(circle at 10% 20%, rgba(79,140,255,0.06), transparent 10%),
radial-gradient(circle at 85% 80%, rgba(255,138,0,0.04), transparent 12%);
background-size:200% 200%,100% 100%,100% 100%;
animation: ots-login-bg-shift-inline 14s linear infinite;}
@keyframes ots-login-bg-shift-inline{0%{background-position:0% 50%,0 0,0 0}50%{background-position:100% 50%,0 0,0 0}100%{background-position:0% 50%,0 0,0 0}}
/* Ensure login card sits above fallback background */
.login-card{position:relative;z-index:2}
</style>
<div class="container">
{% if authCASEnabled %}
<form id="cas-login-form" class="login-card text-center" action="{{ url_for("cas.login") }}" method="post">
@@ -48,9 +31,11 @@
<p>{% trans %}Connect with the Central Authentication Server{% endtrans %}</p>
<div aria-live="polite">
{% for loginMessage in flash('cas_login_message') %}
<div class="alert alert-danger">{{ loginMessage }}</div>
<div class="alert alert-danger" role="alert">{{ loginMessage }}</div>
{% endfor %}
</div>
<p><button class="btn btn-signin" type="submit" name="logincas">{% trans %} CAS Login{% endtrans %}</button></p>
</form>
@@ -64,16 +49,20 @@
<p class="lead">{% trans "Please provide your credentials" %}</p>
<label for="username" class="sr-only">{% trans "User" %}</label>
<input id="username" class="form-control input-block-level" name="username" type="text" placeholder="{% trans "User" %}" autofocus autocomplete="username">
<label for="password" class="sr-only">{% trans "Password" %}</label>
<input id="password" class="form-control input-block-level" name="password" type="password" placeholder="{% trans "Password" %}" autocomplete="current-password">
<div aria-live="polite">
{% for loginMessage in flash('login_message') %}
<div class="alert alert-danger">{{ loginMessage }}</div>
<div class="alert alert-danger" role="alert">{{ loginMessage }}</div>
{% endfor %}
</div>
<p><button class="btn btn-signin" type="submit">{% trans "Login" %}</button></p>
{% if passwordReminderEnabled %}<p><a href="#" id="reminder-form-toggle">{% trans "Forgotten your password?" %}</a></p>{% endif %}
{% if passwordReminderEnabled %}<p><a href="#" id="reminder-form-toggle" role="button">{% trans "Forgotten your password?" %}</a></p>{% endif %}
</form>
{% endif %}
@@ -83,15 +72,18 @@
<p class="login-brand"><a href="{{ theme.getThemeConfig("theme_url") }}"><img class="login-logo" src="{{ theme.uri("img/xibologo.png") }}"></a><span class="login-brand-text">OTS Signs</span></p>
<p>{% trans "Please provide your user name" %}</p>
<input id="username" class="form-control input-block-level" name="username" type="text" placeholder="{% trans "User" %}">
<label for="reminder-username" class="sr-only">{% trans "User" %}</label>
<input id="reminder-username" class="form-control input-block-level" name="username" type="text" placeholder="{% trans "User" %}">
<div aria-live="polite">
{% for loginMessage in flash('login_message') %}
<div class="alert alert-danger">{{ loginMessage }}</div>
<div class="alert alert-danger" role="alert">{{ loginMessage }}</div>
{% endfor %}
</div>
<p><button class="btn btn-signin" type="submit">{% trans "Send Reset" %}</button></p>
<p><a href="#" id="login-form-toggle">{% trans "Login instead?" %}</a></p>
<p><a href="#" id="login-form-toggle" role="button">{% trans "Login instead?" %}</a></p>
</form>
{% endif %}
@@ -101,18 +93,22 @@
<script src="{{ theme.rootUri() }}dist/vendor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script type="text/javascript" nonce="{{ cspNonce }}">
$(function() {
$("#reminder-form-toggle").on("click", function (e) {
$("#reminder-form-toggle").on("click keydown", function (e) {
if (e.type === "keydown" && e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
$("#login-form").addClass("d-none");
$("#reminder-form").removeClass("d-none");
$("#reminder-form").find("input:first").focus();
});
$("#login-form-toggle").on("click", function (e) {
$("#login-form-toggle").on("click keydown", function (e) {
if (e.type === "keydown" && e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
$("#login-form").removeClass("d-none");
$("#reminder-form").addClass("d-none");
$("#login-form").find("input:first").focus();
});
});
</script>

View File

@@ -26,7 +26,16 @@
--color-text-inverse: #ffffff;
--color-on-primary: #ffffff;
--ots-sidebar-content-gap: 12px;
/* ── Dashboard stat tile tokens ── */
--stat-tile-bg: var(--color-surface);
--stat-tile-border: var(--color-border);
--stat-tile-number-color: var(--color-text-primary);
--stat-tile-label-color: var(--color-text-tertiary);
--stat-tile-icon-color: var(--color-primary);
--stat-tile-hover-border: rgba(var(--ots-primary-rgb, 59, 130, 246), 0.4);
--stat-tile-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color-scheme: dark;
}
@@ -4218,13 +4227,49 @@ textarea:focus {
============================================================================= */
#help-pane {
background: var(--ots-surface-2);
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
#help-pane .help-pane-container {
position: relative;
width: 400px;
flex-direction: column;
align-items: flex-start;
border-radius: 4px;
border: 1px solid var(--ots-border);
background: var(--ots-surface-2);
overflow: hidden;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
#help-pane .help-pane-btn {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50px;
background: var(--ots-primary);
color: #0b1020;
cursor: pointer;
}
#help-pane .help-pane-btn:hover {
background: var(--ots-primary-2);
}
#help-pane .help-pane-btn i {
font-size: 24px;
color: #fff;
}
/* =============================================================================
@@ -7129,3 +7174,150 @@ body.ots-light-mode .calendar-view .cal-day-today .cal-month-day-number {
gap: 10px;
}
}
/* =============================================================================
DASHBOARD STAT BAR
============================================================================= */
.ots-stat-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.ots-stat-tile {
display: flex;
align-items: center;
gap: 14px;
padding: 18px 20px;
background: var(--stat-tile-bg);
border: 1px solid var(--stat-tile-border);
border-radius: 10px;
box-shadow: var(--stat-tile-shadow);
text-decoration: none !important;
color: inherit !important;
transition: border-color 200ms ease, box-shadow 200ms ease;
}
.ots-stat-tile:hover {
border-color: var(--stat-tile-hover-border);
}
.ots-stat-tile-icon {
flex-shrink: 0;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 18px;
background: rgba(59, 130, 246, 0.12);
color: var(--stat-tile-icon-color);
}
.ots-stat-tile-content {
display: flex;
flex-direction: column;
min-width: 0;
}
.ots-stat-tile-number {
font-size: 22px;
font-weight: 700;
line-height: 1.2;
color: var(--stat-tile-number-color);
}
.ots-stat-tile-label {
font-size: 12px;
font-weight: 500;
color: var(--stat-tile-label-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ots-stat-tile-icon--blue { background: rgba(50, 110, 220, 0.14); color: #5c9bff; }
.ots-stat-tile-icon--green { background: rgba(22, 175, 120, 0.14); color: #2eb88a; }
.ots-stat-tile-icon--orange { background: rgba(232, 120, 0, 0.14); color: #e87800; }
.ots-stat-tile-icon--purple { background: rgba(145, 80, 220, 0.14); color: #b37dd9; }
/* Light-mode overrides for stat tiles */
body.ots-light-mode .ots-stat-tile {
background: #ffffff;
border-color: #e2e8f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
body.ots-light-mode .ots-stat-tile-number { color: #0f172a; }
body.ots-light-mode .ots-stat-tile-label { color: #64748b; }
/* =============================================================================
ICON DASHBOARD section/grid/card-desc styles
(card layout and icon-colour variants live in dashboard-icon-page.twig inline <style>)
============================================================================= */
.icon-dash-section {
margin-top: 32px;
}
.icon-dash-section:first-of-type {
margin-top: 24px;
}
.icon-dash-section details {
border: none;
}
.icon-dash-section details summary {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 8px;
user-select: none;
}
.icon-dash-section details summary::-webkit-details-marker {
display: none;
}
.icon-dash-section details summary::after {
content: '';
width: 18px;
height: 18px;
margin-left: auto;
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 7L9 11L13 7' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
transition: transform 200ms ease;
}
.icon-dash-section details[open] summary::after {
transform: rotate(180deg);
}
.icon-dash-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 18px;
margin-top: 14px;
}
.icon-dash-card-desc {
font-size: 12px;
color: var(--color-text-tertiary);
margin-top: 2px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Override the generic .card/.dashboard-card flex-direction:column that
breaks the row layout of icon-dash-card items */
.icon-dash-card.dashboard-card {
flex-direction: row !important;
align-items: center !important;
overflow: visible !important;
}

View File

@@ -6,15 +6,22 @@
... inner content ...
{% endblock %}
{% endembed %}
Optional blocks: badge, description, actions
These are additive — existing embeds that only use `body` are unaffected.
#}
<div class="dashboard-card {{ classes|default('') }}" {% if id is defined %}id="{{ id }}"{% endif %}>
{% if title is defined and title %}
<div class="dashboard-card-header">
{{ title|raw }}
{% block badge %}{% endblock %}
</div>
{% endif %}
<div class="dashboard-card-body">
{% block body %}{% endblock %}
{% block description %}{% endblock %}
</div>
{% block actions %}{% endblock %}
</div>

View File

@@ -41,10 +41,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Player Versions" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "Type" %}{% endset %}

View File

@@ -39,10 +39,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Playlists" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab"><span>{% trans "General" %}</span></a></li>

View File

@@ -280,42 +280,38 @@
</div>
</div>
<div class="tab-pane" id="calendar-view">
<div class="row">
{# ── OTS FullCalendar container (replaces legacy CalendarContainer visually) ── #}
<div id="ots-fullcalendar"></div>
{# Legacy container kept hidden so Xibo bundle doesn't error.
NOTE: id="Calendar" is intentionally omitted to prevent the Xibo bundle
from initialising the legacy calendar and throwing _loadEvents errors.
The data-agenda-link is preserved on CalendarContainer for our event-click handler. #}
<div class="row d-none" id="ots-legacy-calendar-row">
<div id="CalendarContainer"
data-agenda-link="{{ url_for("schedule.events", {id: ':id'}) }}"
data-calendar-type="{{ settings.CALENDAR_TYPE }}" class="col-sm-12"
data-default-lat="{{ defaultLat }}"
data-default-long="{{ defaultLong }}">
<div class="calendar-view" id="Calendar"></div>
<div class="calendar-view" id="ots-legacy-calendar-stub"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="cal-legend">
<ul>
<li class="event-always"><span
class="fa fa-retweet"></span> {% trans "Always showing" %}</li>
<li class="event-info"><span
class="fa fa-desktop"></span> {% trans "Single Display" %}</li>
<li class="event-success"><span
class="fa fa-desktop"></span> {% trans "Multi Display" %}</li>
<li class="event-important"><span
class="fa fa-bullseye"></span> {% trans "Priority" %}</li>
<li class="event-special"><span
class="fa fa-repeat"></span> {% trans "Recurring" %}</li>
<li class="event-inverse"><span
class="fa fa-lock"></span> {% trans "View Only" %}</li>
<li class="event-command"><span
class="fa fa-wrench"></span> {% trans "Command" %}</li>
<li class="event-interrupt"><span
class="fa fa-hand-paper"></span> {% trans "Interrupt" %}</li>
<li class="event-geo-location"><span
class="fa fa-map-marker"></span> {% trans "Geo Location" %}</li>
<li class="event-action"><span
class="fa fa-paper-plane "></span> {% trans "Interactive Action" %}
</li>
<li class="event-sync"><span
class="fa fa-refresh"></span> {% trans "Synchronised" %}</li>
<li><span class="fa fa-retweet" style="color:#6366f1"></span> {% trans "Always showing" %}</li>
<li><span class="fa fa-desktop" style="color:#0ea5e9"></span> {% trans "Single Display" %}</li>
<li><span class="fa fa-desktop" style="color:#10b981"></span> {% trans "Multi Display" %}</li>
<li><span class="fa fa-bullseye" style="color:#ef4444"></span> {% trans "Priority" %}</li>
<li><span class="fa fa-repeat" style="color:#8b5cf6"></span> {% trans "Recurring" %}</li>
<li><span class="fa fa-lock" style="color:#64748b"></span> {% trans "View Only" %}</li>
<li><span class="fa fa-wrench" style="color:#f59e0b"></span> {% trans "Command" %}</li>
<li><span class="fa fa-hand-paper" style="color:#f97316"></span> {% trans "Interrupt" %}</li>
<li><span class="fa fa-map-marker" style="color:#14b8a6"></span> {% trans "Geo Location" %}</li>
<li><span class="fa fa-paper-plane" style="color:#ec4899"></span> {% trans "Interactive Action" %}</li>
<li><span class="fa fa-refresh" style="color:#06b6d4"></span> {% trans "Synchronised" %}</li>
</ul>
</div>
</div>
@@ -356,14 +352,211 @@
{# Add page source code bundle ( JS ) #}
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/pages/schedule-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{# ── FullCalendar v6 (inlined to bypass MIME issues with /custom/ paths) ── #}
<script nonce="{{ cspNonce }}">
{% include "fullcalendar-lib.twig" %}
</script>
{# ── OTS FullCalendar integration ── #}
<script nonce="{{ cspNonce }}">
$(function() {
// OTS calendar nav arrows drive FullCalendar directly (do NOT proxy to
// Xibo's data-calendar-nav buttons — those trigger the legacy calendar
// and cause _loadEvents / options.events errors).
$('#ots-cal-prev').on('click', function() {
$('button[data-calendar-nav="prev"]').trigger('click');
if (otsCalendar) { otsCalendar.prev(); }
});
$('#ots-cal-next').on('click', function() {
$('button[data-calendar-nav="next"]').trigger('click');
if (otsCalendar) { otsCalendar.next(); }
});
// ── FullCalendar initialisation ──
var otsCalendar = null;
var calendarTabActive = false;
// Map Xibo event data to FullCalendar event object
function mapEvent(item) {
var title = item.schedule || item.campaign || item.command || item.layout || 'Event #' + item.eventId;
var cls = 'fc-event-single';
// Priority flag overrides everything
if (item.isPriority && parseInt(item.isPriority, 10) > 0) {
cls = 'fc-event-priority';
}
// Command events
else if (parseInt(item.eventTypeId, 10) === 2) {
cls = 'fc-event-command';
}
// Interrupt events
else if (parseInt(item.eventTypeId, 10) === 4) {
cls = 'fc-event-interrupt';
}
// Action events
else if (parseInt(item.eventTypeId, 10) === 6) {
cls = 'fc-event-action';
}
// "Always" daypart
else if (item.isAlways && parseInt(item.isAlways, 10) === 1) {
cls = 'fc-event-always';
}
// Geo-aware
else if (item.isGeoAware && parseInt(item.isGeoAware, 10) === 1) {
cls = 'fc-event-geo';
}
// Recurring
else if (item.recurrenceType && item.recurrenceType !== '' && item.recurrenceType !== 'null') {
cls = 'fc-event-recurring';
}
// Multi-display vs single
else if (item.displayGroups && item.displayGroups.length > 1) {
cls = 'fc-event-multi';
}
// Sync events
if (item.syncGroupId && parseInt(item.syncGroupId, 10) > 0) {
cls = 'fc-event-sync';
}
return {
id: item.eventId,
title: title,
start: item.fromDt,
end: item.toDt,
allDay: (item.isAlways && parseInt(item.isAlways, 10) === 1),
className: cls,
extendedProps: item
};
}
// Fetch events from Xibo schedule.search API
function fetchEvents(info, successCallback, failureCallback) {
var filterData = {
fromDt: info.startStr,
toDt: info.endStr
};
// Pull in active filter values
var $filter = $('#schedule-filter');
if ($filter.length) {
$filter.find(':input[name]').each(function() {
var $el = $(this);
var name = $el.attr('name');
var val = $el.val();
if (val && val !== '' && name !== 'fromDt' && name !== 'toDt') {
filterData[name] = val;
}
});
}
$.ajax({
url: scheduleSearchUrl,
type: 'GET',
dataType: 'json',
data: filterData,
success: function(response) {
var events = [];
var rows = response.data || response || [];
for (var i = 0; i < rows.length; i++) {
events.push(mapEvent(rows[i]));
}
successCallback(events);
},
error: function(xhr) {
console.warn('OTS: FullCalendar event fetch failed', xhr.status);
failureCallback(xhr);
}
});
}
// Handle event click → open Xibo edit form
function handleEventClick(info) {
var ev = info.event;
var props = ev.extendedProps || {};
if (props.eventId) {
var url = $('#CalendarContainer').data('agenda-link');
if (url) {
url = url.replace(':id', props.eventId);
XiboFormRender(url);
}
}
}
// Initialise FullCalendar on first Calendar-tab show
function initFullCalendar() {
if (otsCalendar) return;
var el = document.getElementById('ots-fullcalendar');
if (!el || typeof FullCalendar === 'undefined') return;
otsCalendar = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
buttonText: {
today: '{{ "Today"|trans }}',
month: '{{ "Month"|trans }}',
week: '{{ "Week"|trans }}',
day: '{{ "Day"|trans }}',
list: '{{ "List"|trans }}'
},
firstDay: 1,
nowIndicator: true,
navLinks: true,
editable: false,
selectable: false,
eventDisplay: 'block',
dayMaxEvents: 4,
height: 'auto',
events: fetchEvents,
eventClick: handleEventClick,
loading: function(isLoading) {
$('#calendar-progress').toggle(isLoading);
}
});
otsCalendar.render();
}
// Init when Calendar tab is shown
$('a[data-toggle="tab"][href="#calendar-view"]').on('shown.bs.tab', function() {
calendarTabActive = true;
initFullCalendar();
// Hide the Xibo calendar header (we use FC's built-in toolbar)
$('.xibo-calendar-header-container').hide();
});
// Re-show Xibo header when switching back to Grid; suspend FC interception
$('a[data-toggle="tab"][href="#grid-view"]').on('shown.bs.tab', function() {
$('.xibo-calendar-header-container').show();
calendarTabActive = false;
});
// Intercept Xibo's data-calendar-nav buttons when calendar tab is active
// and drive FullCalendar directly, preventing the legacy calendar from being invoked.
$(document).on('click', '[data-calendar-nav]', function(e) {
if (!calendarTabActive || !otsCalendar) return; // Grid tab active — let Xibo handle it
e.stopImmediatePropagation();
var nav = $(this).data('calendar-nav');
if (nav === 'prev') { otsCalendar.prev(); }
else if (nav === 'next') { otsCalendar.next(); }
else if (nav === 'today') { otsCalendar.today(); }
});
// Refetch events when filter changes
$('#schedule-filter').on('change', ':input', function() {
if (otsCalendar) {
otsCalendar.refetchEvents();
}
});
// If Calendar tab is active by default (URL hash), init immediately
if (window.location.hash === '#calendar-view' || $('#calendar-tab').hasClass('active')) {
calendarTabActive = true;
setTimeout(initFullCalendar, 100);
}
});
</script>
{% endblock %}

View File

@@ -25,10 +25,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Tags" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}

View File

@@ -78,6 +78,9 @@
updatePlaylistEditorBackground();
});
editorObs.observe(target, { childList: true, subtree: true });
// Store reference for cleanup
window._otsEditorObs = editorObs;
})();
/**
@@ -210,7 +213,8 @@
}
if (collapseBtn) {
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
let isCollapsed = false;
try { isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true'; } catch(e) {}
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
@@ -225,7 +229,7 @@
sidebar.classList.toggle('collapsed');
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); } catch(e) {}
syncSubmenuDisplayForState(nowCollapsed);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -241,7 +245,7 @@
sidebar.classList.remove('collapsed');
body.classList.remove('ots-sidebar-collapsed');
document.documentElement.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false'); } catch(e) {}
syncSubmenuDisplayForState(false);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -372,6 +376,43 @@
});
}
/**
* Arrow key navigation within sidebar submenus.
* Up/Down moves between links, Escape collapses the group.
*/
function initSidebarKeyboardNav() {
var nav = document.querySelector('.ots-sidebar nav, .ots-sidebar [role="navigation"]');
if (!nav) return;
nav.addEventListener('keydown', function(e) {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Escape') return;
var submenu = e.target.closest('.sidebar-submenu');
if (!submenu) return;
var links = submenu.querySelectorAll('a:not([disabled])');
if (!links.length) return;
if (e.key === 'Escape') {
e.preventDefault();
var group = submenu.closest('.sidebar-group');
var toggle = group ? group.querySelector('.sidebar-group-toggle') : null;
if (toggle) toggle.click();
if (toggle) toggle.focus();
return;
}
e.preventDefault();
var idx = Array.prototype.indexOf.call(links, e.target);
if (e.key === 'ArrowDown') {
idx = idx < links.length - 1 ? idx + 1 : 0;
} else {
idx = idx > 0 ? idx - 1 : links.length - 1;
}
links[idx].focus();
});
}
/**
* Initialize dropdown menus
*/
@@ -744,56 +785,30 @@
});
});
// Continuously guard: check that menus stay in body and have correct styles
let guardInterval = setInterval(function() {
try {
selectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
// If menu got moved back, move it to body again
if (el.parentElement !== document.body && el.parentElement !== null) {
document.body.appendChild(el);
}
// Reapply critical styles in case they got overridden
applyMenuStyles(el);
});
});
} catch (err) {}
}, 200);
// Keep guard alive for the page lifetime, but stop if no menus found after 30s
let noMenuCount = 0;
const checkGuard = setInterval(function() {
const hasMenus = selectors.some(sel => document.querySelector(sel));
if (!hasMenus) {
noMenuCount++;
if (noMenuCount > 150) {
clearInterval(guardInterval);
clearInterval(checkGuard);
}
} else {
noMenuCount = 0;
}
}, 200);
// Observe for dynamically added menus
// Use a MutationObserver instead of polling to detect new menus
try {
const mo = new MutationObserver(function(muts) {
muts.forEach(m => {
(m.addedNodes || []).forEach(node => {
var forceMenuObs = new MutationObserver(function(muts) {
muts.forEach(function(m) {
(m.addedNodes || []).forEach(function(node) {
try {
if (!node || node.nodeType !== 1) return;
selectors.forEach(sel => {
selectors.forEach(function(sel) {
if (node.matches && node.matches(sel)) {
moveToBody(node);
applyMenuStyles(node);
}
const found = node.querySelectorAll && node.querySelectorAll(sel);
found && found.forEach(moveToBody);
var found = node.querySelectorAll && node.querySelectorAll(sel);
if (found) found.forEach(function(el) {
moveToBody(el);
applyMenuStyles(el);
});
});
} catch (err) {}
});
});
});
mo.observe(document.body, { childList: true, subtree: true });
forceMenuObs.observe(document.body, { childList: true, subtree: true });
window._otsForceMenuObs = forceMenuObs;
} catch (err) {}
}
@@ -834,7 +849,7 @@
if (filterCollapseBtn && filterContent) {
const storageKey = `ots-filter-collapsed:${window.location.pathname}`;
let isCollapsed = false;
let isCollapsed = filterContent.classList.contains('collapsed');
filterCollapseBtn.addEventListener('click', function() {
isCollapsed = !isCollapsed;
@@ -842,23 +857,28 @@
// Rotate icon
const icon = filterCollapseBtn.querySelector('i');
icon.classList.toggle('fa-chevron-up');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up', !isCollapsed);
icon.classList.toggle('fa-chevron-down', isCollapsed);
// Save preference to localStorage
localStorage.setItem(storageKey, isCollapsed);
try { localStorage.setItem(storageKey, isCollapsed); } catch(e) {}
});
// Restore saved preference
const savedState = localStorage.getItem(storageKey);
// Restore saved preference (overrides HTML default)
let savedState = null;
try { savedState = localStorage.getItem(storageKey); } catch(e) {}
if (savedState === 'true') {
isCollapsed = true;
filterContent.classList.add('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
} else {
} else if (savedState === 'false') {
isCollapsed = false;
filterContent.classList.remove('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
}
}
@@ -871,27 +891,17 @@
let debounceTimeout;
const syncFolderLayout = () => {
// Check actual visibility using computed styles
const computedStyle = window.getComputedStyle(folderTree);
const isHidden = computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
folderTree.offsetHeight === 0;
console.log('Folder collapse sync:', {
isHidden,
display: computedStyle.display,
visibility: computedStyle.visibility,
offsetHeight: folderTree.offsetHeight
requestAnimationFrame(() => {
// Batch all reads first
const computedStyle = window.getComputedStyle(folderTree);
const display = computedStyle.display;
const visibility = computedStyle.visibility;
const height = folderTree.offsetHeight;
const isHidden = display === 'none' || visibility === 'hidden' || height === 0;
// Then write
folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
});
folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
// Log the result
console.log('Container classes:', folderContainer.className);
console.log('Grid template columns:', window.getComputedStyle(folderContainer).gridTemplateColumns);
// Force reflow
folderContainer.offsetHeight;
};
const debouncedSync = () => {
@@ -901,7 +911,6 @@
// Watch for style/class changes on folderTree (let Xibo's code run first)
const treeObserver = new MutationObserver(() => {
console.log('Folder tree mutation detected, debouncing sync...');
debouncedSync();
});
treeObserver.observe(folderTree, {
@@ -1055,6 +1064,55 @@
const $ = window.jQuery;
if (!$.fn || !$.fn.dataTable) return;
// ── Override global DataTables template for modern layout ──
// Only if Xibo hasn't already set a custom template
if (typeof window.dataTablesTemplate === 'string') {
// Keep Xibo's template but ensure it includes our improvements
} else if (typeof window.dataTablesTemplate === 'undefined') {
window.dataTablesTemplate = '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>rt<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>';
}
// ── Enhance Xibo DataTables on draw ──
$(document).on('draw.dt', function(e, settings) {
try {
var api = new $.fn.dataTable.Api(settings);
var wrapper = $(api.table().container());
var tbody = $(api.table().body());
// Inject custom empty state when table has no visible rows
if (api.rows({ search: 'applied' }).count() === 0) {
var existing = wrapper.find('.ots-table-empty-state');
if (!existing.length) {
var emptyHtml = '<div class="ots-table-empty-state">' +
'<div class="ots-empty-icon"><i class="fa fa-inbox"></i></div>' +
'<div class="ots-empty-text">No results found</div>' +
'<div class="ots-empty-hint">Try adjusting your filters or search terms</div>' +
'</div>';
// Insert after the table, before pagination
var tableEl = $(api.table().node());
tableEl.after(emptyHtml);
}
} else {
wrapper.find('.ots-table-empty-state').remove();
}
// Apply status badge styling to known status cells
tbody.find('td .label, td .badge').each(function() {
var el = $(this);
if (el.hasClass('ots-badge')) return;
var text = (el.text() || '').toLowerCase().trim();
var cls = 'ots-badge ots-badge--neutral';
if (/online|active|enabled|yes|licensed|authorised/.test(text)) cls = 'ots-badge ots-badge--success';
else if (/offline|inactive|disabled|no|expired|revoked/.test(text)) cls = 'ots-badge ots-badge--danger';
else if (/pending|waiting|unknown|checking/.test(text)) cls = 'ots-badge ots-badge--warning';
else if (/edit|draft|building/.test(text)) cls = 'ots-badge ots-badge--info';
el.addClass(cls);
});
} catch (err) {
// DataTable enhancement failure is non-critical
}
});
// Skip Xibo-managed grids to avoid double initialization
if (document.querySelector('.XiboGrid')) return;
@@ -1103,7 +1161,7 @@
e.preventDefault();
const isLight = body.classList.toggle('ots-light-mode');
root.classList.toggle('ots-light-mode', isLight);
localStorage.setItem('ots-theme-mode', isLight ? 'light' : 'dark');
try { localStorage.setItem('ots-theme-mode', isLight ? 'light' : 'dark'); } catch(e) {}
updateThemeLabel();
});
@@ -1244,10 +1302,26 @@
}
}, true); // ← true = capture phase
// Close on Escape key
// Close on Escape key + arrow navigation within open menus
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && activeMenu) {
closeMenu();
return;
}
if (!activeMenu) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
var items = activeMenu.querySelectorAll('.dropdown-item:not(.disabled):not([disabled])');
if (!items.length) return;
var idx = Array.prototype.indexOf.call(items, document.activeElement);
if (e.key === 'ArrowDown') {
idx = idx < items.length - 1 ? idx + 1 : 0;
} else {
idx = idx > 0 ? idx - 1 : items.length - 1;
}
items[idx].focus();
}
});
@@ -1390,6 +1464,7 @@
function init() {
initSidebarToggle();
initSidebarSectionToggles();
initSidebarKeyboardNav();
buildFlyoutHeaders();
initThemeToggle();
initDropdowns();
@@ -1552,3 +1627,31 @@ function initUserProfileQrFix() {
if (checks > 12) clearInterval(interval);
}, 500);
}
/**
* OTS: Help pane fallback click handler.
* The core help-pane.js may fail to render templates in white-label themes
* (isXiboThemed = false). This handler catches the click and opens the
* help landing page directly if the core handler didn't show the container.
*/
(function() {
'use strict';
var btn = document.querySelector('#help-pane .help-pane-btn');
if (!btn) return;
btn.addEventListener('click', function() {
// Give the core handler 150ms to show the container
setTimeout(function() {
var container = document.querySelector('#help-pane .help-pane-container');
if (container && container.offsetHeight > 0 && container.innerHTML.trim() !== '') {
return; // Core handler worked — do nothing
}
// Fallback: open the help landing page directly
var pane = document.getElementById('help-pane');
var url = pane && pane.getAttribute('data-url-help-landing-page');
if (url) {
window.open(url, '_blank', 'noopener');
}
}, 150);
});
})();

View File

@@ -25,10 +25,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Users" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "Username" %}{% endset %}

View File

@@ -25,10 +25,10 @@
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter User Groups" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
<i class="fa fa-chevron-down"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %}