Enhance OTS Signage Theme with Icon Dashboard and UI Improvements

- Updated theme.js to refine dropdown initialization, excluding user menu handling.
- Modified authed.twig to allow CSS variable theming for background and text colors, and adjusted content wrapper classes based on navigation state.
- Adjusted override-styles.twig to improve layout responsiveness and ensure proper styling for horizontal navigation mode.
- Enhanced theme-scripts.twig to improve user menu functionality, including repositioning and click handling.
- Introduced a new dashboard-icon-page.twig for a stylized icon dashboard, featuring card-based buttons for quick access to various functionalities, along with custom styles for better user experience.
This commit is contained in:
Matt Batchelder
2026-02-09 18:47:14 -05:00
parent 86030cb881
commit 54e092ec01
7 changed files with 1820 additions and 103 deletions

View File

@@ -52,7 +52,9 @@
})();
</script>
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
<style nonce="{{ cspNonce }}">
/* Let the CSS variable theming (light/dark) control page background */
html,body{background-color:var(--color-background,#0f172a)!important;color:var(--color-text-primary,#ffffff)!important}
/* Hide the old topbar strip entirely — actions are now in .ots-page-actions */
.row.header.header-side,
.ots-topbar-strip { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }
@@ -104,9 +106,10 @@
{% endif %}
{% endif %}
<div id="content-wrapper"{% if hideNavigation == "1" %} class="no-nav"{% endif %}>
<div id="content-wrapper" class="{% if hideNavigation == "1" %}no-nav{% endif %}{% if horizontalNav %} ots-horizontal-nav{% endif %}">
{# Floating top-right actions: notification bell + user menu #}
{% if not forceHide %}
{# Hidden when horizontal nav is active — the navbar already has these controls #}
{% if not forceHide and not horizontalNav %}
<div class="ots-page-actions"{% if hideNavigation == "1" %} style="display:none!important"{% endif %}>
{% include "authed-theme-topbar.twig" ignore missing %}
{% if currentUser.featureEnabled("drawer") %}

View File

@@ -0,0 +1,461 @@
{#
/**
* OTS Signage Theme - Icon Dashboard Override
*
* Custom stylized icon dashboard that uses card-based buttons
* matching the OTS dashboard design system.
*
* Based on Xibo CMS dashboard-icon-page.twig
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block pageContent %}
{% include "theme-dashboard-message.twig" ignore missing %}
<div class="dashboard-page">
<div class="page-header">
<h1>{% trans "Dashboard" %}</h1>
<p class="text-muted">{% trans "Quick access to all areas of your signage network" %}</p>
</div>
{# ── Scheduling ────────────────────────────────────────────── #}
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% if scheduleCount > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-calendar"></i> {% trans "Scheduling" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("schedule.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("schedule.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-calendar-check-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Schedule" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage event schedules" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("daypart.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("daypart.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-clock-o"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Dayparting" %}</span>
<span class="icon-dash-card-desc">{% trans "Define time slots" %}</span>
</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
{# ── Design ────────────────────────────────────────────────── #}
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-paint-brush"></i> {% trans "Design" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("campaign.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("campaign.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-bullhorn"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Campaigns" %}</span>
<span class="icon-dash-card-desc">{% trans "Organise layout playlists" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("layout.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-columns"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Layouts" %}</span>
<span class="icon-dash-card-desc">{% trans "Design screen layouts" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("template.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-clone"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Templates" %}</span>
<span class="icon-dash-card-desc">{% trans "Reusable layout templates" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("resolution.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--teal">
<i class="fa fa-expand"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Resolutions" %}</span>
<span class="icon-dash-card-desc">{% trans "Screen resolution presets" %}</span>
</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
{# ── Library ───────────────────────────────────────────────── #}
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-picture-o"></i> {% trans "Library" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("library.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("library.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-image"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Library" %}</span>
<span class="icon-dash-card-desc">{% trans "Upload & manage media" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("playlist.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("playlist.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-list"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Playlists" %}</span>
<span class="icon-dash-card-desc">{% trans "Content playlists" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("dataset.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-database"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "DataSets" %}</span>
<span class="icon-dash-card-desc">{% trans "Tabular data sources" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("menuBoard.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--red">
<i class="fa fa-cutlery"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Menu Boards" %}</span>
<span class="icon-dash-card-desc">{% trans "Digital menu management" %}</span>
</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
{# ── Displays ──────────────────────────────────────────────── #}
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view"]) %}
{% if countViewable > 0 %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-desktop"></i> {% trans "Displays" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("displays.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("display.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--green">
<i class="fa fa-desktop"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Displays" %}</span>
<span class="icon-dash-card-desc">{% trans "Manage all screens" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displaygroup.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
<i class="fa fa-object-group"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Groups" %}</span>
<span class="icon-dash-card-desc">{% trans "Organise screen groups" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("displayprofile.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
<i class="fa fa-cog"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Display Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "Player configuration profiles" %}</span>
</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
{# ── Administration ────────────────────────────────────────── #}
{% set showAdmin = false %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
{% set showAdmin = true %}
{% endif %}
{% if currentUser.isSuperUser() %}
{% set showAdmin = true %}
{% endif %}
{% if showAdmin %}
<div class="icon-dash-section">
<h3 class="section-title"><i class="fa fa-cogs"></i> {% trans "Administration" %}</h3>
<div class="icon-dash-grid">
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("user.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
<i class="fa fa-users"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Users" %}</span>
<span class="icon-dash-card-desc">{% trans "User accounts & permissions" %}</span>
</div>
</a>
{% endif %}
{% if currentUser.isSuperUser() %}
<a class="icon-dash-card dashboard-card" href="{{ url_for("admin.view") }}">
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
<i class="fa fa-cogs"></i>
</div>
<div class="icon-dash-card-body">
<span class="icon-dash-card-title">{% trans "Settings" %}</span>
<span class="icon-dash-card-desc">{% trans "CMS system configuration" %}</span>
</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
<style>
/* ===================================================================
ICON DASHBOARD Card Button Styles
Matches the OTS dashboard-card design system
=================================================================== */
/* Section spacing */
.icon-dash-section {
margin-top: 32px;
}
.icon-dash-section:first-of-type {
margin-top: 24px;
}
/* Grid layout responsive card grid */
.icon-dash-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 18px;
}
/* Individual card inherits .dashboard-card base from override.css */
.icon-dash-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 18px;
padding: 22px 24px;
text-decoration: none !important;
color: var(--color-text-primary) !important;
cursor: pointer;
position: relative;
overflow: hidden;
/* Override rigid dashboard-card flex-direction:column if set */
flex-direction: row !important;
}
/* Subtle radial glow matching kpi-card--modern */
.icon-dash-card::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.10), transparent 60%);
pointer-events: none;
transition: opacity 0.3s ease;
opacity: 0;
}
.icon-dash-card:hover::after {
opacity: 1;
}
/* Icon container */
.icon-dash-card-icon {
flex-shrink: 0;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
font-size: 22px;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.icon-dash-card:hover .icon-dash-card-icon {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}
/* Icon colour variants */
.icon-dash-card-icon--blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.28), rgba(59, 130, 246, 0.12));
color: #60a5fa;
}
.icon-dash-card-icon--green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.28), rgba(16, 185, 129, 0.12));
color: #34d399;
}
.icon-dash-card-icon--orange {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.28), rgba(245, 158, 11, 0.12));
color: #fbbf24;
}
.icon-dash-card-icon--red {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.28), rgba(239, 68, 68, 0.12));
color: #f87171;
}
.icon-dash-card-icon--purple {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.28), rgba(124, 58, 237, 0.12));
color: #a78bfa;
}
.icon-dash-card-icon--indigo {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.28), rgba(99, 102, 241, 0.12));
color: #818cf8;
}
.icon-dash-card-icon--teal {
background: linear-gradient(135deg, rgba(20, 184, 166, 0.28), rgba(20, 184, 166, 0.12));
color: #2dd4bf;
}
/* Text area */
.icon-dash-card-body {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
/* Reset inherited dashboard-card body padding */
padding: 0 !important;
background: transparent !important;
}
.icon-dash-card-title {
font-size: 15px;
font-weight: 700;
color: var(--color-text-primary);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon-dash-card-desc {
font-size: 12px;
font-weight: 400;
color: var(--color-text-tertiary);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hover effects matching action-card--modern */
.icon-dash-card:hover {
border-color: rgba(59, 130, 246, 0.45) !important;
transform: translateY(-3px);
box-shadow: 0 20px 40px rgba(8, 15, 30, 0.45) !important;
}
.icon-dash-card:active {
transform: translateY(0px);
box-shadow: 0 10px 20px rgba(8, 15, 30, 0.35) !important;
}
/* Section title with icon */
.icon-dash-section .section-title i {
margin-right: 8px;
opacity: 0.65;
}
/* ── Light mode overrides ─────────────────────────────────────── */
body.ots-light-mode .icon-dash-card {
background: linear-gradient(180deg, #ffffff, #f8fafc) !important;
border-color: rgba(148, 163, 184, 0.25) !important;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06) !important;
}
body.ots-light-mode .icon-dash-card:hover {
background: linear-gradient(180deg, #ffffff, #f1f5f9) !important;
border-color: rgba(59, 130, 246, 0.4) !important;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1) !important;
}
body.ots-light-mode .icon-dash-card-desc {
color: #64748b;
}
/* ── Responsive adjustments ───────────────────────────────────── */
@media (max-width: 768px) {
.icon-dash-grid {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.icon-dash-card {
padding: 16px 18px;
gap: 14px;
}
.icon-dash-card-icon {
width: 44px;
height: 44px;
font-size: 18px;
border-radius: 12px;
}
.icon-dash-card-title {
font-size: 13px;
}
.icon-dash-card-desc {
display: none;
}
}
@media (max-width: 480px) {
.icon-dash-grid {
grid-template-columns: 1fr;
}
.icon-dash-card-desc {
display: block;
}
}
</style>
{% endblock %}

View File

@@ -468,7 +468,7 @@ body {
align-items: center;
justify-content: flex-start;
gap: 4px;
height: 56px;
height: 67px;
z-index: 1100;
position: relative;
width: auto;
@@ -555,6 +555,47 @@ nav.navbar + #content-wrapper .page-content {
padding-top: 56px;
}
/* Horizontal nav mode: no sidebar, so remove sidebar margin from content */
nav.navbar + #content-wrapper {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
padding-left: 20px !important;
padding-right: 20px !important;
}
nav.navbar + #content-wrapper .page-content {
padding-left: 0 !important;
padding-right: 0 !important;
}
nav.navbar + #content-wrapper .page-content .row {
margin-left: 0 !important;
margin-right: 0 !important;
}
nav.navbar + #content-wrapper .page-content [class*="col-"] {
padding-left: 0 !important;
padding-right: 0 !important;
}
nav.navbar + #content-wrapper .page-content > .row > .col-sm-12 {
padding-left: 0 !important;
padding-right: 0 !important;
}
nav.navbar + #content-wrapper .page-header {
margin-left: 0 !important;
padding-left: 0 !important;
}
/* Page background follows the light/dark theme when horizontal nav is active */
nav.navbar + #content-wrapper,
nav.navbar + #content-wrapper .page-content {
background-color: var(--color-background) !important;
color: var(--color-text-primary) !important;
}
/* Right-side controls: notification bell + account menu */
.navbar-collapse .navbar-nav.navbar-right {
margin-left: auto;
@@ -580,9 +621,9 @@ nav.navbar + #content-wrapper .page-content {
z-index: 1100 !important;
background-color: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--color-border) !important;
height: 56px !important;
min-height: 56px !important;
max-height: 56px !important;
height: 67px !important;
min-height: 67px !important;
max-height: 67px !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
@@ -601,7 +642,7 @@ body:not(.ots-sidebar-collapsed) .ots-topbar-strip {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
height: 56px !important;
height: 67px !important;
padding: 0 16px !important;
margin: 0 !important;
gap: 12px !important;
@@ -3008,28 +3049,50 @@ legend {
.modal,
.modal-body,
.modal-footer {
background-color: transparent !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
color: var(--modal-body-text, var(--color-text-primary)) !important;
}
.modal-content {
background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
background-color: var(--modal-bg, var(--color-surface)) !important;
color: var(--modal-body-text, var(--color-text-primary)) !important;
border: 1px solid var(--modal-border, var(--color-border)) !important;
border-radius: var(--modal-radius, var(--ots-radius-lg)) !important;
box-shadow: var(--modal-shadow) !important;
overflow: hidden;
}
.modal-backdrop,
.modal-backdrop.show,
.modal-backdrop.in {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(4px) !important;
background-color: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.3)) !important;
backdrop-filter: blur(var(--modal-backdrop-blur, 4px)) !important;
-webkit-backdrop-filter: blur(var(--modal-backdrop-blur, 4px)) !important;
opacity: 1 !important;
}
.modal-header {
background-color: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--color-border) !important;
background-color: var(--modal-header-bg, var(--color-surface-elevated)) !important;
border-bottom: 1px solid var(--modal-header-border, var(--color-border)) !important;
padding: 16px 20px !important;
}
.modal-title,
.modal-header h4,
.modal-header h5 {
color: var(--modal-header-text, var(--color-text-primary)) !important;
font-weight: 600;
}
.modal-body {
background-color: var(--modal-body-bg, transparent) !important;
color: var(--modal-body-text, var(--color-text-primary)) !important;
padding: 20px !important;
}
.modal-footer {
background-color: var(--modal-footer-bg, transparent) !important;
border-top: 1px solid var(--modal-footer-border, var(--color-border)) !important;
padding: 12px 20px !important;
}
.dropdown-menu a,
@@ -3373,6 +3436,7 @@ a.text-muted:hover {
}
/* Light mode token overrides so layout backgrounds follow theme */
html.ots-light-mode,
body.ots-light-mode {
--ots-bg: var(--color-background);
--ots-surface: var(--color-surface);
@@ -3450,7 +3514,8 @@ hr {
background: var(--ots-bg);
}
#content-wrapper {
/* Only apply sidebar content gap when in sidebar mode (inside #page-wrapper) */
#page-wrapper #content-wrapper {
padding-left: var(--ots-sidebar-content-gap);
box-sizing: border-box;
}
@@ -3459,12 +3524,23 @@ hr {
padding-top: 24px;
}
/* Override Bootstrap col padding inside page-content */
.page-content > .row > .col-sm-12 {
/* Override Bootstrap col padding inside page-content (sidebar mode only) */
#page-wrapper .page-content > .row > .col-sm-12 {
padding-left: 16px;
padding-right: 16px;
}
/* Horizontal nav: ensure no extra left padding leaks through */
nav.navbar + #content-wrapper {
padding-left: 20px !important;
padding-right: 20px !important;
}
nav.navbar + #content-wrapper .page-content > .row > .col-sm-12 {
padding-left: 0 !important;
padding-right: 0 !important;
}
/* =============================================================================
NAVBAR / TOPBAR
============================================================================= */
@@ -3475,8 +3551,8 @@ hr {
border: none;
border-bottom: 1px solid var(--ots-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
height: 56px;
min-height: 56px;
height: 67px;
min-height: 67px;
padding: 0 16px;
display: flex;
align-items: center;
@@ -3649,30 +3725,44 @@ hr {
.widget,
.card,
.panel,
.modal-content {
.panel {
background: var(--color-surface) !important;
border: 1px solid var(--color-border) !important;
border-radius: var(--radius-md);
box-shadow: var(--shadow-base);
}
.modal-content {
background: var(--modal-bg, var(--color-surface)) !important;
border: 1px solid var(--modal-border, var(--color-border)) !important;
border-radius: var(--modal-radius, var(--ots-radius-lg)) !important;
box-shadow: var(--modal-shadow) !important;
}
.widget-title,
.panel-heading,
.card-header,
.modal-header {
.card-header {
background: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--color-border) !important;
color: var(--color-text-primary) !important;
}
.modal-header {
background: var(--modal-header-bg, var(--color-surface-elevated)) !important;
border-bottom: 1px solid var(--modal-header-border, var(--color-border)) !important;
color: var(--modal-header-text, var(--color-text-primary)) !important;
}
.widget-body,
.panel-body,
.card-body,
.modal-body {
.card-body {
color: var(--ots-text);
}
.modal-body {
color: var(--modal-body-text, var(--ots-text));
}
/* =============================================================================
BUTTONS
============================================================================= */
@@ -3913,30 +4003,121 @@ textarea:focus {
============================================================================= */
.modal-content {
border-radius: var(--radius-lg);
background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
border-radius: var(--modal-radius, var(--ots-radius-lg));
background-color: var(--modal-bg, var(--color-surface)) !important;
color: var(--modal-body-text, var(--color-text-primary)) !important;
border: 1px solid var(--modal-border, var(--color-border)) !important;
box-shadow: var(--modal-shadow) !important;
overflow: hidden;
}
.modal,
.modal-header,
.modal-body,
.modal-footer {
background-color: transparent !important;
color: var(--color-text-primary) !important;
color: var(--modal-body-text, var(--color-text-primary)) !important;
}
.modal-header {
background-color: var(--modal-header-bg, var(--color-surface-elevated)) !important;
border-bottom: 1px solid var(--modal-header-border, var(--ots-border)) !important;
padding: 16px 20px !important;
}
.modal-body {
background-color: var(--modal-body-bg, transparent) !important;
padding: 20px !important;
}
.modal-backdrop,
.modal-backdrop.show,
.modal-backdrop.in {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(4px) !important;
background-color: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.3)) !important;
backdrop-filter: blur(var(--modal-backdrop-blur, 4px)) !important;
-webkit-backdrop-filter: blur(var(--modal-backdrop-blur, 4px)) !important;
opacity: 1 !important;
}
.modal-footer {
border-top: 1px solid var(--ots-border);
background-color: var(--modal-footer-bg, transparent) !important;
border-top: 1px solid var(--modal-footer-border, var(--ots-border)) !important;
padding: 12px 20px !important;
}
/* Modal footer buttons — secondary */
.modal-footer .btn,
.modal-footer button {
border-radius: var(--ots-radius-sm) !important;
border: 1px solid var(--modal-border, var(--ots-border)) !important;
background: var(--modal-body-bg, var(--ots-surface-3)) !important;
color: var(--modal-body-text, var(--ots-text)) !important;
transition: background var(--ots-transition),
color var(--ots-transition),
border-color var(--ots-transition);
}
.modal-footer .btn:hover,
.modal-footer button:hover {
background: rgba(79, 140, 255, 0.08) !important;
border-color: var(--ots-primary) !important;
color: var(--ots-primary) !important;
}
/* Modal footer buttons — primary */
.modal-footer .btn-primary,
.modal-footer .btn.btn-primary {
background: var(--ots-primary) !important;
color: #0b1020 !important;
border-color: var(--ots-primary-2, var(--ots-primary)) !important;
font-weight: 600;
}
.modal-footer .btn-primary:hover {
background: var(--ots-primary-2, var(--color-primary-dark)) !important;
color: #ffffff !important;
}
/* Modal footer buttons — danger */
.modal-footer .btn-danger {
background: var(--ots-danger) !important;
color: #ffffff !important;
border-color: var(--ots-danger) !important;
}
/* Modal close button */
.modal-header .close,
.modal-header [data-dismiss="modal"] {
color: var(--modal-close-color, var(--ots-text-muted)) !important;
opacity: 1 !important;
text-shadow: none !important;
}
.modal-header .close:hover,
.modal-header [data-dismiss="modal"]:hover {
color: var(--modal-close-hover, var(--ots-text)) !important;
}
/* Modal form controls */
.modal-body .form-control,
.modal-body input[type="text"],
.modal-body input[type="number"],
.modal-body input[type="email"],
.modal-body input[type="password"],
.modal-body textarea,
.modal-body select {
background-color: var(--modal-input-bg, var(--ots-bg)) !important;
border: 1px solid var(--modal-input-border, var(--ots-border)) !important;
border-radius: var(--ots-radius-sm) !important;
color: var(--modal-input-text, var(--ots-text)) !important;
}
.modal-body .form-control:focus,
.modal-body input:focus,
.modal-body textarea:focus,
.modal-body select:focus {
border-color: var(--modal-input-focus-border, var(--ots-primary)) !important;
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(79, 140, 255, 0.2)) !important;
outline: none !important;
}
/* =============================================================================
@@ -4076,3 +4257,65 @@ textarea:focus {
text-indent: 0 !important;
list-style: none !important;
}
/* =============================================================================
HORIZONTAL NAV — FINAL OVERRIDE (must be last in file to win cascade)
Applied when the top navbar is active and there is no sidebar.
============================================================================= */
nav.navbar + #content-wrapper,
.navbar.navbar-expand-lg ~ #content-wrapper,
.navbar-default.navbar-expand-lg ~ #content-wrapper {
margin-left: 0 !important;
padding-left: 20px !important;
padding-right: 20px !important;
width: 100% !important;
max-width: 100% !important;
}
nav.navbar + #content-wrapper .page-content,
.navbar.navbar-expand-lg ~ #content-wrapper .page-content {
padding-left: 0 !important;
padding-right: 0 !important;
}
nav.navbar + #content-wrapper .page-content .row,
.navbar.navbar-expand-lg ~ #content-wrapper .page-content .row {
margin-left: 0 !important;
margin-right: 0 !important;
}
nav.navbar + #content-wrapper .page-content [class*="col-"],
nav.navbar + #content-wrapper .page-content > .row > .col-sm-12,
.navbar.navbar-expand-lg ~ #content-wrapper .page-content [class*="col-"],
.navbar.navbar-expand-lg ~ #content-wrapper .page-content > .row > .col-sm-12 {
padding-left: 0 !important;
padding-right: 0 !important;
}
/* =============================================================================
HORIZONTAL NAV — CLASS-BASED OVERRIDE (highest cascade priority)
.ots-horizontal-nav is added directly to #content-wrapper in authed.twig
============================================================================= */
#content-wrapper.ots-horizontal-nav {
margin-left: 0 !important;
padding-left: 30px !important;
padding-right: 30px !important;
width: 100% !important;
max-width: 100% !important;
}
#content-wrapper.ots-horizontal-nav .page-content {
padding-left: 0 !important;
padding-right: 0 !important;
}
#content-wrapper.ots-horizontal-nav .page-content .row {
margin-left: 0 !important;
margin-right: 0 !important;
}
#content-wrapper.ots-horizontal-nav .page-content [class*="col-"],
#content-wrapper.ots-horizontal-nav .page-content > .row > .col-sm-12 {
padding-left: 0 !important;
padding-right: 0 !important;
}

