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:
Matt Batchelder
2026-02-05 09:04:06 -05:00
parent d8f8c0f916
commit 122d098be4
23 changed files with 2447 additions and 190 deletions

View File

@@ -270,6 +270,26 @@ hr {
padding: 0; padding: 0;
} }
/* Dark-mode repair overrides: ensure sidebar doesn't draw a vertical divider */
#sidebar-wrapper,
.ots-sidebar {
border-right: none !important;
box-shadow: none !important;
}
/* Hide pseudo-elements that might draw a divider */
.ots-sidebar::before,
.ots-sidebar::after,
#sidebar-wrapper::before,
#sidebar-wrapper::after,
#page-wrapper::before,
#page-wrapper::after {
content: none !important;
display: none !important;
background: transparent !important;
box-shadow: none !important;
}
/* Sidebar Main Item */ /* Sidebar Main Item */
.ots-sidebar li.sidebar-main > a { .ots-sidebar li.sidebar-main > a {
display: flex; display: flex;
@@ -740,7 +760,9 @@ textarea:focus {
.modal-content { .modal-content {
border-radius: var(--ots-radius-lg); border-radius: var(--ots-radius-lg);
background-color: var(--ots-surface-2) !important; background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
} }
.modal, .modal,
@@ -748,6 +770,7 @@ textarea:focus {
.modal-body, .modal-body,
.modal-footer { .modal-footer {
background-color: transparent !important; background-color: transparent !important;
color: var(--color-text-primary) !important;
} }
.modal-backdrop, .modal-backdrop,
@@ -759,7 +782,7 @@ textarea:focus {
} }
.modal-footer { .modal-footer {
border-top: 1px solid var(--ots-border); border-top: 1px solid var(--color-border) !important;
} }
/* ============================================================================= /* =============================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,8 @@
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]'); const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
const sidebar = document.querySelector('.ots-sidebar'); const sidebar = document.querySelector('.ots-sidebar');
const closeBtn = document.querySelector('.ots-sidebar-close'); const closeBtn = document.querySelector('.ots-sidebar-close');
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; const body = document.body;
if (!sidebar) return; if (!sidebar) return;
@@ -42,6 +43,7 @@
if (isCollapsed) { if (isCollapsed) {
sidebar.classList.add('collapsed'); sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed'); body.classList.add('ots-sidebar-collapsed');
updateSidebarStateClass();
} }
collapseBtn.addEventListener('click', function(e) { collapseBtn.addEventListener('click', function(e) {
@@ -52,6 +54,22 @@
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
// Update measured sidebar width when collapsed state changes // Update measured sidebar width when collapsed state changes
updateSidebarWidth(); updateSidebarWidth();
// Recalculate nav offset so items remain below header after collapse
updateSidebarNavOffset();
updateSidebarStateClass();
});
}
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');
updateSidebarWidth();
// Recalculate nav offset after expanding
updateSidebarNavOffset();
updateSidebarStateClass();
}); });
} }
@@ -79,18 +97,82 @@
if (!sidebar) return; if (!sidebar) return;
// If collapsed, use the known collapsed width; otherwise use measured width // If collapsed, use the known collapsed width; otherwise use measured width
const collapsed = sidebar.classList.contains('collapsed'); const collapsed = sidebar.classList.contains('collapsed');
const base = collapsed ? 88 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240; const base = collapsed ? 64 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 256;
const padding = 5; // user requested ~5px padding const padding = 0;
const value = Math.max(88, Math.round(base + padding)); const value = Math.max(64, Math.round(base + padding));
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`); document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
} }
/**
* 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;
// Calculate header bottom relative to the sidebar top (so it works with scrolling)
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;
}
// Add a small gap so nav doesn't touch the header edge
const gap = 8;
const paddingTop = offset > 0 ? offset + gap : '';
// apply as inline style to ensure it overrides static CSS rules
if (paddingTop) {
// Use setProperty with priority so it overrides stylesheet !important rules
try {
nav.style.setProperty('padding-top', `${paddingTop}px`, 'important');
} catch (err) {
// fallback
nav.style.paddingTop = `${paddingTop}px`;
}
} else {
// remove inline override
try {
nav.style.removeProperty('padding-top');
} catch (err) {
nav.style.paddingTop = '';
}
}
}
// simple debounce helper
function debounce(fn, wait) {
let t;
return function () {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, arguments), wait);
};
}
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');
}
}
/** /**
* Initialize sidebar section collapse/expand functionality * Initialize sidebar section collapse/expand functionality
*/ */
function initSidebarSectionToggles() { function initSidebarSectionToggles() {
const groupToggles = document.querySelectorAll('.sidebar-group-toggle'); const groupToggles = document.querySelectorAll('.sidebar-group-toggle');
syncSidebarActiveStates();
groupToggles.forEach(toggle => { groupToggles.forEach(toggle => {
const group = toggle.closest('.sidebar-group'); const group = toggle.closest('.sidebar-group');
const submenu = group ? group.querySelector('.sidebar-submenu') : null; const submenu = group ? group.querySelector('.sidebar-submenu') : null;
@@ -112,6 +194,7 @@
group.classList.toggle('is-open', !isOpen); group.classList.toggle('is-open', !isOpen);
toggle.setAttribute('aria-expanded', (!isOpen).toString()); toggle.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block'; submenu.style.display = isOpen ? 'none' : 'block';
syncSidebarActiveStates();
}); });
if (caret) { if (caret) {
@@ -141,9 +224,32 @@
group.classList.toggle('is-open', !isOpen); group.classList.toggle('is-open', !isOpen);
target.setAttribute('aria-expanded', (!isOpen).toString()); target.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block'; submenu.style.display = isOpen ? 'none' : 'block';
syncSidebarActiveStates();
}, true); }, true);
} }
function syncSidebarActiveStates() {
const groups = document.querySelectorAll('.sidebar-group');
groups.forEach(group => {
const toggle = group.querySelector('.sidebar-group-toggle');
if (!toggle) return;
const hasActiveChild = Boolean(
group.querySelector('.sidebar-list.active') ||
group.querySelector('.sidebar-list > a.active')
);
toggle.classList.toggle('active', hasActiveChild);
if (hasActiveChild) {
group.classList.add('is-open');
const submenu = group.querySelector('.sidebar-submenu');
if (submenu) submenu.style.display = 'block';
toggle.setAttribute('aria-expanded', 'true');
}
});
}
/** /**
* Initialize dropdown menus * Initialize dropdown menus
*/ */
@@ -462,7 +568,13 @@
initChartSafeguard(); initChartSafeguard();
// Set initial sidebar width variable and keep it updated // Set initial sidebar width variable and keep it updated
updateSidebarWidth(); updateSidebarWidth();
window.addEventListener('resize', updateSidebarWidth); // Set initial nav offset and keep it updated on resize
updateSidebarNavOffset();
const debouncedUpdateNavOffset = debounce(function() {
updateSidebarNavOffset();
updateSidebarWidth();
}, 120);
window.addEventListener('resize', debouncedUpdateNavOffset);
} }
// Wait for DOM to be ready // Wait for DOM to be ready

