Refactor filter panels and enhance sidebar functionality
- Updated filter panel toggle icons from chevron-up to chevron-down across multiple pages for consistency. - Added 'collapsed' class to filter content divs to manage visibility state. - Enhanced library page button for tidying up media items, replacing the trash icon with a custom SVG broom icon. - Improved CSS styles for sidebar and page header to ensure visibility and proper layout when the sidebar is collapsed. - Introduced JavaScript functionality to manage sidebar width and state, including theme toggle for light/dark mode. - Created a new notification drawer template that adapts based on the compact view state.
This commit is contained in:
26
custom/otssignange/views/authed-notification-drawer.twig
Normal file
26
custom/otssignange/views/authed-notification-drawer.twig
Normal file
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
Compact-aware notification drawer override
|
||||
#}
|
||||
{% if compact is defined and compact %}
|
||||
<div class="dropdown nav-item item ots-notif-compact">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarNotificationDrawer">
|
||||
<span class="ots-topbar-icon fa fa-bell" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-notif-menu" aria-labelledby="navbarNotificationDrawer">
|
||||
<div class="dropdown-header">Notifications</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">No new notifications</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<li class="dropdown nav-item item">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarNotificationDrawer">
|
||||
<span class="ots-topbar-icon fa fa-bell" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-notif-menu" aria-labelledby="navbarNotificationDrawer">
|
||||
<div class="dropdown-header">Notifications</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">No new notifications</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -11,7 +11,10 @@
|
||||
</span>
|
||||
<span class="brand-text">OTS Signs</span>
|
||||
</a>
|
||||
<button class="sidebar-collapse-btn" type="button" aria-label="{% trans "Collapse sidebar" %}">
|
||||
<button class="sidebar-expand-btn" type="button" aria-label="{% trans "Expand sidebar" %}">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="sidebar-collapse-btn sidebar-collapse-btn-visible" type="button" aria-label="{% trans "Collapse sidebar" %}">
|
||||
<i class="fa fa-chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,4 @@
|
||||
Optional include rendered in authed.twig (top right navbar)
|
||||
Minimal, low-risk addition for verification
|
||||
#}
|
||||
<li class="nav-item ots-theme-badge">
|
||||
<span class="nav-link">OTS Theme</span>
|
||||
</li>
|
||||
{# OTS topbar badge removed #}
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
Based on Xibo CMS default authed-user-menu.twig (master branch)
|
||||
Minimal change: add ots-user-menu class for easy verification
|
||||
#}
|
||||
{% if compact is defined and compact %}
|
||||
<div class="dropdown nav-item item ots-user-menu-compact">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
|
||||
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
|
||||
{% else %}
|
||||
<li class="dropdown nav-item item">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
|
||||
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
|
||||
{% endif %}
|
||||
<h6 class="dropdown-header">{{ currentUser.userName }}<br/>
|
||||
<div id="XiboClock">{{ clock }}</div>
|
||||
</h6>
|
||||
@@ -21,6 +29,10 @@
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.applications") }}" title="{% trans "View my authenticated applications" %}">{% trans "My Applications" %}</a>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" id="ots-theme-toggle" href="#" title="Toggle light/dark mode">
|
||||
<i class="fa fa-moon-o" id="ots-theme-icon" aria-hidden="true"></i>
|
||||
<span id="ots-theme-label">Dark Mode</span>
|
||||
</a>
|
||||
<a class="dropdown-item" id="reshowWelcomeMenuItem" href="{{ url_for("welcome.view") }}">{% trans "Reshow welcome" %}</a>
|
||||
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("about") }}" title="{% trans "About the CMS" %}">{% trans "About" %}</a>
|
||||
@@ -30,4 +42,9 @@
|
||||
<a class="dropdown-item" title="{% trans "Logout" %}" href="{{ logoutUrl }}">{% trans "Logout" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if compact is defined and compact %}
|
||||
</div>
|
||||
{% else %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -22,6 +22,50 @@
|
||||
#}
|
||||
{% extends "base.twig" %}
|
||||
|
||||
{% block headContent %}
|
||||
<script nonce="{{ cspNonce }}">
|
||||
(function(){
|
||||
try{
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var mode = stored || (prefersLight ? 'light' : 'light');
|
||||
if(mode === 'light') document.documentElement.classList.add('ots-light-mode');
|
||||
else document.documentElement.classList.remove('ots-light-mode');
|
||||
}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
// Apply collapsed sidebar state early to prevent header flashing
|
||||
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');
|
||||
// Also set the CSS variable used for collapsed width so layout shifts correctly
|
||||
try {
|
||||
var v = getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-collapsed-width') || '64px';
|
||||
document.documentElement.style.setProperty('--ots-sidebar-width', v);
|
||||
} catch(e){}
|
||||
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) {}
|
||||
})();
|
||||
|
||||
</script>
|
||||
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
|
||||
/* Hide the top header row immediately when sidebar is collapsed to prevent flash */
|
||||
html.ots-sidebar-collapsed .row.header.header-side,
|
||||
body.ots-sidebar-collapsed .row.header.header-side,
|
||||
.ots-sidebar.collapsed ~ .ots-main .row.header.header-side,
|
||||
.ots-sidebar.collapsed .row.header.header-side { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set horizontalNav = currentUser.getOptionValue("navigationMenuPosition", theme.getSetting("NAVIGATION_MENU_POSITION", "vertical")) == "horizontal" %}
|
||||
|
||||
@@ -36,7 +80,10 @@
|
||||
<nav class="navbar navbar-default navbar-expand-lg">
|
||||
<a class="navbar-brand xibo-logo-container" href="#">
|
||||
<img class="xibo-logo" src="{{ theme.uri("img/xibologo.png") }}">
|
||||
<span class="xibo-logo-text">Xibo</span>
|
||||
<span class="xibo-logo-text">
|
||||
<span class="brand-line brand-line-top">OTS</span>
|
||||
<span class="brand-line brand-line-bottom">Signs</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
@@ -78,14 +125,16 @@
|
||||
<span class="fa fa-bars"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="user pull-right">
|
||||
{% include "authed-user-menu.twig" %}
|
||||
<div class="user-actions pull-right">
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
<div class="user-notif">
|
||||
{% include "authed-notification-drawer.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="user">
|
||||
{% include "authed-user-menu.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
</div>
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
<div class="user user-notif pull-right">
|
||||
{% include "authed-notification-drawer.twig" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "authed-theme-topbar.twig" ignore missing %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Campaigns" %}</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 %}
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Commands" %}</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 %}
|
||||
|
||||
@@ -1,52 +1,123 @@
|
||||
/* High-specificity DataTables contrast overrides
|
||||
Ensures table body text is readable against dark theme backgrounds.
|
||||
Light text on dark backgrounds.
|
||||
Light text on dark backgrounds (dark mode).
|
||||
Dark text on light backgrounds (light mode).
|
||||
*/
|
||||
|
||||
/* FIRST: Light mode rules that check actual background colors (not dependent on body class) */
|
||||
#datatable-container table.dataTable tbody td,
|
||||
#datatable-container .dataTables_wrapper table.dataTable tbody td,
|
||||
.ots-table-card table.dataTable tbody td,
|
||||
.ots-table-card table.dataTable tbody td * {
|
||||
color: #f5f5f5 !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable thead th,
|
||||
.ots-table-card table.dataTable thead th,
|
||||
#datatable-container table.dataTable thead th * {
|
||||
color: #ffffff !important;
|
||||
color: var(--color-text-secondary) !important;
|
||||
opacity: 1 !important;
|
||||
background-color: rgba(0,0,0,0.3) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr.table-success td,
|
||||
#datatable-container table.dataTable tbody tr.success td,
|
||||
#datatable-container table.dataTable tbody tr.selected td,
|
||||
#datatable-container table.dataTable tbody tr.highlight td {
|
||||
background-color: rgba(255,255,255,0.08) !important;
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody td .btn,
|
||||
#datatable-container table.dataTable tbody td .badge,
|
||||
#datatable-container table.dataTable tbody td .dropdown-toggle {
|
||||
color: #ffffff !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr {
|
||||
background-color: rgba(0,0,0,0.1) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr:nth-child(even) {
|
||||
background-color: var(--color-surface-elevated) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr:hover {
|
||||
background-color: rgba(255,255,255,0.05) !important;
|
||||
background-color: rgba(37, 99, 235, 0.06) !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input,
|
||||
.dataTables_wrapper .dataTables_length select,
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
color: #f5f5f5 !important;
|
||||
background: rgba(0,0,0,0.2) !important;
|
||||
border-color: rgba(255,255,255,0.1) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
background: var(--color-surface) !important;
|
||||
border-color: var(--color-border) !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_filter,
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
/* SECOND: Explicit light mode class overrides for when .ots-light-mode is present */
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td,
|
||||
body.ots-light-mode #datatable-container .dataTables_wrapper table.dataTable tbody td,
|
||||
body.ots-light-mode .ots-table-card table.dataTable tbody td,
|
||||
body.ots-light-mode .ots-table-card table.dataTable tbody td * {
|
||||
color: #0f172a !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable thead th,
|
||||
body.ots-light-mode .ots-table-card table.dataTable thead th,
|
||||
body.ots-light-mode #datatable-container table.dataTable thead th * {
|
||||
color: #334155 !important;
|
||||
opacity: 1 !important;
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.table-success td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.success td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.selected td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.highlight td {
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .btn,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .badge,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .dropdown-toggle {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr:nth-child(even) {
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr:hover {
|
||||
background-color: rgba(37, 99, 235, 0.06) !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_filter input,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_length select,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
color: #0f172a !important;
|
||||
background: #ffffff !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_info,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_filter,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_length,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_paginate {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody td img,
|
||||
@@ -62,5 +133,6 @@
|
||||
|
||||
.ots-table-card table.dataTable tbody tr td,
|
||||
.ots-table-card table.dataTable tbody tr td * {
|
||||
-webkit-text-fill-color: #f5f5f5 !important;
|
||||
-webkit-text-fill-color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Dayparts" %}</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 %}
|
||||
|
||||
@@ -36,7 +36,45 @@
|
||||
|
||||
{% block headContent %}
|
||||
{# Add page source code bundle ( CSS ) #}
|
||||
<link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
|
||||
<script nonce="{{ cspNonce }}">
|
||||
(function(){
|
||||
try{
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var mode = stored || (prefersLight ? 'light' : 'light');
|
||||
if(mode === 'light') document.documentElement.classList.add('ots-light-mode');
|
||||
else document.documentElement.classList.remove('ots-light-mode');
|
||||
}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
// Apply collapsed sidebar state early to prevent header flashing
|
||||
try {
|
||||
var collapsed = localStorage.getItem('otsTheme:sidebarCollapsed');
|
||||
if (collapsed === 'true') {
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early (page):', collapsed); } catch(e){}
|
||||
document.documentElement.classList.add('ots-sidebar-collapsed');
|
||||
if (document.body) document.body.classList.add('ots-sidebar-collapsed');
|
||||
try {
|
||||
var v = getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-collapsed-width') || '64px';
|
||||
document.documentElement.style.setProperty('--ots-sidebar-width', v);
|
||||
} catch(e){}
|
||||
try { console.debug && console.debug('applied ots-sidebar-collapsed early (page)'); } catch(e){}
|
||||
} else {
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early (page): not set'); } catch(e){}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
|
||||
/* Hide the top header row immediately when sidebar is collapsed to prevent flash */
|
||||
html.ots-sidebar-collapsed .row.header.header-side,
|
||||
body.ots-sidebar-collapsed .row.header.header-side,
|
||||
.ots-sidebar.collapsed ~ .ots-main .row.header.header-side,
|
||||
.ots-sidebar.collapsed .row.header.header-side { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; }
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
@@ -53,10 +91,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Displays" %}</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="#filter-general" role="tab" data-toggle="tab">{% trans "General" %}</a></li>
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Display Settings" %}</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">
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Layouts" %}</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" aria-selected="true"><span>{% trans "General" %}</span></a></li>
|
||||
|
||||
@@ -33,7 +33,22 @@
|
||||
<button class="btn btn-icon btn-success XiboFormButton" title="{% trans "Add a new media item to the library via external URL" %}" href="{{ url_for("library.uploadUrl.form") }}"><i class="fa fa-link" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %}
|
||||
<button class="btn btn-icon btn-danger XiboFormButton" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-icon btn-warning XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}">
|
||||
<svg class="icon icon-broom-pantry" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<g fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- dustpan -->
|
||||
<path d="M3 6h6l2 6v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6z" fill="currentColor" opacity="0.08"/>
|
||||
<path d="M9 6v0" />
|
||||
<path d="M3.5 7.5L8 7.5" />
|
||||
<!-- broom handle -->
|
||||
<path d="M14 3l6 6-7 7" />
|
||||
<!-- bristles -->
|
||||
<path d="M11 14l4.5-4.5M12 15l5-5M13 16l5.5-5.5" />
|
||||
<!-- small hand grip accent -->
|
||||
<circle cx="14.5" cy="4.5" r="0.5" fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button class="btn btn-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Menu Boards" %}</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 %}
|
||||
|
||||
@@ -29,6 +29,78 @@
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Ensure the page title/description remain visible when the sidebar is collapsed */
|
||||
html.ots-sidebar-collapsed .page-header,
|
||||
body.ots-sidebar-collapsed .page-header,
|
||||
.ots-sidebar.collapsed ~ .ots-main .page-header,
|
||||
.ots-main .page-header {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
height: auto !important;
|
||||
padding-top: 16px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
html.ots-sidebar-collapsed .page-header h1,
|
||||
body.ots-sidebar-collapsed .page-header h1,
|
||||
.ots-sidebar.collapsed ~ .ots-main .page-header h1,
|
||||
.ots-main .page-header h1 {
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
html.ots-sidebar-collapsed .page-header p.text-muted,
|
||||
body.ots-sidebar-collapsed .page-header p.text-muted,
|
||||
.ots-sidebar.collapsed ~ .ots-main .page-header p.text-muted,
|
||||
.ots-main .page-header p.text-muted {
|
||||
color: var(--color-text-tertiary) !important;
|
||||
}
|
||||
|
||||
/* Ensure page header is not obscured by panels when sidebar is collapsed */
|
||||
.ots-main .page-header {
|
||||
position: relative !important;
|
||||
z-index: 2500 !important;
|
||||
margin-top: 8px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
/* Prevent immediate container clipping near the top */
|
||||
.ots-content,
|
||||
.ots-main,
|
||||
.page-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Fixed-position fallback: keep the page header visible and readable when sidebar is collapsed */
|
||||
@media (min-width: 992px) {
|
||||
html.ots-sidebar-collapsed .page-header,
|
||||
body.ots-sidebar-collapsed .page-header,
|
||||
.ots-sidebar.collapsed ~ .ots-main .page-header {
|
||||
position: fixed !important;
|
||||
top: 16px !important;
|
||||
left: calc(var(--ots-sidebar-width,64px) + 24px) !important;
|
||||
/* fallback positions in case the CSS variable isn't set early */
|
||||
left: 80px !important;
|
||||
left: 260px !important;
|
||||
right: 24px !important;
|
||||
z-index: 3200 !important;
|
||||
background: transparent !important;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* If sidebar is collapsed, prefer a smaller left offset */
|
||||
html.ots-sidebar-collapsed .page-header,
|
||||
body.ots-sidebar-collapsed .page-header {
|
||||
left: 80px !important;
|
||||
}
|
||||
|
||||
html.ots-sidebar-collapsed .page-header .ots-filter-header,
|
||||
body.ots-sidebar-collapsed .page-header .ots-filter-header {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
@@ -144,7 +216,15 @@ body {
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 12px 0 120px;
|
||||
padding: 72px 0 120px;
|
||||
}
|
||||
|
||||
/* Extra top padding when sidebar is collapsed or expanded so items clear header */
|
||||
.ots-sidebar.collapsed .sidebar-nav,
|
||||
.ots-sidebar-collapsed .sidebar-nav,
|
||||
.ots-sidebar:not(.collapsed) .sidebar-nav,
|
||||
.ots-sidebar-collapsed:not(.collapsed) .sidebar-nav {
|
||||
padding-top: 72px !important;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
@@ -354,13 +434,16 @@ body {
|
||||
.ots-topbar {
|
||||
background-color: var(--color-surface-elevated);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
padding: 10px 32px;
|
||||
padding: 8px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
height: 64px;
|
||||
z-index: 1000;
|
||||
z-index: 1100;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Topbar nav container - override .navbar-nav defaults */
|
||||
@@ -369,9 +452,10 @@ body {
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ots-topbar .nav-item {
|
||||
@@ -382,12 +466,44 @@ body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Brand stacking for 'OTS Signs' */
|
||||
.xibo-logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.xibo-logo-text {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-line {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.brand-line-top {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.brand-line-bottom {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
opacity: 0.95;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.ots-topbar .nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
@@ -402,6 +518,162 @@ body {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Ensure content is offset below the sticky topbar when horizontal nav present */
|
||||
nav.navbar + #content-wrapper,
|
||||
nav.navbar + #content-wrapper .page-content {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
/* Right-side controls: notification bell + account menu */
|
||||
.navbar-collapse .navbar-nav.navbar-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.navbar-collapse .navbar-nav.navbar-right > li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-side .user,
|
||||
.header-side .user-notif,
|
||||
.header-side .user-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-side .user-actions {
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-side .user-actions > * {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.header-side .user-actions li,
|
||||
.header-side .user-actions .nav-item,
|
||||
.header-side .user-actions .dropdown,
|
||||
.header-side .user-actions .item,
|
||||
.header-side .user-actions .nav-link {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.header-side .user-actions img.nav-avatar {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.header-side .user-actions .dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Ensure header area does not clip absolutely positioned dropdowns */
|
||||
.row.header.header-side,
|
||||
.row.header.header-side .col-sm-12,
|
||||
.row.header.header-side .user-actions {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure user dropdown renders above other content */
|
||||
.header-side .user-actions .dropdown-menu,
|
||||
.ots-user-menu {
|
||||
z-index: 3000 !important;
|
||||
}
|
||||
|
||||
/* When JS decides to open to the left (avoid viewport overflow) */
|
||||
.dropdown-menu-left {
|
||||
left: auto !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
/* When JS wants explicit left-aligned menu (menu's left edge aligned to trigger's left) */
|
||||
.dropdown-menu-left-align {
|
||||
left: 0 !important;
|
||||
right: auto !important;
|
||||
}
|
||||
|
||||
/* Force header row into a flex container so right-side controls align horizontally */
|
||||
.row.header.header-side {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.row.header.header-side .col-sm-12 {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Ensure notification and user li elements render inline in header */
|
||||
.header-side li.dropdown.nav-item.item,
|
||||
.header-side .dropdown.nav-item.item,
|
||||
.header-side .user-actions > li,
|
||||
.header-side .user-actions > .dropdown,
|
||||
.header-side .user-actions > .nav-item {
|
||||
display: inline-flex !important;
|
||||
float: none !important;
|
||||
vertical-align: middle !important;
|
||||
margin: 0 8px !important;
|
||||
}
|
||||
|
||||
.header-side .nav-link {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.header-side .ots-topbar-icon,
|
||||
.header-side .nav-avatar,
|
||||
.header-side img.nav-avatar {
|
||||
display: inline-block !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* Push user actions to the right and maintain flex layout */
|
||||
.row.header.header-side .meta {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.row.header.header-side .user-actions {
|
||||
margin-left: auto;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Ensure sidebar items are visible and above header when sidebar is collapsed */
|
||||
.ots-sidebar.collapsed {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.ots-sidebar.collapsed .ots-nav-icon {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
/* When collapsed on small screens, allow the right nav to flow normally */
|
||||
.navbar-collapse .navbar-nav.navbar-right {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ots-topbar .nav-item.open .nav-link,
|
||||
.ots-topbar .nav-item.active .nav-link {
|
||||
background-color: rgba(59, 130, 246, 0.12);
|
||||
@@ -709,19 +981,8 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* OTS theme badge in topbar (authed-theme-topbar.twig) */
|
||||
.ots-theme-badge .nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
/* OTS topbar badge removed */
|
||||
|
||||
|
||||
.dropdown-menu li a {
|
||||
display: flex;
|
||||
@@ -781,6 +1042,22 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* When the sidebar is collapsed, hide the page header and meta area to
|
||||
provide a compact layout consistent with the collapsed navigation state. */
|
||||
.ots-sidebar.collapsed ~ .ots-main .page .meta,
|
||||
.ots-sidebar.collapsed ~ .ots-main .page-header,
|
||||
.ots-sidebar.collapsed ~ .ots-main .header-side,
|
||||
.ots-sidebar.collapsed + .ots-main .page .meta,
|
||||
.ots-sidebar.collapsed + .ots-main .page-header,
|
||||
.ots-sidebar-collapsed .ots-main .page .meta,
|
||||
.ots-sidebar-collapsed .ots-main .page-header,
|
||||
body.ots-sidebar-collapsed .page .meta,
|
||||
body.ots-sidebar-collapsed .page-header {
|
||||
display: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 32px;
|
||||
@@ -1258,6 +1535,46 @@ body .panel .panel-heading,
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* When the whole filter card is collapsed, reduce to a small floating button
|
||||
positioned top-left so the page has minimal clutter but the user can
|
||||
reopen the filter. This keeps the header and toggle usable. */
|
||||
.ots-filter-card.collapsed {
|
||||
position: fixed !important;
|
||||
top: 12px !important;
|
||||
left: 12px !important;
|
||||
width: 48px !important;
|
||||
height: 48px !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 6px !important;
|
||||
background: var(--color-surface, #ffffff) !important;
|
||||
box-shadow: 0 6px 20px rgba(6,10,20,0.18) !important;
|
||||
z-index: 1400 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.ots-filter-card.collapsed .ots-filter-header {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.ots-filter-card.collapsed .ots-filter-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ots-filter-card.collapsed .ots-filter-toggle {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
border-radius: 8px !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ots-filter-card.collapsed .ots-filter-content {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ots-filter-card .nav-tabs {
|
||||
display: none;
|
||||
}
|
||||
@@ -1299,8 +1616,9 @@ body .panel .panel-heading,
|
||||
.ots-filter-card .select2-selection,
|
||||
.ots-filter-card .input-group-addon,
|
||||
.ots-filter-card .input-group-text {
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
background-image: none !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 12px 14px !important;
|
||||
@@ -1309,7 +1627,7 @@ body .panel .panel-heading,
|
||||
transition: border 150ms ease, box-shadow 150ms ease !important;
|
||||
height: 48px !important;
|
||||
line-height: 1.4 !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
|
||||
box-shadow: none !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
@@ -1351,11 +1669,12 @@ body .panel .panel-heading,
|
||||
|
||||
.ots-filter-card .select2-container--default .select2-selection--single,
|
||||
.ots-filter-card .select2-container--default .select2-selection--multiple {
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
background-image: none !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 10px !important;
|
||||
min-height: 44px !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ots-filter-card .select2-container--default .select2-selection--multiple .select2-search__field {
|
||||
@@ -1372,13 +1691,14 @@ body .panel .panel-heading,
|
||||
|
||||
.ots-filter-card .bootstrap-tagsinput,
|
||||
.ots-filter-card .tagsinput {
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
background-image: none !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 10px !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
min-height: 44px !important;
|
||||
padding: 6px 10px !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ots-filter-card .bootstrap-tagsinput input,
|
||||
@@ -2976,19 +3296,19 @@ hr {
|
||||
.card,
|
||||
.panel,
|
||||
.modal-content {
|
||||
background: var(--ots-surface-2);
|
||||
border: 1px solid var(--ots-border);
|
||||
border-radius: var(--ots-radius-md);
|
||||
box-shadow: var(--ots-shadow-sm);
|
||||
background: var(--color-surface) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
|
||||
.widget-title,
|
||||
.panel-heading,
|
||||
.card-header,
|
||||
.modal-header {
|
||||
background: var(--ots-surface-3);
|
||||
border-bottom: 1px solid var(--ots-border);
|
||||
color: var(--ots-text);
|
||||
background: var(--color-surface-elevated) !important;
|
||||
border-bottom: 1px solid var(--color-border) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
.widget-body,
|
||||
@@ -3238,8 +3558,10 @@ textarea:focus {
|
||||
============================================================================= */
|
||||
|
||||
.modal-content {
|
||||
border-radius: var(--ots-radius-lg);
|
||||
background-color: var(--ots-surface-2) !important;
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--color-surface) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
}
|
||||
|
||||
.modal,
|
||||
@@ -3247,6 +3569,7 @@ textarea:focus {
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
.modal-backdrop,
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Resolutions" %}</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 "Enabled" %}{% endset %}
|
||||
|
||||
@@ -51,10 +51,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Schedule" %}</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="schedule-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" aria-selected="true"><span>{% trans "General" %}</span></a></li>
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Sync 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 %}
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Templates" %}</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 %}
|
||||
|
||||
@@ -6,6 +6,25 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Apply saved or system-preferred theme as early as possible to avoid
|
||||
// a flash from dark -> light when navigating between pages.
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var initial = stored || (prefersLight ? 'light' : 'light');
|
||||
if (initial === 'light') {
|
||||
document.documentElement.classList.add('ots-light-mode');
|
||||
if (document.body) document.body.classList.add('ots-light-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('ots-light-mode');
|
||||
if (document.body) document.body.classList.remove('ots-light-mode');
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore failures (e.g. localStorage unavailable)
|
||||
}
|
||||
})();
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
sidebarCollapsed: 'otsTheme:sidebarCollapsed'
|
||||
};
|
||||
@@ -17,10 +36,181 @@
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const collapsed = sidebar.classList.contains('collapsed');
|
||||
const base = collapsed ? 88 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240;
|
||||
// If called with a forced mode, use the stored defaults
|
||||
const forceMode = updateSidebarWidth._forceMode || null;
|
||||
const base = (forceMode === 'full') ? (window.__otsFullSidebarWidth || 256)
|
||||
: (forceMode === 'collapsed') ? (window.__otsCollapsedSidebarWidth || 70)
|
||||
: (collapsed ? 70 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240);
|
||||
const padding = 5;
|
||||
const value = Math.max(88, Math.round(base + padding));
|
||||
const value = Math.max(70, Math.round(base + padding));
|
||||
// Apply CSS variable used by layout and also set an inline width fallback
|
||||
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
|
||||
try {
|
||||
// Inline width helps force an immediate reflow when CSS rules/important flags interfere
|
||||
// Use setProperty with 'important' so stylesheet !important rules can't override it.
|
||||
sidebar.style.setProperty('width', `${value}px`, 'important');
|
||||
// Force reflow to encourage the browser to apply the new sizing immediately
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
sidebar.offsetHeight;
|
||||
} catch (err) {
|
||||
try { sidebar.style.width = `${value}px`; } catch (e) { /* ignore */ }
|
||||
}
|
||||
// Debug logging to help identify timing/specifity issues in the wild
|
||||
if (window.__otsDebug) {
|
||||
console.log('[OTS] updateSidebarWidth', { collapsed, base, value, cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width') });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to request a forced width update
|
||||
function forceSidebarWidthMode(mode) {
|
||||
updateSidebarWidth._forceMode = mode; // 'full' | 'collapsed' | null
|
||||
updateSidebarWidth();
|
||||
updateSidebarWidth._forceMode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar header bottom and set the top padding of the nav list
|
||||
* so nav items always begin below the header (logo + buttons).
|
||||
*/
|
||||
function updateSidebarNavOffset() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const header = sidebar.querySelector('.sidebar-header');
|
||||
const nav = sidebar.querySelector('.sidebar-nav, .ots-sidebar-nav');
|
||||
if (!nav) return;
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
const headerRect = header ? header.getBoundingClientRect() : null;
|
||||
let offset = 0;
|
||||
if (headerRect) {
|
||||
offset = Math.max(0, Math.ceil(headerRect.bottom - sidebarRect.top));
|
||||
} else if (header) {
|
||||
offset = header.offsetHeight || 0;
|
||||
}
|
||||
const gap = 8;
|
||||
const paddingTop = offset > 0 ? offset + gap : '';
|
||||
if (paddingTop) {
|
||||
try {
|
||||
nav.style.setProperty('padding-top', `${paddingTop}px`, 'important');
|
||||
} catch (err) {
|
||||
nav.style.paddingTop = `${paddingTop}px`;
|
||||
}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarNavOffset applied', { paddingTop });
|
||||
} else {
|
||||
try {
|
||||
nav.style.removeProperty('padding-top');
|
||||
} catch (err) {
|
||||
nav.style.paddingTop = '';
|
||||
}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarNavOffset cleared');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar and set an explicit left margin on the page wrapper
|
||||
* so the gap between the sidebar and page content is exactly 5px.
|
||||
*/
|
||||
function updateSidebarGap() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
// target likely content containers in this app
|
||||
const targets = [
|
||||
document.getElementById('page-wrapper'),
|
||||
document.querySelector('.ots-main'),
|
||||
document.getElementById('content-wrapper'),
|
||||
document.querySelector('#content')
|
||||
].filter(Boolean);
|
||||
if (!sidebar || !targets.length) return;
|
||||
|
||||
const gap = (typeof window.__otsDesiredSidebarGap !== 'undefined') ? Number(window.__otsDesiredSidebarGap) : 0; // desired gap in px (default 0)
|
||||
const rect = sidebar.getBoundingClientRect();
|
||||
|
||||
// desired inner left padding (allows trimming space inside the content area)
|
||||
const desiredInnerPadding = (typeof window.__otsDesiredPagePaddingLeft !== 'undefined') ? Number(window.__otsDesiredPagePaddingLeft) : 8;
|
||||
|
||||
targets.forEach(pageWrapper => {
|
||||
const pageRect = pageWrapper.getBoundingClientRect();
|
||||
const computed = window.getComputedStyle(pageWrapper);
|
||||
const currentMargin = parseFloat(computed.marginLeft) || 0;
|
||||
const currentGap = Math.round(pageRect.left - rect.right);
|
||||
// Calculate how much to adjust margin-left so gap becomes `gap`.
|
||||
const delta = currentGap - gap;
|
||||
const newMargin = Math.max(0, Math.round(currentMargin - delta));
|
||||
try {
|
||||
pageWrapper.style.setProperty('margin-left', `${newMargin}px`, 'important');
|
||||
pageWrapper.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
|
||||
} catch (err) {
|
||||
pageWrapper.style.marginLeft = `${newMargin}px`;
|
||||
pageWrapper.style.paddingLeft = `${desiredInnerPadding}px`;
|
||||
}
|
||||
// Also adjust common child wrapper padding if present
|
||||
try {
|
||||
const inner = pageWrapper.querySelector('.page-content') || pageWrapper.querySelector('.ots-content') || pageWrapper.querySelector('.container');
|
||||
if (inner) inner.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
|
||||
} catch (err) {}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarGap', {
|
||||
target: pageWrapper.tagName + (pageWrapper.id ? '#'+pageWrapper.id : ''),
|
||||
sidebarWidth: rect.width,
|
||||
sidebarRight: rect.right,
|
||||
pageLeft: pageRect.left,
|
||||
currentGap,
|
||||
newMargin
|
||||
});
|
||||
|
||||
// Detect narrow intervening elements (visual separator) and neutralize their visuals
|
||||
try {
|
||||
const sampleXs = [Math.round(rect.right + 2), Math.round((rect.right + pageRect.left) / 2), Math.round(pageRect.left - 2)];
|
||||
const ys = [Math.floor(window.innerHeight / 2), Math.floor(window.innerHeight / 4), Math.floor(window.innerHeight * 0.75)];
|
||||
const seen = new Set();
|
||||
sampleXs.forEach(x => {
|
||||
ys.forEach(y => {
|
||||
try {
|
||||
const els = document.elementsFromPoint(x, y) || [];
|
||||
els.forEach(el => {
|
||||
if (!el || el === document.documentElement || el === document.body) return;
|
||||
if (el === sidebar || el === pageWrapper) return;
|
||||
const b = el.getBoundingClientRect();
|
||||
// narrow vertical candidates between sidebar and content
|
||||
if (b.left >= rect.right - 4 && b.right <= pageRect.left + 4 && b.width <= 80 && b.height >= 40) {
|
||||
const id = el.tagName + (el.id ? '#'+el.id : '') + (el.className ? '.'+el.className.split(' ').join('.') : '');
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
try {
|
||||
el.style.setProperty('background', 'transparent', 'important');
|
||||
el.style.setProperty('background-image', 'none', 'important');
|
||||
el.style.setProperty('box-shadow', 'none', 'important');
|
||||
el.style.setProperty('border', 'none', 'important');
|
||||
el.style.setProperty('pointer-events', 'none', 'important');
|
||||
if (window.__otsDebug) console.log('[OTS] neutralized intervening element', { id, rect: b });
|
||||
} catch (err) {}
|
||||
}
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return function () {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn.apply(this, arguments), wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflect sidebar open/collapsed state on the document body
|
||||
*/
|
||||
function updateSidebarStateClass() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const body = document.body;
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
if (!isCollapsed) {
|
||||
body.classList.add('ots-sidebar-open');
|
||||
} else {
|
||||
body.classList.remove('ots-sidebar-open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +219,8 @@
|
||||
function initSidebarToggle() {
|
||||
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn-visible');
|
||||
const expandBtn = document.querySelector('.sidebar-expand-btn');
|
||||
const body = document.body;
|
||||
|
||||
if (!sidebar) return;
|
||||
@@ -46,6 +237,8 @@
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
body.classList.add('ots-sidebar-collapsed');
|
||||
updateSidebarStateClass();
|
||||
updateSidebarGap();
|
||||
}
|
||||
|
||||
collapseBtn.addEventListener('click', function(e) {
|
||||
@@ -54,7 +247,50 @@
|
||||
sidebar.classList.toggle('collapsed');
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
updateSidebarWidth();
|
||||
// Force collapsed width immediately
|
||||
forceSidebarWidthMode('collapsed');
|
||||
// Recalculate nav offset so items remain below header after collapse
|
||||
updateSidebarNavOffset();
|
||||
// Ensure page content gap is updated for collapsed width
|
||||
updateSidebarGap();
|
||||
// Re-run shortly after to catch any late layout changes
|
||||
setTimeout(updateSidebarGap, 80);
|
||||
updateSidebarStateClass();
|
||||
// Debug state after toggle
|
||||
try {
|
||||
console.log('[OTS] collapseBtn clicked', {
|
||||
nowCollapsed,
|
||||
classes: sidebar.className,
|
||||
inlineStyle: sidebar.getAttribute('style'),
|
||||
computedWidth: getComputedStyle(sidebar).width,
|
||||
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
if (expandBtn) {
|
||||
expandBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('collapsed');
|
||||
body.classList.remove('ots-sidebar-collapsed');
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
|
||||
// Force full width when expanding
|
||||
forceSidebarWidthMode('full');
|
||||
// Recalculate nav offset after expanding
|
||||
updateSidebarNavOffset();
|
||||
// Ensure page content gap is updated for expanded width
|
||||
updateSidebarGap();
|
||||
setTimeout(updateSidebarGap, 80);
|
||||
updateSidebarStateClass();
|
||||
try {
|
||||
console.log('[OTS] expandBtn clicked', {
|
||||
classes: sidebar.className,
|
||||
inlineStyle: sidebar.getAttribute('style'),
|
||||
computedWidth: getComputedStyle(sidebar).width,
|
||||
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,9 +302,13 @@
|
||||
|
||||
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure initial state class is set
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,9 +385,35 @@
|
||||
|
||||
// Toggle menu on button click
|
||||
dropdown.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]')) {
|
||||
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]') || e.target.closest('#navbarUserMenu')) {
|
||||
e.preventDefault();
|
||||
dropdown.classList.toggle('active');
|
||||
|
||||
// If this dropdown contains the user menu, compute placement to avoid going off-screen
|
||||
const menu = dropdown.querySelector('.dropdown-menu.ots-user-menu');
|
||||
const trigger = dropdown.querySelector('#navbarUserMenu');
|
||||
if (menu && trigger) {
|
||||
// Reset any previous placement classes
|
||||
menu.classList.remove('dropdown-menu-left');
|
||||
menu.classList.remove('dropdown-menu-right');
|
||||
|
||||
// Use getBoundingClientRect for accurate placement
|
||||
const trigRect = trigger.getBoundingClientRect();
|
||||
// Ensure menu is in DOM and has an offsetWidth
|
||||
const menuWidth = menu.offsetWidth || 220; // fallback estimate
|
||||
|
||||
const spaceRight = window.innerWidth - trigRect.right;
|
||||
const spaceLeft = trigRect.left;
|
||||
|
||||
// Prefer opening to the right where possible, otherwise open to the left
|
||||
if (spaceRight < menuWidth && spaceLeft > menuWidth) {
|
||||
// not enough space on the right, open to left
|
||||
menu.classList.add('dropdown-menu-left');
|
||||
} else {
|
||||
// default to right-aligned
|
||||
menu.classList.add('dropdown-menu-right');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -318,6 +584,7 @@
|
||||
sidebar.classList.add('mobile');
|
||||
}
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -442,12 +709,55 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize light/dark mode toggle
|
||||
*/
|
||||
function initThemeToggle() {
|
||||
const themeToggle = document.getElementById('ots-theme-toggle');
|
||||
if (!themeToggle) return;
|
||||
|
||||
const storedTheme = localStorage.getItem('ots-theme-mode');
|
||||
const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const effectiveTheme = storedTheme || (prefersLight ? 'light' : 'dark');
|
||||
const body = document.body;
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply stored theme on page load (apply to both <html> and <body>)
|
||||
if (effectiveTheme === 'light') {
|
||||
body.classList.add('ots-light-mode');
|
||||
root.classList.add('ots-light-mode');
|
||||
updateThemeLabel();
|
||||
}
|
||||
|
||||
// Toggle on click (keep <html> in sync so :root variables reflect mode)
|
||||
themeToggle.addEventListener('click', function(e) {
|
||||
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');
|
||||
updateThemeLabel();
|
||||
});
|
||||
|
||||
function updateThemeLabel() {
|
||||
const icon = document.getElementById('ots-theme-icon');
|
||||
const label = document.getElementById('ots-theme-label');
|
||||
const isLight = body.classList.contains('ots-light-mode');
|
||||
if (icon) {
|
||||
icon.className = isLight ? 'fa fa-sun-o' : 'fa fa-moon-o';
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = isLight ? 'Light Mode' : 'Dark Mode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
function init() {
|
||||
initSidebarToggle();
|
||||
initSidebarSectionToggles();
|
||||
initThemeToggle();
|
||||
initDropdowns();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
@@ -456,7 +766,14 @@
|
||||
makeResponsive();
|
||||
initChartSafeguard();
|
||||
updateSidebarWidth();
|
||||
window.addEventListener('resize', updateSidebarWidth);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarGap();
|
||||
var debouncedUpdate = debounce(function() {
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
}, 120);
|
||||
window.addEventListener('resize', debouncedUpdate);
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
|
||||
Reference in New Issue
Block a user