Refactor toolbar buttons across various pages to unify styling

- Updated button classes for consistency in the playersoftware-page, playlist-page, resolution-page, schedule-page, settings-page, syncgroup-page, tag-page, task-page, template-page, transition-page, user-page, and usergroup-page.
- Removed unnecessary text from button titles and ensured all buttons have the 'ots-toolbar-btn' class for uniformity.
- Cleaned up the code by removing commented-out sections and ensuring proper indentation.
This commit is contained in:
Matt Batchelder
2026-02-07 14:50:40 -05:00
parent 1c5c23f100
commit 86030cb881
34 changed files with 2614 additions and 554 deletions

View File

@@ -904,19 +904,29 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
background-size: 48px 48px, cover;
}
body.login-page .container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
.login-card,
.login-panel,
.auth-card,
.xibo-login-box,
#login-box {
width: 100%;
max-width: 560px;
border-radius: 12px;
padding: 32px 36px;
background: var(--login-panel-bg);
border: 1px solid rgba(255,255,255,0.04);
box-shadow: var(--ots-shadow-lg);
max-width: 520px;
border-radius: 16px;
padding: 36px 40px 34px;
background: linear-gradient(180deg, rgba(7,12,22,0.92), rgba(10,16,30,0.9));
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.55);
color: var(--ots-text);
backdrop-filter: blur(14px);
}
.login-card .login-logo,
@@ -942,23 +952,24 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
/* Brand text next to logo on login */
.login-brand {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
gap: 10px;
margin-bottom: 18px;
}
.login-brand .login-logo {
width: 92px;
height: 92px;
width: 72px;
height: 72px;
display: inline-block;
}
.login-brand-text {
color: var(--ots-text);
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 0.01em;
font-size: 1.2rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.login-card h1,
@@ -972,8 +983,9 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
.login-card .lead,
.login-panel .lead {
text-align: center;
color: var(--ots-text-muted);
color: rgba(226,232,240,0.74);
margin-bottom: 18px;
font-size: 0.95rem;
}
.login-card .form-group,
@@ -989,12 +1001,12 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
.login-panel input[type="email"],
.login-panel input[type="password"] {
width: 100%;
background: linear-gradient(180deg, rgba(255,255,255,0.016), rgba(255,255,255,0.01));
border: 1px solid rgba(255,255,255,0.06);
background: rgba(11, 18, 33, 0.65);
border: 1px solid rgba(148, 163, 184, 0.18);
color: var(--ots-text);
padding: 14px 16px;
border-radius: 12px;
font-size: 1rem;
padding: 12px 14px;
border-radius: 10px;
font-size: 0.98rem;
line-height: 1.25;
transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
}
@@ -1003,9 +1015,9 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
.login-panel input:focus,
.login-card .form-control:focus {
outline: none;
border-color: rgba(255,138,0,0.95);
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
box-shadow: 0 8px 22px rgba(0,0,0,0.32), 0 0 0 6px rgba(255,138,0,0.04);
border-color: rgba(255,138,0,0.85);
background: rgba(11, 18, 33, 0.75);
box-shadow: 0 10px 26px rgba(0,0,0,0.38), 0 0 0 5px rgba(255,138,0,0.08);
}
.login-card input::placeholder,
@@ -1034,13 +1046,13 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
gap: 8px;
width: 100%;
padding: 10px 14px;
background: rgba(255,255,255,0.03);
color: var(--ots-text);
background: #f8fafc;
color: #0b1221 !important;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.8);
font-weight: 600;
box-shadow: none;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
box-shadow: 0 10px 22px rgba(2,6,23,0.35);
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}
.login-card .btn-signin .icon,
@@ -1054,15 +1066,16 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
.login-card .btn-signin:hover,
.login-panel .btn-signin:hover {
background: rgba(255,255,255,0.06);
border-color: rgba(255,255,255,0.18);
background: #ffffff;
border-color: rgba(255,255,255,0.95);
transform: translateY(-1px);
}
.login-card .btn-signin:focus,
.login-panel .btn-signin:focus {
outline: none;
box-shadow: 0 6px 18px rgba(0,0,0,0.28), 0 0 0 6px rgba(255,138,0,0.04);
border-color: rgba(255,138,0,0.28);
box-shadow: 0 10px 24px rgba(0,0,0,0.32), 0 0 0 5px rgba(255,138,0,0.18);
border-color: rgba(255,138,0,0.45);
}
.login-card .forgot-link,
@@ -1075,7 +1088,7 @@ body.login, body.login-page, .xibo-login, #login, .login-wrapper {
/* Small screens: compress card padding */
@media (max-width: 520px) {
.login-card, .login-panel { padding: 20px; border-radius: 10px; }
.login-card, .login-panel { padding: 24px; border-radius: 12px; }
.login-card .login-logo .logo-icon { width: 72px; height: 72px; }
}
@@ -1129,7 +1142,7 @@ body.login-page::before {
left: -8%;
top: -6%;
background: radial-gradient(circle at 30% 30%, rgba(79,140,255,0.65), rgba(79,140,255,0.18) 35%, transparent 50%);
animation: ots-blob-move-1 20s ease-in-out infinite alternate !important;
animation: ots-blob-move-1 16s ease-in-out infinite alternate !important;
}
.ots-login-blob--2 {
@@ -1138,7 +1151,7 @@ body.login-page::before {
right: 6%;
bottom: 18%;
background: radial-gradient(circle at 60% 40%, rgba(255,138,0,0.45), rgba(255,138,0,0.14) 36%, transparent 55%);
animation: ots-blob-move-2 26s ease-in-out infinite alternate !important;
animation: ots-blob-move-2 20s ease-in-out infinite alternate !important;
}
.ots-login-blob--3 {
@@ -1147,7 +1160,11 @@ body.login-page::before {
left: 18%;
bottom: -4%;
background: radial-gradient(circle at 40% 60%, rgba(94,200,255,0.28), rgba(94,200,255,0.08) 40%, transparent 60%);
animation: ots-blob-move-3 22s ease-in-out infinite alternate !important;
animation: ots-blob-move-3 18s ease-in-out infinite alternate !important;
}
.ots-login-bg {
animation-duration: 10s !important;
}
/* Disable other animations/transitions on the login page so only blobs animate */

File diff suppressed because it is too large Load Diff

View File

@@ -109,8 +109,11 @@
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
syncSubmenuDisplayForState(nowCollapsed);
updateSidebarNavOffset();
updateSidebarStateClass();
updateSidebarWidth();
setTimeout(updateSidebarWidth, 250);
});
}
@@ -121,14 +124,20 @@
body.classList.remove('ots-sidebar-collapsed');
document.documentElement.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
syncSubmenuDisplayForState(false);
updateSidebarNavOffset();
updateSidebarStateClass();
updateSidebarWidth();
setTimeout(updateSidebarWidth, 250);
});
}
// Initialize sidebar section toggles
initSidebarSectionToggles();
// Inject flyout headers (icon + label) into each submenu for collapsed state
buildFlyoutHeaders();
// Close sidebar when clicking outside on mobile
document.addEventListener('click', function(e) {
if (window.innerWidth <= 768) {
@@ -147,12 +156,10 @@
* This function is kept as a no-op for backward compatibility.
*/
function updateSidebarWidth() {
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
if (window.__otsDebug) {
const sidebar = document.querySelector('.ots-sidebar');
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
}
const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return;
const w = sidebar.offsetWidth;
document.documentElement.style.setProperty('--ots-sidebar-actual-width', w + 'px');
}
/**
@@ -160,39 +167,12 @@
* 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 = '';
}
/* No-op: sidebar uses flex-direction:column so the header and
nav content are separate flex children that never overlap.
Previously this set padding-top:~72px which created a huge gap. */
var nav = document.querySelector('.ots-sidebar .sidebar-nav, .ots-sidebar .ots-sidebar-nav');
if (nav) {
try { nav.style.removeProperty('padding-top'); } catch(e) { nav.style.paddingTop = ''; }
}
}
@@ -217,6 +197,75 @@
}
}
/**
* Build flyout headers for each sidebar-submenu.
* Pulls the icon class(es) and label from the parent group toggle
* and injects a styled header <li> at the top of the submenu.
* Idempotent — skips submenus that already have a header.
*/
function buildFlyoutHeaders() {
var groups = document.querySelectorAll('.sidebar-group');
groups.forEach(function(group) {
var submenu = group.querySelector('.sidebar-submenu');
if (!submenu) return;
// Don't inject twice
if (submenu.querySelector('.flyout-header')) return;
var toggle = group.querySelector('.sidebar-group-toggle');
if (!toggle) return;
// Grab the icon element's class list and the label text
var iconEl = toggle.querySelector('.ots-nav-icon');
var textEl = toggle.querySelector('.ots-nav-text');
if (!textEl) return;
var label = textEl.textContent.trim();
// Build the header <li>
var header = document.createElement('li');
header.className = 'flyout-header';
header.setAttribute('aria-hidden', 'true');
// Clone the icon
if (iconEl) {
var icon = document.createElement('span');
icon.className = iconEl.className; // copies all fa classes
icon.classList.add('flyout-header-icon');
icon.setAttribute('aria-hidden', 'true');
header.appendChild(icon);
}
var text = document.createElement('span');
text.className = 'flyout-header-text';
text.textContent = label;
header.appendChild(text);
submenu.insertBefore(header, submenu.firstChild);
});
}
/**
* When toggling between collapsed/expanded, sync all submenu inline
* display styles so that:
* - Collapsed: no inline display → CSS :hover handles flyouts
* - Expanded: inline display block/none based on is-open state
*/
function syncSubmenuDisplayForState(isCollapsed) {
var groups = document.querySelectorAll('.sidebar-group');
groups.forEach(function(group) {
var submenu = group.querySelector('.sidebar-submenu');
if (!submenu) return;
if (isCollapsed) {
// Remove inline display so CSS visibility/opacity hover rules work
submenu.style.removeProperty('display');
} else {
// Expanded mode: show/hide based on is-open class
var isOpen = group.classList.contains('is-open');
submenu.style.display = isOpen ? 'block' : 'none';
}
});
}
/**
* Initialize sidebar section collapse/expand functionality
*/
@@ -231,7 +280,16 @@
const caret = toggle.querySelector('.sidebar-group-caret');
if (submenu) {
const isOpen = group.classList.contains('is-open');
submenu.style.display = isOpen ? 'block' : 'none';
// Only set inline display when sidebar is NOT collapsed;
// collapsed state uses CSS :hover to show flyout menus.
const sidebarEl = document.querySelector('.ots-sidebar');
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
if (!isCollapsed) {
submenu.style.display = isOpen ? 'block' : 'none';
} else {
// Clear any leftover inline display so CSS :hover can work
submenu.style.removeProperty('display');
}
toggle.setAttribute('aria-expanded', isOpen.toString());
}
@@ -242,6 +300,12 @@
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
if (!submenu) return;
const sidebarEl = document.querySelector('.ots-sidebar');
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
// When collapsed, don't toggle submenus on click — hover handles it
if (isCollapsed) return;
const isOpen = group.classList.contains('is-open');
group.classList.toggle('is-open', !isOpen);
toggle.setAttribute('aria-expanded', (!isOpen).toString());
@@ -257,27 +321,6 @@
});
}
});
// Capture-phase handler to override any conflicting listeners
document.addEventListener('click', function(e) {
const caret = e.target.closest('.sidebar-group-caret');
const toggle = e.target.closest('.sidebar-group-toggle');
const target = toggle || (caret ? caret.closest('.sidebar-group-toggle') : null);
if (!target) return;
e.preventDefault();
e.stopPropagation();
const group = target.closest('.sidebar-group');
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
if (!submenu) return;
const isOpen = group.classList.contains('is-open');
group.classList.toggle('is-open', !isOpen);
target.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
syncSidebarActiveStates();
}, true);
}
function syncSidebarActiveStates() {

View File

@@ -19,12 +19,8 @@
{% trans "About" %}
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
</h2>
<p>
{% trans "An" %}
<a href="https://oribi-tech.com" target="_blank" rel="noopener noreferrer">Oribi Technology Services</a>
{% trans "product." %}
</p>
<p class="text-muted">{% trans "OTS Signs provides a compact, focused interface and hosting for Xibo CMS" %}</p>
<p class="text-muted">{% trans "OTS Signs provides a compact, focused interface for your digital signage network" %}</p>
{% set appVersion = version|default("dev") %}
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
@@ -46,9 +42,9 @@
<div class="about-disclaimer">
<strong>{% trans "Disclaimer:" %}</strong>
{% trans "OTS Signs is an independent product developed by Oribi Technology Services. It is not affiliated with or endorsed by the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo APIs is subject to their terms and conditions." %}
{% trans "OTS Signs is an custom front end developed by Oribi Technology Services for the Xibo CMS. It is not affiliated with the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo is subject to their terms and conditions." %}
<div class="mt-2">
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "Xibo CMS on GitHub" %}</a>
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "View the Xibo CMS source on GitHub" %}</a>
</div>
</div>
</div>

