pre-img swap
This commit is contained in:
@@ -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'
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
6
ots-signs/js/fullcalendar.global.min.js
vendored
Normal file
6
ots-signs/js/fullcalendar.global.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }}">
|
||||
|
||||
@@ -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") %}
|
||||
|
||||
@@ -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) {}
|
||||
})();
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
6
ots-signs/views/fullcalendar-lib.twig
Normal file
6
ots-signs/views/fullcalendar-lib.twig
Normal file
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user