View File

@@ -379,53 +379,107 @@
// Only handle the user-menu dropdown.
// Let Bootstrap handle topbar nav dropdowns (Schedule, Design, etc.) natively
// so that links like Dayparting navigate normally.
const userDropdown = document.querySelector('#navbarUserMenu') && document.querySelector('#navbarUserMenu').closest('.dropdown');
var userToggle = document.querySelector('#navbarUserMenu');
if (!userToggle) return;
var userDropdown = userToggle.closest('.dropdown');
if (!userDropdown) return;
const userMenu = userDropdown.querySelector('.dropdown-menu');
var userMenu = userDropdown.querySelector('.dropdown-menu');
if (!userMenu) return;
userDropdown.addEventListener('click', function(e) {
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]') || e.target.closest('#navbarUserMenu')) {
e.preventDefault();
const nowActive = !userDropdown.classList.contains('active');
userDropdown.classList.toggle('active');
// ── Neutralize Bootstrap ──────────────────────────────────────────
// Remove data-toggle so Bootstrap's delegated handler never fires.
userToggle.removeAttribute('data-toggle');
try {
var jq = window.jQuery || window.$;
if (jq) {
jq(userToggle).off('.bs.dropdown');
jq(userDropdown).off('.bs.dropdown');
}
} catch (e) {}
// Float / unfloat the user menu
try {
if (nowActive) {
floatMenu(userMenu, userDropdown);
} else {
unfloatMenu(userMenu);
}
} catch (err) { /* ignore */ }
// ── Move menu to <body> ONCE and leave it there ───────────────────
// This escapes any overflow:hidden ancestors permanently.
// We toggle visibility via the .ots-user-menu-open class (no DOM moves).
document.body.appendChild(userMenu);
// Mark it so the MutationObserver in observeAndFloatMenus() skips it
userMenu.setAttribute('data-ots-floating', 'permanent');
userMenu.setAttribute('data-ots-floating-obs', '1');
// Start hidden
userMenu.classList.add('ots-user-menu-body');
userMenu.classList.remove('show', 'ots-floating-menu');
// Compute placement to avoid going off-screen
const trigger = userDropdown.querySelector('#navbarUserMenu');
if (trigger) {
userMenu.classList.remove('dropdown-menu-left', 'dropdown-menu-right');
const trigRect = trigger.getBoundingClientRect();
const menuWidth = userMenu.offsetWidth || 220;
const spaceRight = window.innerWidth - trigRect.right;
const spaceLeft = trigRect.left;
if (spaceRight < menuWidth && spaceLeft > menuWidth) {
userMenu.classList.add('dropdown-menu-left');
} else {
userMenu.classList.add('dropdown-menu-right');
}
}
function positionMenu() {
var rect = userToggle.getBoundingClientRect();
var menuWidth = userMenu.offsetWidth || 220;
var spaceRight = window.innerWidth - rect.right;
// Vertically: below the avatar
userMenu.style.top = Math.round(rect.bottom + 6) + 'px';
// Horizontally: align right edge to avatar right edge,
// but fall back to left-aligned if not enough space
if (spaceRight >= menuWidth) {
userMenu.style.left = Math.round(rect.left) + 'px';
userMenu.style.right = 'auto';
} else {
userMenu.style.left = 'auto';
userMenu.style.right = Math.round(window.innerWidth - rect.right) + 'px';
}
}
function openUserMenu() {
userDropdown.classList.remove('show');
userMenu.classList.remove('show');
positionMenu();
userMenu.classList.add('ots-user-menu-open');
userDropdown.classList.add('active');
}
function closeUserMenu() {
userMenu.classList.remove('ots-user-menu-open');
userDropdown.classList.remove('active', 'show');
userMenu.classList.remove('show');
}
// ── Click handler on the toggle element itself ─────────────────────
userToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (userMenu.classList.contains('ots-user-menu-open')) {
closeUserMenu();
} else {
openUserMenu();
}
});
// Close user menu when clicking outside
// ── Close when clicking outside ───────────────────────────────────
document.addEventListener('click', function(e) {
if (!userDropdown.contains(e.target) && !userMenu.contains(e.target)) {
const hadActive = userDropdown.classList.contains('active');
userDropdown.classList.remove('active');
if (hadActive) {
try { unfloatMenu(userMenu); } catch (err) {}
}
if (!userMenu.classList.contains('ots-user-menu-open')) return;
if (userToggle.contains(e.target)) return;
if (userMenu.contains(e.target)) return;
closeUserMenu();
});
// ── Close when a modal opens ─────────────────────────────────────
// Menu items like Preferences / Edit Profile trigger Bootstrap modals
// via .XiboFormButton — close the dropdown as soon as any modal shows.
document.addEventListener('show.bs.modal', function() { closeUserMenu(); }, true);
try {
var jq = window.jQuery || window.$;
if (jq) {
jq(document).on('show.bs.modal', function() { closeUserMenu(); });
}
} catch (e) {}
// ── Reposition on scroll/resize ───────────────────────────────────
window.addEventListener('scroll', function() {
if (userMenu.classList.contains('ots-user-menu-open')) positionMenu();
}, true);
window.addEventListener('resize', function() {
if (userMenu.classList.contains('ots-user-menu-open')) positionMenu();
});
}
@@ -434,7 +488,10 @@
* Adds `.ots-floating-menu` and positions absolutely based on the trigger rect.
*/
function floatMenu(menuEl, triggerEl) {
if (!menuEl || !triggerEl || menuEl.getAttribute('data-ots-floating') === '1') return;
if (!menuEl || !triggerEl) return;
// Skip if already floating or permanently managed by initDropdowns
var floatAttr = menuEl.getAttribute('data-ots-floating');
if (floatAttr === '1' || floatAttr === 'permanent') return;
try {
// Remember original parent and next sibling so we can restore later
menuEl._otsOriginalParent = menuEl.parentNode || null;
@@ -511,16 +568,25 @@
}
function unfloatMenu(menuEl) {
if (!menuEl || menuEl.getAttribute('data-ots-floating') !== '1') return;
if (!menuEl) return;
var floatAttr = menuEl.getAttribute('data-ots-floating');
// Skip permanently managed menus and menus that aren't floating
if (floatAttr === 'permanent' || floatAttr !== '1') return;
try {
menuEl.removeAttribute('data-ots-floating');
menuEl.classList.remove('ots-floating-menu');
// Clear ALL inline styles that floatMenu() may have set (including
// transform which was previously missed, causing stale styles).
menuEl.style.position = '';
menuEl.style.top = '';
menuEl.style.left = '';
menuEl.style.zIndex = '';
menuEl.style.minWidth = '';
menuEl.style.pointerEvents = '';
menuEl.style.transform = '';
menuEl.style.visibility = '';
menuEl.style.display = '';
menuEl.style.opacity = '';
// Remove reposition listeners
if (menuEl._otsReposition) {
window.removeEventListener('scroll', menuEl._otsReposition, true);