View 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 %}

View File

@@ -11,7 +11,10 @@
</span> </span>
<span class="brand-text">OTS Signs</span> <span class="brand-text">OTS Signs</span>
</a> </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> <i class="fa fa-chevron-left" aria-hidden="true"></i>
</button> </button>
</div> </div>

View File

@@ -3,6 +3,4 @@
Optional include rendered in authed.twig (top right navbar) Optional include rendered in authed.twig (top right navbar)
Minimal, low-risk addition for verification Minimal, low-risk addition for verification
#} #}
<li class="nav-item ots-theme-badge"> {# OTS topbar badge removed #}
<span class="nav-link">OTS Theme</span>
</li>

View File

@@ -3,11 +3,19 @@
Based on Xibo CMS default authed-user-menu.twig (master branch) Based on Xibo CMS default authed-user-menu.twig (master branch)
Minimal change: add ots-user-menu class for easy verification 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"> <li class="dropdown nav-item item">
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu"> <a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" /> <img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
</a> </a>
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu"> <div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
{% endif %}
<h6 class="dropdown-header">{{ currentUser.userName }}<br/> <h6 class="dropdown-header">{{ currentUser.userName }}<br/>
<div id="XiboClock">{{ clock }}</div> <div id="XiboClock">{{ clock }}</div>
</h6> </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> <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> <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" 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> <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> <a class="dropdown-item" title="{% trans "Logout" %}" href="{{ logoutUrl }}">{% trans "Logout" %}</a>
{% endif %} {% endif %}
</div> </div>
{% if compact is defined and compact %}
</div>
{% else %}
</div>
</li> </li>
{% endif %}

View File

@@ -22,6 +22,50 @@
#} #}
{% extends "base.twig" %} {% 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 %} {% block content %}
{% set horizontalNav = currentUser.getOptionValue("navigationMenuPosition", theme.getSetting("NAVIGATION_MENU_POSITION", "vertical")) == "horizontal" %} {% set horizontalNav = currentUser.getOptionValue("navigationMenuPosition", theme.getSetting("NAVIGATION_MENU_POSITION", "vertical")) == "horizontal" %}
@@ -36,7 +80,10 @@
<nav class="navbar navbar-default navbar-expand-lg"> <nav class="navbar navbar-default navbar-expand-lg">
<a class="navbar-brand xibo-logo-container" href="#"> <a class="navbar-brand xibo-logo-container" href="#">
<img class="xibo-logo" src="{{ theme.uri("img/xibologo.png") }}"> <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> </a>
<!-- Brand and toggle get grouped for better mobile display --> <!-- Brand and toggle get grouped for better mobile display -->
@@ -78,14 +125,16 @@
<span class="fa fa-bars"></span> <span class="fa fa-bars"></span>
</button> </button>
{% endif %} {% endif %}
<div class="user pull-right"> <div class="user-actions pull-right">
{% include "authed-user-menu.twig" %}
</div>
{% if currentUser.featureEnabled("drawer") %} {% if currentUser.featureEnabled("drawer") %}
<div class="user user-notif pull-right"> <div class="user-notif">
{% include "authed-notification-drawer.twig" %} {% include "authed-notification-drawer.twig" with { 'compact': true } %}
</div> </div>
{% endif %} {% endif %}
<div class="user">
{% include "authed-user-menu.twig" with { 'compact': true } %}
</div>
</div>
{% include "authed-theme-topbar.twig" ignore missing %} {% include "authed-theme-topbar.twig" ignore missing %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Campaigns" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %} {% set title %}{% trans "Name" %}{% endset %}

View File

@@ -49,10 +49,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Commands" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %} {% set title %}{% trans "Name" %}{% endset %}

View File

@@ -1,52 +1,123 @@
/* High-specificity DataTables contrast overrides /* High-specificity DataTables contrast overrides
Ensures table body text is readable against dark theme backgrounds. 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 table.dataTable tbody td,
#datatable-container .dataTables_wrapper 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,
.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; opacity: 1 !important;
} }
#datatable-container table.dataTable thead th, #datatable-container table.dataTable thead th,
.ots-table-card table.dataTable thead th, .ots-table-card table.dataTable thead th,
#datatable-container table.dataTable thead th * { #datatable-container table.dataTable thead th * {
color: #ffffff !important; color: var(--color-text-secondary) !important;
opacity: 1 !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.table-success td,
#datatable-container table.dataTable tbody tr.success td, #datatable-container table.dataTable tbody tr.success td,
#datatable-container table.dataTable tbody tr.selected td, #datatable-container table.dataTable tbody tr.selected td,
#datatable-container table.dataTable tbody tr.highlight td { #datatable-container table.dataTable tbody tr.highlight td {
background-color: rgba(255,255,255,0.08) !important; background-color: rgba(16, 185, 129, 0.1) !important;
color: #ffffff !important; color: var(--color-text-primary) !important;
} }
#datatable-container table.dataTable tbody td .btn, #datatable-container table.dataTable tbody td .btn,
#datatable-container table.dataTable tbody td .badge, #datatable-container table.dataTable tbody td .badge,
#datatable-container table.dataTable tbody td .dropdown-toggle { #datatable-container table.dataTable tbody td .dropdown-toggle {
color: #ffffff !important; color: var(--color-text-primary) !important;
} }
#datatable-container table.dataTable tbody tr { #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 { #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_filter input,
.dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dataTables_paginate .paginate_button { .dataTables_wrapper .dataTables_paginate .paginate_button {
color: #f5f5f5 !important; color: var(--color-text-primary) !important;
background: rgba(0,0,0,0.2) !important; background: var(--color-surface) !important;
border-color: rgba(255,255,255,0.1) !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, #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,
.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;
} }

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Dayparts" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %} {% set title %}{% trans "Name" %}{% endset %}

View File

@@ -36,6 +36,44 @@
{% block headContent %} {% block headContent %}
{# Add page source code bundle ( CSS ) #} {# Add page source code bundle ( CSS ) #}
<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 }}"> <link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
{% endblock %} {% endblock %}
@@ -53,10 +91,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Displays" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist"> <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> <li class="nav-item"><a class="nav-link active" href="#filter-general" role="tab" data-toggle="tab">{% trans "General" %}</a></li>

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Display Settings" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">

View File

@@ -31,10 +31,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Layouts" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist"> <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> <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>

View File

@@ -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> <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 %} {% endif %}
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %} {% 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 %}
{% 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> <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>

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Menu Boards" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %} {% set title %}{% trans "ID" %}{% endset %}

View File

@@ -29,6 +29,78 @@
color-scheme: dark; 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, html,
body { body {
background-color: var(--color-background); background-color: var(--color-background);
@@ -144,7 +216,15 @@ body {
.sidebar-nav { .sidebar-nav {
list-style: none; list-style: none;
margin: 0; 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 { .sidebar-nav li {
@@ -354,13 +434,16 @@ body {
.ots-topbar { .ots-topbar {
background-color: var(--color-surface-elevated); background-color: var(--color-surface-elevated);
border-bottom: 2px solid var(--color-border); border-bottom: 2px solid var(--color-border);
padding: 10px 32px; padding: 8px 24px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
gap: 16px; gap: 8px;
height: 64px; height: 64px;
z-index: 1000; z-index: 1100;
position: sticky;
top: 0;
width: 100%;
} }
/* Topbar nav container - override .navbar-nav defaults */ /* Topbar nav container - override .navbar-nav defaults */
@@ -369,9 +452,10 @@ body {
border: 0 !important; border: 0 !important;
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
gap: 4px; gap: 6px;
height: 100%; height: 100%;
align-items: center; align-items: center;
justify-content: flex-start;
} }
.ots-topbar .nav-item { .ots-topbar .nav-item {
@@ -382,12 +466,44 @@ body {
padding: 0 !important; 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 { .ots-topbar .nav-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
gap: 6px; gap: 6px;
padding: 0 12px; padding: 0 8px;
border-radius: 6px; border-radius: 6px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500; font-weight: 500;
@@ -402,6 +518,162 @@ body {
color: var(--color-primary); 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.open .nav-link,
.ots-topbar .nav-item.active .nav-link { .ots-topbar .nav-item.active .nav-link {
background-color: rgba(59, 130, 246, 0.12); background-color: rgba(59, 130, 246, 0.12);
@@ -709,19 +981,8 @@ body {
display: block; display: block;
} }
/* OTS theme badge in topbar (authed-theme-topbar.twig) */ /* OTS topbar badge removed */
.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;
}
.dropdown-menu li a { .dropdown-menu li a {
display: flex; display: flex;
@@ -781,6 +1042,22 @@ body {
margin-bottom: 16px; 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 { .page-header h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 32px; font-size: 32px;
@@ -1258,6 +1535,46 @@ body .panel .panel-heading,
display: none; 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 { .ots-filter-card .nav-tabs {
display: none; display: none;
} }
@@ -1299,8 +1616,9 @@ body .panel .panel-heading,
.ots-filter-card .select2-selection, .ots-filter-card .select2-selection,
.ots-filter-card .input-group-addon, .ots-filter-card .input-group-addon,
.ots-filter-card .input-group-text { .ots-filter-card .input-group-text {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important; background-color: var(--color-surface) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important; background-image: none !important;
border: 1px solid var(--color-border) !important;
color: var(--color-text-primary) !important; color: var(--color-text-primary) !important;
border-radius: 10px !important; border-radius: 10px !important;
padding: 12px 14px !important; padding: 12px 14px !important;
@@ -1309,7 +1627,7 @@ body .panel .panel-heading,
transition: border 150ms ease, box-shadow 150ms ease !important; transition: border 150ms ease, box-shadow 150ms ease !important;
height: 48px !important; height: 48px !important;
line-height: 1.4 !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; 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--single,
.ots-filter-card .select2-container--default .select2-selection--multiple { .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; background-color: var(--color-surface) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important; background-image: none !important;
border: 1px solid var(--color-border) !important;
border-radius: 10px !important; border-radius: 10px !important;
min-height: 44px !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 { .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 .bootstrap-tagsinput,
.ots-filter-card .tagsinput { .ots-filter-card .tagsinput {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important; background-color: var(--color-surface) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important; background-image: none !important;
border: 1px solid var(--color-border) !important;
border-radius: 10px !important; border-radius: 10px !important;
color: var(--color-text-primary) !important; color: var(--color-text-primary) !important;
min-height: 44px !important; min-height: 44px !important;
padding: 6px 10px !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, .ots-filter-card .bootstrap-tagsinput input,
@@ -2976,19 +3296,19 @@ hr {
.card, .card,
.panel, .panel,
.modal-content { .modal-content {
background: var(--ots-surface-2); background: var(--color-surface) !important;
border: 1px solid var(--ots-border); border: 1px solid var(--color-border) !important;
border-radius: var(--ots-radius-md); border-radius: var(--radius-md);
box-shadow: var(--ots-shadow-sm); box-shadow: var(--shadow-base);
} }
.widget-title, .widget-title,
.panel-heading, .panel-heading,
.card-header, .card-header,
.modal-header { .modal-header {
background: var(--ots-surface-3); background: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--ots-border); border-bottom: 1px solid var(--color-border) !important;
color: var(--ots-text); color: var(--color-text-primary) !important;
} }
.widget-body, .widget-body,
@@ -3238,8 +3558,10 @@ textarea:focus {
============================================================================= */ ============================================================================= */
.modal-content { .modal-content {
border-radius: var(--ots-radius-lg); border-radius: var(--radius-lg);
background-color: var(--ots-surface-2) !important; background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
} }
.modal, .modal,
@@ -3247,6 +3569,7 @@ textarea:focus {
.modal-body, .modal-body,
.modal-footer { .modal-footer {
background-color: transparent !important; background-color: transparent !important;
color: var(--color-text-primary) !important;
} }
.modal-backdrop, .modal-backdrop,

View File

@@ -49,10 +49,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Resolutions" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "Enabled" %}{% endset %} {% set title %}{% trans "Enabled" %}{% endset %}

View File

@@ -51,10 +51,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Schedule" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="schedule-filter">
<ul class="nav nav-tabs" role="tablist"> <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> <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>

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Sync Groups" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %} {% set title %}{% trans "ID" %}{% endset %}

View File

@@ -48,10 +48,10 @@
<div class="ots-filter-header"> <div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Templates" %}</h3> <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' %}"> <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> </button>
</div> </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"> <div class="FilterDiv card-body" id="Filter">
<form class="form-inline"> <form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %} {% set title %}{% trans "Name" %}{% endset %}

View File

@@ -6,6 +6,25 @@
(function() { (function() {
'use strict'; '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 = { const STORAGE_KEYS = {
sidebarCollapsed: 'otsTheme:sidebarCollapsed' sidebarCollapsed: 'otsTheme:sidebarCollapsed'
}; };
@@ -17,10 +36,181 @@
const sidebar = document.querySelector('.ots-sidebar'); const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return; if (!sidebar) return;
const collapsed = sidebar.classList.contains('collapsed'); 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 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`); 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() { function initSidebarToggle() {
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]'); const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
const sidebar = document.querySelector('.ots-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; const body = document.body;
if (!sidebar) return; if (!sidebar) return;
@@ -46,6 +237,8 @@
if (isCollapsed) { if (isCollapsed) {
sidebar.classList.add('collapsed'); sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed'); body.classList.add('ots-sidebar-collapsed');
updateSidebarStateClass();
updateSidebarGap();
} }
collapseBtn.addEventListener('click', function(e) { collapseBtn.addEventListener('click', function(e) {
@@ -54,7 +247,50 @@
sidebar.classList.toggle('collapsed'); sidebar.classList.toggle('collapsed');
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed); body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); 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')) { if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
sidebar.classList.remove('active'); sidebar.classList.remove('active');
updateSidebarStateClass();
} }
} }
}); });
// Ensure initial state class is set
updateSidebarStateClass();
} }
/** /**
@@ -145,9 +385,35 @@
// Toggle menu on button click // Toggle menu on button click
dropdown.addEventListener('click', function(e) { 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(); e.preventDefault();
dropdown.classList.toggle('active'); 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'); sidebar.classList.add('mobile');
} }
updateSidebarWidth(); 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 * Initialize all features when DOM is ready
*/ */
function init() { function init() {
initSidebarToggle(); initSidebarToggle();
initSidebarSectionToggles(); initSidebarSectionToggles();
initThemeToggle();
initDropdowns(); initDropdowns();
initSearch(); initSearch();
initPageInteractions(); initPageInteractions();
@@ -456,7 +766,14 @@
makeResponsive(); makeResponsive();
initChartSafeguard(); initChartSafeguard();
updateSidebarWidth(); 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 // Wait for DOM to be ready