View File

@@ -39,8 +39,8 @@
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add an Application" %}" href="{{ url_for("application.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Application" %}</button>
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add an Application" %}" href="{{ url_for("application.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="applications" class="table table-striped">
<thead>

View File

@@ -30,8 +30,8 @@
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% if scheduleCount > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="scheduling">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="scheduling">
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Scheduling" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -60,8 +60,8 @@
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="media">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="media">
<span class="ots-nav-icon fa fa-picture-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Media" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -108,8 +108,8 @@
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="design">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="design">
<span class="ots-nav-icon fa fa-paint-brush" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Design" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -156,8 +156,8 @@
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view", "display.syncView"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="displays">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="displays">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Displays" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -228,8 +228,8 @@
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view", "tag.view", "font.view"]) %}
{% if countViewable > 0 or userMenuViewable %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="settings">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="settings">
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -330,8 +330,8 @@
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="reporting">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="reporting">
<span class="ots-nav-icon fa fa-bar-chart" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Reporting" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -369,8 +369,8 @@
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="advanced">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="advanced">
<span class="ots-nav-icon fa fa-shield" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Advanced" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
@@ -417,8 +417,8 @@
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group is-open">
<a class="sidebar-group-toggle" href="#" aria-expanded="true" data-group="developer">
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="developer">
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Developer" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>

View File

@@ -33,7 +33,8 @@
<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" href="https://portal.oribi-tech.com" target="_blank" rel="noopener noreferrer" title="{% trans "Client Portal" %}">{% trans "Client Portal" %}</a>
<a class="dropdown-item XiboFormButton" href="{{ url_for("about") }}" title="{% trans "About the CMS" %}">{% trans "About" %}</a>

View File

@@ -104,10 +104,10 @@
{% endif %}
{% endif %}
<div id="content-wrapper">
<div id="content-wrapper"{% if hideNavigation == "1" %} class="no-nav"{% endif %}>
{# Floating top-right actions: notification bell + user menu #}
{% if not forceHide %}
<div class="ots-page-actions">
<div class="ots-page-actions"{% if hideNavigation == "1" %} style="display:none!important"{% endif %}>
{% include "authed-theme-topbar.twig" ignore missing %}
{% if currentUser.featureEnabled("drawer") %}
<div class="ots-topbar-action">

View File

@@ -103,17 +103,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("campaign.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Campaign" %}" href="{{ url_for("campaign.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Campaign" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Campaign" %}" href="{{ url_for("campaign.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="campaigns" class="table table-striped" data-content-type="campaign" data-content-id-name="campaignId" data-state-preference-name="campaignGrid" style="width: 100%;">
<thead>

View File

@@ -60,9 +60,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("command.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Command" %}" href="{{ url_for("command.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Command" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Command" %}" href="{{ url_for("command.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="commands" class="table table-striped" data-state-preference-name="commandGrid">
<thead>

View File

@@ -88,17 +88,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("dataset.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new DataSet" %}" href="{{ url_for("dataSet.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add DataSet" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new DataSet" %}" href="{{ url_for("dataSet.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="datasets" class="table table-striped" data-state-preference-name="dataSetGrid" style="width: 100%;">
<thead>

View File

@@ -62,9 +62,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("daypart.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Daypart" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="dayparts" class="table table-striped" data-state-preference-name="daypartGrid">
<thead>

View File

@@ -256,24 +256,17 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{{ "Open / Close Folder Search options"|trans }}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div class="map-controller d-none pl-1 ots-grid-controller">
<button type="button" id="map_button" class="btn btn-icon btn-primary" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
</div>
<div class="list-controller d-none pl-1 ots-grid-controller">
<button type="button" id="list_button" class="btn btn-icon btn-primary" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "Open / Close Folder Search options"|trans }}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
<button type="button" id="map_button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
<button type="button" id="list_button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a Display via user_code displayed on the Player screen" %}" href="{{ url_for("display.addViaCode.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Display" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a Display via user_code displayed on the Player screen" %}" href="{{ url_for("display.addViaCode.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="displays" class="table table-striped" data-content-type="display" data-content-id-name="displayId" data-state-preference-name="displayGrid" style="width: 100%;">
<thead>

View File

@@ -102,18 +102,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("displaygroup.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Display Group" %}" href="{{ url_for("displayGroup.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Group" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Display Group" %}" href="{{ url_for("displayGroup.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="displaygroups" class="table table-striped" data-content-type="displayGroup" data-content-id-name="displayGroupId" data-state-preference-name="displayGroupGrid" style="width: 100%;">
<thead>

View File

@@ -60,9 +60,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("displayprofile.add") %}
<button class="btn btn-sm btn-info XiboFormButton" title="{% trans "Add a new Display Settings Profile" %}" href="{{ url_for("displayProfile.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Profile" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Display Settings Profile" %}" href="{{ url_for("displayProfile.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="displayProfiles" class="table table-striped" data-state-preference-name="displayProfileGrid">
<thead>

View File

@@ -43,9 +43,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("font.add") %}
<button class="btn btn-sm btn-success" href="#" id="fontUploadForm" title="{% trans "Add a new Font" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Upload Font" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="fontUploadForm" title="{% trans "Add a new Font" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="fonts" class="table table-striped" data-state-preference-name="fontGrid">
<thead>

View File

@@ -157,19 +157,16 @@
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("layout.add") %}
<button class="btn btn-sm btn-success layout-add-button" title="{% trans "Add a new Layout and jump to the layout editor." %}" href="{{ url_for("layout.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Layout" %}</button>
<button class="btn btn-sm btn-info" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i> {% trans "Import" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn layout-add-button" title="{% trans "Add a new Layout and jump to the layout editor." %}" href="{{ url_for("layout.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-info ots-toolbar-btn" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="layouts" class="table table-striped responsive nowrap" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="layoutGrid" style="width: 100%;">
<thead>

View File

@@ -133,23 +133,21 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabledCount(["library.add", "library.modify"]) > 0 or settings.SETTING_LIBRARY_TIDY_ENABLED == 1 %}
{% if currentUser.featureEnabled("library.add") %}
<button class="btn btn-sm btn-success" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Media" %}</button>
<button class="btn btn-sm 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> {% trans "Add URL" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-success ots-toolbar-btn 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-sm btn-warning XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-broom" aria-hidden="true"></i> {% trans "Tidy" %}</button>
<button class="btn btn-sm btn-warning ots-toolbar-btn XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-broom" aria-hidden="true"></i></button>
{% endif %}
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="libraryItems" class="table table-striped responsive nowrap" data-content-type="media" data-content-id-name="mediaId" data-state-preference-name="libraryGrid" style="width: 100%;">
<thead>

View File

@@ -89,17 +89,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("menuBoard.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Menu Board" %}" href="{{ url_for("menuBoard.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Menu Board" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Menu Board" %}" href="{{ url_for("menuBoard.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="menuBoards" class="table table-striped responsive nowrap" data-content-type="menuBoard" data-content-id-name="menuId" data-state-preference-name="menuBoardGrid" style="width: 100%;">
<thead>
@@ -121,6 +119,7 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -39,7 +39,7 @@
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="modules" class="table table-striped" data-state-preference-name="moduleGrid">
<thead>

View File

@@ -25,6 +25,7 @@
--color-text-tertiary: #e2e8f0;
--color-text-inverse: #ffffff;
--color-on-primary: #ffffff;
--ots-sidebar-content-gap: 12px;
color-scheme: dark;
}
@@ -173,24 +174,82 @@ body {
overflow-y: auto;
}
.sidebar-nav {
.sidebar-nav,
.ots-sidebar-nav {
list-style: none;
margin: 0;
padding: 64px 0 60px;
padding: 4px 0 16px;
position: relative;
}
/* 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;
.ots-sidebar ul.sidebar.ots-sidebar-nav {
position: relative !important;
top: auto !important;
bottom: auto !important;
}
/* Ensure nav list starts below header in expanded/collapsed states */
.ots-sidebar.collapsed .ots-sidebar-nav,
.ots-sidebar-collapsed .ots-sidebar-nav {
padding-top: 4px !important;
padding-bottom: 16px !important;
}
.ots-sidebar:not(.collapsed) .ots-sidebar-nav {
padding-top: 4px !important;
}
.sidebar-nav li {
display: block;
}
/* Reset any fixed heights from base theme that cause overlapping */
.ots-sidebar .sidebar-list {
height: auto !important;
}
/* Expanded sidebar submenu styling (prevents overlap) */
.ots-sidebar .sidebar-submenu {
list-style: none;
margin: 4px 0 8px;
padding: 0 0 0 12px;
border-left: 1px solid var(--ots-sidebar-border);
display: none;
}
.ots-sidebar .sidebar-group.is-open .sidebar-submenu,
.ots-sidebar .sidebar-group-toggle[aria-expanded="true"] ~ .sidebar-submenu,
.ots-sidebar .sidebar-group-toggle[aria-expanded="true"] + .sidebar-submenu {
display: block !important;
}
.ots-sidebar .sidebar-submenu .sidebar-list {
margin: 0;
padding: 0;
}
.ots-sidebar .sidebar-submenu .sidebar-list > a {
display: flex;
align-items: center;
gap: 12px;
margin: 4px 0;
background: var(--ots-sidebar-submenu-bg);
width: 100%;
box-sizing: border-box;
padding: 8px 48px 8px 28px;
border-left: 4px solid var(--ots-sidebar-submenu-border);
border-radius: var(--ots-sidebar-item-radius);
color: var(--ots-sidebar-link);
font-weight: 500;
text-decoration: none;
min-height: var(--ots-sidebar-item-height);
}
.ots-sidebar .sidebar-submenu .sidebar-list > a:hover {
background: var(--ots-sidebar-submenu-hover-bg);
color: var(--ots-sidebar-link-hover-text);
}
/* Compatibility: Xibo sidebar markup uses `sidebar-list`, `sidebar-main`, `sidebar-title`.
Map those into the modern `.nav-item/.nav-text/.nav-icon` style system so styles apply.
*/
@@ -620,10 +679,10 @@ body:not(.ots-sidebar-collapsed) .ots-topbar-strip {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
width: 28px !important;
height: 28px !important;
padding: 0 !important;
border-radius: 8px !important;
border-radius: 6px !important;
color: var(--color-text-secondary) !important;
transition: all 150ms ease !important;
border: none !important;
@@ -648,11 +707,11 @@ body:not(.ots-sidebar-collapsed) .ots-topbar-strip {
/* Avatar in topbar */
.ots-topbar-action img.nav-avatar {
display: block !important;
width: 32px !important;
height: 32px !important;
width: 24px !important;
height: 24px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid transparent !important;
border: 1px solid transparent !important;
transition: border-color 150ms ease !important;
}
@@ -798,6 +857,12 @@ body:not(.ots-sidebar-collapsed) .ots-topbar-strip {
/* Ensure sidebar items are visible and above header when sidebar is collapsed */
.ots-sidebar.collapsed {
z-index: 1200;
overflow: visible !important;
}
/* Allow flyout menus to escape the sidebar-content scroll container */
.ots-sidebar.collapsed .sidebar-content {
overflow: visible !important;
}
.ots-sidebar.collapsed .ots-nav-icon {
@@ -1965,6 +2030,80 @@ body .panel .panel-heading,
overflow: hidden;
}
/* ============================================================================
TOOLBAR - Icon-only action buttons
============================================================================ */
.ots-table-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px 12px;
flex-wrap: wrap;
}
.ots-table-toolbar .ots-toolbar-btn,
.ots-table-toolbar .btn.ots-toolbar-btn {
width: 36px;
height: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 15px;
line-height: 1;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.ots-table-toolbar .ots-toolbar-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.ots-table-toolbar .ots-toolbar-btn:active {
transform: translateY(0);
}
/* Folder toggle in toolbar */
.ots-table-toolbar .folder-controller.btn {
width: 36px;
height: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 15px;
line-height: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* Breadcrumbs in toolbar */
.ots-table-toolbar #breadcrumbs {
font-size: 13px;
color: var(--color-text-tertiary);
margin: 0;
padding: 0;
}
/* View toggle buttons (map/list) */
.ots-table-toolbar .map-controller,
.ots-table-toolbar .list-controller {
width: 36px;
height: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 15px;
line-height: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
#datatable-container {
width: 100%;
overflow-x: auto;
@@ -3063,6 +3202,44 @@ small {
border-color: var(--color-primary) !important;
}
/* Properties Panel - Override layout-editor bundle backgrounds */
.properties-panel-container #properties-panel {
background-color: var(--color-surface) !important;
}
.properties-panel-container #properties-panel #properties-panel-form-container {
background: var(--color-surface) !important;
background-color: var(--color-surface) !important;
}
.properties-panel-container #properties-panel .loading-container {
background: var(--color-surface) !important;
}
/* Properties Panel Tabs - Override layout-editor bundle (2-ID specificity) */
.properties-panel-container #properties-panel #properties-panel-form-container .form-container .nav > li > a {
background-color: var(--color-surface) !important;
background: var(--color-surface) !important;
color: var(--color-text-secondary) !important;
border-color: var(--color-primary) !important;
border-width: 0 0 2px 0 !important;
}
.properties-panel-container #properties-panel #properties-panel-form-container .form-container .nav > li > a:hover {
background-color: rgba(59, 130, 246, 0.15) !important;
background: rgba(59, 130, 246, 0.15) !important;
color: var(--color-text-primary) !important;
}
.properties-panel-container #properties-panel #properties-panel-form-container .form-container .nav > li > a.active,
.properties-panel-container #properties-panel #properties-panel-form-container .form-container .nav > li > a.active:hover {
background-color: var(--color-primary) !important;
background: var(--color-primary) !important;
color: white !important;
border-color: var(--color-primary) !important;
opacity: 100%;
}
/* Cards */
.card {
background-color: var(--color-surface) !important;
@@ -3168,7 +3345,7 @@ a.text-muted:hover {
:root {
color-scheme: dark;
--ots-bg: #0b111a;
--ots-bg: var(--color-background);
--ots-surface: #141c2b;
--ots-surface-2: #1b2436;
--ots-surface-3: #222c3f;
@@ -3195,6 +3372,25 @@ a.text-muted:hover {
--ots-transition: 160ms ease;
}
/* Light mode token overrides so layout backgrounds follow theme */
body.ots-light-mode {
--ots-bg: var(--color-background);
--ots-surface: var(--color-surface);
--ots-surface-2: var(--color-surface-elevated);
--ots-surface-3: var(--color-surface-elevated);
--ots-border: var(--color-border);
--ots-border-soft: var(--color-border-light);
--ots-text: var(--color-text-primary);
--ots-text-muted: var(--color-text-secondary);
--ots-text-faint: var(--color-text-tertiary);
--ots-primary: var(--color-primary);
--ots-primary-2: var(--color-primary-dark);
--ots-success: var(--color-success);
--ots-warning: var(--color-warning);
--ots-danger: var(--color-danger);
--ots-info: var(--color-info);
}
/* =============================================================================
GLOBAL
============================================================================= */
@@ -3254,6 +3450,11 @@ hr {
background: var(--ots-bg);
}
#content-wrapper {
padding-left: var(--ots-sidebar-content-gap);
box-sizing: border-box;
}
.page-content {
padding-top: 24px;
}
@@ -3777,3 +3978,101 @@ textarea:focus {
font-size: 13px;
color: var(--ots-text-muted);
}
/* =============================================================================
NUCLEAR: Collapsed Sidebar Icon Centering
=============================================================================
This section is placed at the VERY END of the last-loaded stylesheet
(override-styles.twig is inlined after all linked CSS files).
It uses high-specificity selectors with !important to guarantee
these rules win the cascade over everything else.
============================================================================= */
/* The 44×44 icon button — grid centering is the most robust method */
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-list > a,
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-group > .sidebar-group-toggle {
display: grid !important;
place-items: center !important;
grid-template-columns: 1fr !important;
grid-template-rows: 1fr !important;
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
max-width: 40px !important;
min-height: 40px !important;
max-height: 40px !important;
margin: 2px auto !important;
padding: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
border: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
float: none !important;
text-indent: 0 !important;
gap: 0 !important;
column-gap: 0 !important;
row-gap: 0 !important;
box-sizing: border-box !important;
}
/* The icon span — centered by grid parent, centers its own ::before glyph */
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-list > a > .ots-nav-icon,
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-group > .sidebar-group-toggle > .ots-nav-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 20px !important;
height: 20px !important;
font-size: 20px !important;
line-height: 1 !important;
margin: 0 !important;
padding: 0 !important;
text-indent: 0 !important;
text-align: center !important;
float: none !important;
opacity: 1 !important;
}
/* The FA ::before glyph — block-level and centered */
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-list > a > .ots-nav-icon::before,
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-group > .sidebar-group-toggle > .ots-nav-icon::before {
display: block !important;
text-align: center !important;
width: 100% !important;
line-height: 1 !important;
margin: 0 !important;
padding: 0 !important;
text-indent: 0 !important;
}
/* The <li> wrapper — full width, centers the button */
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-list,
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav > .sidebar-group {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
text-indent: 0 !important;
float: none !important;
height: auto !important;
list-style: none !important;
box-sizing: border-box !important;
}
/* The <ul> nav list — full width, no spacing that could offset children */
#sidebar-wrapper.ots-sidebar.collapsed > .sidebar-content > .ots-sidebar-nav {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin: 0 !important;
text-indent: 0 !important;
list-style: none !important;
}

View File

@@ -62,9 +62,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("playersoftware.add") %}
<button class="btn btn-sm btn-success" href="#" id="playerSoftwareUploadForm" title="{% trans "Upload a new Player Software file" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Upload Software" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="playerSoftwareUploadForm" title="{% trans "Upload a new Player Software file" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="playerSoftwareItems" class="table table-striped" data-state-preference-name="playerSoftwareGrid">
<thead>

View File

@@ -132,17 +132,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("playlist.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Playlist" %}" href="{{ url_for("playlist.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Playlist" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Playlist" %}" href="{{ url_for("playlist.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="playlists" class="table table-striped" data-content-type="playlist"
data-content-id-name="playlistId" data-state-preference-name="playlistGrid" style="width: 100%;">

View File

@@ -60,9 +60,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("resolution.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i> {% trans "Add Resolution" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="resolutions" class="table table-striped" data-state-preference-name="resolutionGrid">
<thead>

View File

@@ -199,9 +199,9 @@
<div class="XiboSchedule card dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("schedule.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Scheduled event" %}" href="{{ url_for("schedule.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i> {% trans "Add Event" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Scheduled event" %}" href="{{ url_for("schedule.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">

View File

@@ -23,7 +23,7 @@
<div class="widget-body ots-displays-body">
<div class="ots-table-toolbar">
{% if settings.SETTING_LIBRARY_TIDY_ENABLED %}
<button class="btn btn-sm btn-danger XiboFormButton" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("maintenance.libraryTidy.form") }}"><i class="fa fa-trash" aria-hidden="true"></i> {% trans "Tidy Library" %}</button>
<button class="btn btn-sm btn-danger ots-toolbar-btn XiboFormButton" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("maintenance.libraryTidy.form") }}"><i class="fa fa-trash" aria-hidden="true"></i></button>
{% endif %}
</div>
<div class="row">

View File

@@ -74,17 +74,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("display.syncAdd") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Sync Group" %}" href="{{ url_for("syncgroup.form.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Sync Group" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Sync Group" %}" href="{{ url_for("syncgroup.form.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="syncgroups" class="table table-striped" data-content-type="syncGroup" data-content-id-name="syncGroupId" data-state-preference-name="syncGroupGrid" style="width: 100%;">
<thead>

View File

@@ -48,8 +48,8 @@
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Tag" %}" href="{{ url_for("tag.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Tag" %}</button>
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Tag" %}" href="{{ url_for("tag.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="tags" class="table table-striped">
<thead>

View File

@@ -30,9 +30,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if settings.TASK_CONFIG_LOCKED_CHECKB == 0 or settings.TASK_CONFIG_LOCKED_CHECKB == "Unchecked" %}
<button class="btn btn-sm btn-success XiboFormButton" href="{{ url_for("task.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Task" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" href="{{ url_for("task.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="tasks" class="table table-striped" data-state-preference-name="taskGrid">
<thead>

View File

@@ -74,17 +74,15 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
<div id="breadcrumbs"></div>
{% if currentUser.featureEnabled("template.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Template and jump to the layout editor." %}" href="{{ url_for("template.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Template" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Template and jump to the layout editor." %}" href="{{ url_for("template.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="templates" class="table table-striped" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="templateGrid" style="width: 100%;">
<thead>

View File

@@ -34,12 +34,10 @@
* This function is kept as a no-op for backward compatibility.
*/
function updateSidebarWidth() {
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
if (window.__otsDebug) {
const sidebar = document.querySelector('.ots-sidebar');
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
}
const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return;
const w = sidebar.offsetWidth;
document.documentElement.style.setProperty('--ots-sidebar-actual-width', w + 'px');
}
/**
@@ -47,38 +45,41 @@
* 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');
/* No-op: sidebar uses flex-direction:column so the header and
nav content are separate flex children that never overlap.
Previously this set padding-top:~72px which created a huge gap. */
var nav = document.querySelector('.ots-sidebar .sidebar-nav, .ots-sidebar .ots-sidebar-nav');
if (nav) {
try { nav.style.removeProperty('padding-top'); } catch(e) { nav.style.paddingTop = ''; }
}
}
/**
* Detect whether the playlist/layout editor modal is open and toggle
* body.ots-playlist-editor-active accordingly. Because the editor is
* loaded via AJAX into #editor-container, a one-shot check at page-load
* is not enough we use a MutationObserver that watches for DOM changes.
*/
function updatePlaylistEditorBackground() {
var isActive = !!document.querySelector('.editor-modal, #playlist-editor, #layout-editor');
document.body.classList.toggle('ots-playlist-editor-active', isActive);
}
/* Start a MutationObserver that fires updatePlaylistEditorBackground
whenever children are added to or removed from the page. */
(function initEditorObserver() {
// Run once immediately
updatePlaylistEditorBackground();
var target = document.body;
if (!target) return;
var editorObs = new MutationObserver(function() {
updatePlaylistEditorBackground();
});
editorObs.observe(target, { childList: true, subtree: true });
})();
/**
* DISABLED: Cleanup function to remove inline styles that were forcing incorrect margins
* The sidebar layout is now controlled entirely by CSS variables and margin-left.
@@ -225,8 +226,12 @@
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
syncSubmenuDisplayForState(nowCollapsed);
updateSidebarNavOffset();
updateSidebarStateClass();
// Update measured width immediately and again after CSS transition
updateSidebarWidth();
setTimeout(updateSidebarWidth, 250);
});
}
@@ -237,8 +242,12 @@
body.classList.remove('ots-sidebar-collapsed');
document.documentElement.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
syncSubmenuDisplayForState(false);
updateSidebarNavOffset();
updateSidebarStateClass();
// Update measured width immediately and again after CSS transition
updateSidebarWidth();
setTimeout(updateSidebarWidth, 250);
});
}
@@ -259,6 +268,69 @@
updateSidebarStateClass();
}
/**
* Build flyout headers for each sidebar-submenu.
* Pulls the icon class(es) and label from the parent group toggle
* and injects a styled header <li> at the top of the submenu.
* Idempotent — skips submenus that already have a header.
*/
function buildFlyoutHeaders() {
var groups = document.querySelectorAll('.sidebar-group');
groups.forEach(function(group) {
var submenu = group.querySelector('.sidebar-submenu');
if (!submenu) return;
if (submenu.querySelector('.flyout-header')) return;
var toggle = group.querySelector('.sidebar-group-toggle');
if (!toggle) return;
var iconEl = toggle.querySelector('.ots-nav-icon');
var textEl = toggle.querySelector('.ots-nav-text');
if (!textEl) return;
var label = textEl.textContent.trim();
var header = document.createElement('li');
header.className = 'flyout-header';
header.setAttribute('aria-hidden', 'true');
if (iconEl) {
var icon = document.createElement('span');
icon.className = iconEl.className;
icon.classList.add('flyout-header-icon');
icon.setAttribute('aria-hidden', 'true');
header.appendChild(icon);
}
var text = document.createElement('span');
text.className = 'flyout-header-text';
text.textContent = label;
header.appendChild(text);
submenu.insertBefore(header, submenu.firstChild);
});
}
/**
* When toggling between collapsed/expanded, sync all submenu inline
* display styles so that:
* - Collapsed: no inline display → CSS :hover handles flyouts
* - Expanded: inline display block/none based on is-open state
*/
function syncSubmenuDisplayForState(isCollapsed) {
var groups = document.querySelectorAll('.sidebar-group');
groups.forEach(function(group) {
var submenu = group.querySelector('.sidebar-submenu');
if (!submenu) return;
if (isCollapsed) {
submenu.style.removeProperty('display');
} else {
var isOpen = group.classList.contains('is-open');
submenu.style.display = isOpen ? 'block' : 'none';
}
});
}
/**
* Initialize sidebar section collapse/expand functionality
*/
@@ -268,10 +340,15 @@
groupToggles.forEach(toggle => {
const group = toggle.closest('.sidebar-group');
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
const caret = toggle.querySelector('.sidebar-group-caret');
if (submenu) {
const isOpen = group.classList.contains('is-open');
submenu.style.display = isOpen ? 'block' : 'none';
const sidebarEl = document.querySelector('.ots-sidebar');
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
if (!isCollapsed) {
submenu.style.display = isOpen ? 'block' : 'none';
} else {
submenu.style.removeProperty('display');
}
toggle.setAttribute('aria-expanded', isOpen.toString());
}
@@ -282,39 +359,16 @@
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
if (!submenu) return;
const sidebarEl = document.querySelector('.ots-sidebar');
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
if (isCollapsed) return;
const isOpen = group.classList.contains('is-open');
group.classList.toggle('is-open', !isOpen);
toggle.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
requestAnimationFrame(updateSidebarWidth);
});
if (caret) {
caret.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggle.click();
});
}
});
// Capture-phase handler to override any conflicting listeners
document.addEventListener('click', function(e) {
const caret = e.target.closest('.sidebar-group-caret');
const toggle = e.target.closest('.sidebar-group-toggle');
const target = toggle || (caret ? caret.closest('.sidebar-group-toggle') : null);
if (!target) return;
e.preventDefault();
e.stopPropagation();
const group = target.closest('.sidebar-group');
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
if (!submenu) return;
const isOpen = group.classList.contains('is-open');
group.classList.toggle('is-open', !isOpen);
target.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
});
}
@@ -1001,92 +1055,145 @@
}
/**
* Move DataTable row dropdown menus to <body> so they escape
* any overflow:hidden / overflow:auto ancestor containers.
* DataTable row action dropdowns — fully managed by OTS theme.
*
* Xibo renders the row action button as:
* <div class="btn-group pull-right dropdown-menu-container">
* <button class="btn btn-white dropdown-toggle" data-toggle="dropdown">
* <div class="dropdown-menu dropdown-menu-right">...items...</div>
* </div>
* Bootstrap 4 + Popper.js positions menus with transform: translate3d(),
* but the theme CSS sets transform: none !important which breaks that.
* Detaching the menu to <body> also triggers Bootstrap's hide event.
*
* Bootstrap fires shown/hide.bs.dropdown on the toggle's parent element
* (the .btn-group). We listen on document with a selector that matches
* the actual Xibo markup.
* Solution: intercept the click on the toggle button in the capture phase
* (before Bootstrap sees it), prevent Bootstrap from handling it, and
* manage show/hide/position entirely ourselves.
*/
function initRowDropdowns() {
var DROPDOWN_PARENT_SEL = '.dropdown-menu-container, .btn-group';
var SCOPE_SEL = '.XiboData ' + DROPDOWN_PARENT_SEL
+ ', .XiboGrid ' + DROPDOWN_PARENT_SEL
+ ', #datatable-container ' + DROPDOWN_PARENT_SEL
+ ', .dataTables_wrapper ' + DROPDOWN_PARENT_SEL;
var TOGGLE_SEL = '.dropdown-menu-container [data-toggle="dropdown"], .btn-group [data-toggle="dropdown"]';
var activeMenu = null; // currently open menu element (in <body>)
var activeParent = null; // original parent (.btn-group / .dropdown-menu-container)
var activeTrigger = null;
$(document).on('shown.bs.dropdown', SCOPE_SEL, function(e) {
var $container = $(this);
var $trigger = $container.find('[data-toggle="dropdown"]');
var $menu = $container.find('.dropdown-menu');
if (!$menu.length || !$trigger.length) return;
function openMenu(trigger) {
closeMenu(); // close any previously open menu first
// Mark the menu so we can style it and find it later
$menu.addClass('ots-row-dropdown');
var $trigger = $(trigger);
var $parent = $trigger.closest('.dropdown-menu-container, .btn-group');
var $menu = $parent.find('.dropdown-menu').first();
if (!$menu.length) return;
// Store original parent so we can put it back on close
$menu.data('ots-original-parent', $container);
activeTrigger = trigger;
activeParent = $parent[0];
// Get trigger position in viewport
var btnRect = $trigger[0].getBoundingClientRect();
// Snapshot button position while menu is still in the normal DOM
var btnRect = trigger.getBoundingClientRect();
// Move to body
// Detach menu and append to body so it escapes overflow:hidden
$menu.detach().appendTo('body');
activeMenu = $menu[0];
// Position below the trigger button, aligned to the right edge
// Make visible-but-hidden so we can measure it
activeMenu.style.cssText = 'display:block !important; visibility:hidden !important; position:fixed !important; transform:none !important;';
var menuW = $menu.outerWidth() || 180;
var menuH = $menu.outerHeight() || 200;
// Compute position: below button, right-aligned to button's right edge
var top = btnRect.bottom + 2;
var left = btnRect.right - $menu.outerWidth();
var left = btnRect.right - menuW;
// Keep within viewport bounds
// Viewport bounds check
if (left < 8) left = 8;
if (top + $menu.outerHeight() > window.innerHeight - 8) {
// Open upward if no room below
top = btnRect.top - $menu.outerHeight() - 2;
if (left + menuW > window.innerWidth - 8) left = window.innerWidth - menuW - 8;
if (top + menuH > window.innerHeight - 8) {
top = btnRect.top - menuH - 2; // flip above the button
}
if (top < 8) top = 8;
$menu.css({
position: 'fixed',
top: top + 'px',
left: left + 'px',
display: 'block'
});
});
// Apply final position — every property with !important
activeMenu.style.cssText = [
'position:fixed !important',
'top:' + top + 'px !important',
'left:' + left + 'px !important',
'right:auto !important',
'bottom:auto !important',
'display:block !important',
'visibility:visible !important',
'transform:none !important',
'will-change:auto !important',
'margin:0 !important',
'z-index:2147483647 !important'
].join(';') + ';';
// When the dropdown closes, move the menu back to its original parent
$(document).on('hide.bs.dropdown', SCOPE_SEL, function(e) {
var $container = $(this);
var $menu = $('body > .dropdown-menu.ots-row-dropdown').filter(function() {
var orig = $(this).data('ots-original-parent');
return orig && orig[0] === $container[0];
});
if ($menu.length) {
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
$menu.detach().appendTo($container);
$menu.addClass('ots-row-dropdown show');
$parent.addClass('show');
}
function closeMenu() {
if (!activeMenu) return;
var $menu = $(activeMenu);
var $parent = $(activeParent);
// Clear all inline styles
activeMenu.style.cssText = '';
$menu.removeClass('ots-row-dropdown show');
// Move menu back to its original parent
$menu.detach().appendTo($parent);
$parent.removeClass('show open');
activeMenu = null;
activeParent = null;
activeTrigger = null;
}
// Intercept clicks in CAPTURE phase — runs BEFORE Bootstrap's handler.
document.addEventListener('click', function(e) {
var toggle = e.target.closest(TOGGLE_SEL);
if (!toggle) {
// Click was not on a toggle — close any open menu
// (unless click is inside the open menu itself)
if (activeMenu && !e.target.closest('.ots-row-dropdown')) {
closeMenu();
}
return;
}
// Only handle toggles inside DataTable areas
var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
|| toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
if (!inTable) return; // not a row dropdown — let Bootstrap handle it
// Prevent Bootstrap from handling this dropdown
e.preventDefault();
e.stopImmediatePropagation();
// Toggle behaviour: if same trigger, close; otherwise open
if (activeTrigger === toggle && activeMenu) {
closeMenu();
} else {
openMenu(toggle);
}
}, true); // ← true = capture phase
// Close on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && activeMenu) {
closeMenu();
}
});
// Also close any orphaned body-appended dropdown on outside click
$(document).on('click', function(e) {
var $openMenus = $('body > .dropdown-menu.ots-row-dropdown');
if (!$openMenus.length) return;
// If the click is inside the menu itself, let it through
if ($(e.target).closest('.ots-row-dropdown').length) return;
$openMenus.each(function() {
var $menu = $(this);
var $parent = $menu.data('ots-original-parent');
if ($parent && $parent.length) {
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
$menu.detach().appendTo($parent);
$parent.removeClass('open show');
}
});
// Close on any scroll (window or scrollable ancestor)
window.addEventListener('scroll', function() {
if (activeMenu) closeMenu();
}, true);
// Block Bootstrap's show/hide events for DataTable row dropdowns
// so it doesn't interfere with our manual management.
$(document).on('show.bs.dropdown hide.bs.dropdown', function(e) {
var toggle = e.relatedTarget;
if (!toggle) return;
var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
|| toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
if (inTable) {
e.preventDefault();
}
});
}
@@ -1096,6 +1203,7 @@
function init() {
initSidebarToggle();
initSidebarSectionToggles();
buildFlyoutHeaders();
initThemeToggle();
initDropdowns();
initRowDropdowns();
@@ -1107,11 +1215,13 @@
initChartSafeguard();
updateSidebarWidth();
updateSidebarNavOffset();
updatePlaylistEditorBackground();
// updateSidebarGap() disabled - use CSS variables instead
initUserProfileQrFix();
var debouncedUpdate = debounce(function() {
updateSidebarNavOffset();
updateSidebarWidth();
updatePlaylistEditorBackground();
// updateSidebarGap() disabled - use CSS variables instead
}, 120);
window.addEventListener('resize', debouncedUpdate);

View File

@@ -29,7 +29,7 @@
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="transitions" class="table table-striped">
<thead>

View File

@@ -58,9 +58,9 @@
{% else %}
{% set addUserFormUrl = url_for("user.onboarding.form") %}
{% endif %}
<button id="user-add-button" class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new User" %}" href="{{ addUserFormUrl }}"><i class="fa fa-user-plus" aria-hidden="true"></i> {% trans "Add User" %}</button>
<button id="user-add-button" class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new User" %}" href="{{ addUserFormUrl }}"><i class="fa fa-user-plus" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="users" class="table table-striped" data-state-preference-name="userGrid">
<thead>

View File

@@ -40,9 +40,9 @@
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.isSuperAdmin() %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new User Group" %}" href="{{ url_for("group.add.form") }}"><i class="fa fa-users" aria-hidden="true"></i> {% trans "Add Group" %}</button>
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new User Group" %}" href="{{ url_for("group.add.form") }}"><i class="fa fa-users" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="userGroups" class="table table-striped" data-state-preference-name="userGroupGrid">
<thead>