diff --git a/dejacode/settings.py b/dejacode/settings.py index fd0941fc..eb1040cf 100644 --- a/dejacode/settings.py +++ b/dejacode/settings.py @@ -355,6 +355,7 @@ def gettext_noop(s): SITE_TITLE = env.str("SITE_TITLE", default="DejaCode") HEADER_TEMPLATE = env.str("HEADER_TEMPLATE", default="includes/header.html") FOOTER_TEMPLATE = env.str("FOOTER_TEMPLATE", default="includes/footer.html") +SHOW_MENU_EXTERNAL_LINKS = env.bool("SHOW_MENU_EXTERNAL_LINKS", default=True) GRAPPELLI_INDEX_DASHBOARD = "dje.dashboard.DejaCodeDashboard" GRAPPELLI_CLEAN_INPUT_TYPES = False @@ -377,13 +378,6 @@ def gettext_noop(s): # An email address displayed in UI for users to reach the support team. DEJACODE_SUPPORT_EMAIL = env.str("DEJACODE_SUPPORT_EMAIL", default="") -# Enable this setting to display a "Tools" section in the navbar including -# links to the "Requests" and "Reporting" views. -SHOW_TOOLS_IN_NAV = env.bool("SHOW_TOOLS_IN_NAV", default=True) - -# Set False to hide the "Product Portfolio" section in the navbar. -SHOW_PP_IN_NAV = env.bool("SHOW_PP_IN_NAV", default=True) - # An integer specifying how many objects should be displayed per table whithin tabs. TAB_PAGINATE_BY = env.int("TAB_PAGINATE_BY", default=100) diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index f49cb19d..fe4f7f88 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -141,9 +141,6 @@ table.text-break thead { background-color: var(--bs-djc-blue-bg); height: 54px; } -.navbar .offcanvas { - background-color: var(--bs-djc-blue-bg) !important; -} .navbar-nav .active>.nav-link, .navbar-nav .show>.nav-link .navbar-nav .nav-link.active, @@ -153,11 +150,30 @@ table.text-break thead { text-underline-position: under; color: var(--bs-white); } -.navbar #search-input::placeholder { - color: var(--bs-gray-300); +.navbar .offcanvas { + background-color: var(--bs-djc-blue-bg) !important; } -.navbar #search-form { - width: 350px; +.offcanvas { + --bs-offcanvas-width: 280px; +} +.nav-chip { + transition: background-color 0.15s ease, border-color 0.15s ease; +} +.nav-chip:hover, +.nav-chip:focus-visible { + background-color: rgba(255, 255, 255, 0.18) !important; + border-color: rgba(255, 255, 255, 0.5) !important; +} + +/* -- Side menu -- */ +#side-menu .nav-pills .nav-link.active { + color: #fff !important; +} +#side-menu .nav-link { + text-decoration: none; +} +#side-menu .nav-pills .nav-link:not(.active):hover { + background-color: var(--bs-tertiary-bg); } /* -- Pagination -- */ diff --git a/dejacode/static/js/bootstrap_theme_toggler.js b/dejacode/static/js/bootstrap_theme_toggler.js index 0e020115..59372ae0 100644 --- a/dejacode/static/js/bootstrap_theme_toggler.js +++ b/dejacode/static/js/bootstrap_theme_toggler.js @@ -1,81 +1,80 @@ /*! -* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) -* Copyright 2011-2023 The Bootstrap Authors -* Licensed under the Creative Commons Attribution 3.0 Unported License. -* https://getbootstrap.com/docs/5.3/customize/color-modes/#javascript -*/ + * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under the Creative Commons Attribution 3.0 Unported License. + */ (() => { -'use strict' + 'use strict' const getStoredTheme = () => localStorage.getItem('theme') const setStoredTheme = theme => localStorage.setItem('theme', theme) -const getPreferredTheme = () => { - const storedTheme = getStoredTheme() - if (storedTheme) { - return storedTheme - } + const getPreferredTheme = () => { + const storedTheme = getStoredTheme() + if (storedTheme) { + return storedTheme + } - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' -} + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } -const setTheme = theme => { - if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { - document.documentElement.setAttribute('data-bs-theme', 'dark') - } else { - document.documentElement.setAttribute('data-bs-theme', theme) + const setTheme = theme => { + if (theme === 'auto') { + document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')) + } else { + document.documentElement.setAttribute('data-bs-theme', theme) + } } -} -setTheme(getPreferredTheme()) + setTheme(getPreferredTheme()) -const showActiveTheme = (theme, focus = false) => { - const themeSwitcher = document.querySelector('#bd-theme') + const showActiveTheme = (theme, focus = false) => { + const themeSwitcher = document.querySelector('#bd-theme') - if (!themeSwitcher) { - return - } + if (!themeSwitcher) { + return + } - const themeSwitcherText = document.querySelector('#bd-theme-text') - const activeThemeIcon = document.querySelector('.theme-icon-active use') - const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) - const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href') + const themeSwitcherText = document.querySelector('#bd-theme-text') + const activeThemeIcon = document.querySelector('.theme-icon-active use') + const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) + const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href') - document.querySelectorAll('[data-bs-theme-value]').forEach(element => { - element.classList.remove('active') - element.setAttribute('aria-pressed', 'false') - }) + document.querySelectorAll('[data-bs-theme-value]').forEach(element => { + element.classList.remove('active') + element.setAttribute('aria-pressed', 'false') + }) - btnToActive.classList.add('active') - btnToActive.setAttribute('aria-pressed', 'true') - activeThemeIcon.setAttribute('href', svgOfActiveBtn) - const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` - themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) + btnToActive.classList.add('active') + btnToActive.setAttribute('aria-pressed', 'true') + activeThemeIcon.setAttribute('href', svgOfActiveBtn) + const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` + themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) - if (focus) { - themeSwitcher.focus() + if (focus) { + themeSwitcher.focus() + } } -} -window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - const storedTheme = getStoredTheme() - if (storedTheme !== 'light' && storedTheme !== 'dark') { - setTheme(getPreferredTheme()) - } -}) + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + const storedTheme = getStoredTheme() + if (storedTheme !== 'light' && storedTheme !== 'dark') { + setTheme(getPreferredTheme()) + } + }) -window.addEventListener('DOMContentLoaded', () => { - showActiveTheme(getPreferredTheme()) + window.addEventListener('DOMContentLoaded', () => { + showActiveTheme(getPreferredTheme()) - document.querySelectorAll('[data-bs-theme-value]') - .forEach(toggle => { - toggle.addEventListener('click', () => { - const theme = toggle.getAttribute('data-bs-theme-value') - setStoredTheme(theme) - setTheme(theme) - showActiveTheme(theme, true) + document.querySelectorAll('[data-bs-theme-value]') + .forEach(toggle => { + toggle.addEventListener('click', () => { + const theme = toggle.getAttribute('data-bs-theme-value') + setStoredTheme(theme) + setTheme(theme) + showActiveTheme(theme, true) + }) }) - }) -}) + }) })() diff --git a/dejacode/static/js/dejacode_main.js b/dejacode/static/js/dejacode_main.js index b6df8b64..428dd899 100644 --- a/dejacode/static/js/dejacode_main.js +++ b/dejacode/static/js/dejacode_main.js @@ -128,6 +128,73 @@ function setupHTMX() { }); } +function setupSearchModal() { + const searchForm = document.getElementById('search-form'); + const searchInput = document.getElementById('search-input'); + const searchModal = document.getElementById('search-modal'); + + if (!searchModal) return; + + // Scope selector buttons + if (searchForm) { + document.querySelectorAll('.search-scope-btn').forEach(button => { + button.addEventListener('click', () => { + document.querySelectorAll('.search-scope-btn').forEach(b => b.classList.remove('active')); + button.classList.add('active'); + searchForm.setAttribute('action', button.dataset.scopeAction); + searchInput.focus(); + }); + }); + } + + // Autofocus input when modal opens + searchModal.addEventListener('shown.bs.modal', () => { + searchInput.focus(); + searchInput.select(); + }); + + // Move focus out before aria-hidden is restored to avoid accessibility warning + searchModal.addEventListener('hide.bs.modal', () => { + if (document.activeElement) document.activeElement.blur(); + }); + + // Keyboard shortcuts: Ctrl/Cmd+K and / to open the modal + document.addEventListener('keydown', (event) => { + const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable; + const modalInstance = bootstrap.Modal.getOrCreateInstance(searchModal); + + if ((event.ctrlKey || event.metaKey) && event.key === 'k') { + event.preventDefault(); + modalInstance.show(); + } else if (event.key === '/' && !isTyping) { + event.preventDefault(); + modalInstance.show(); + } + }); +} + +function setupThemeSwitcher() { + // Reflects the active theme on the dropdown buttons since Bootstrap's own + // script targets its docs-specific markup and skips ours. + const getActiveTheme = () => localStorage.getItem('theme') || 'auto'; + + const markActiveTheme = theme => { + document.querySelectorAll('[data-bs-theme-value]').forEach(btn => { + const isActive = btn.getAttribute('data-bs-theme-value') === theme; + btn.classList.toggle('active', isActive); + btn.setAttribute('aria-pressed', isActive); + }); + }; + + markActiveTheme(getActiveTheme()); + + document.querySelectorAll('[data-bs-theme-value]').forEach(btn => { + btn.addEventListener('click', () => { + markActiveTheme(btn.getAttribute('data-bs-theme-value')); + }); + }); +} + document.addEventListener('DOMContentLoaded', () => { NEXB = {}; NEXB.client_data = JSON.parse(document.getElementById("client_data").textContent); @@ -157,17 +224,11 @@ document.addEventListener('DOMContentLoaded', () => { document.body.appendChild(overlay); } - // Search selection in the header - $('#search-selector-list a').click(function(event) { - event.preventDefault(); - $('#search-form').attr('action', $(this).attr('href')); - $('#search-selector-content').html($(this).html()); - $('#search-input').focus(); - }); - setupTooltips(); setupPopovers(); setupSelectionCheckboxes(); setupBackToTop(); setupHTMX(); + setupSearchModal(); + setupThemeSwitcher(); }); diff --git a/dje/context_processors.py b/dje/context_processors.py index 3ee98911..01723226 100644 --- a/dje/context_processors.py +++ b/dje/context_processors.py @@ -20,8 +20,7 @@ def dejacode_context(request): "SITE_TITLE": settings.SITE_TITLE, "HEADER_TEMPLATE": settings.HEADER_TEMPLATE, "FOOTER_TEMPLATE": settings.FOOTER_TEMPLATE, - "SHOW_PP_IN_NAV": settings.SHOW_PP_IN_NAV, - "SHOW_TOOLS_IN_NAV": settings.SHOW_TOOLS_IN_NAV, + "SHOW_MENU_EXTERNAL_LINKS": settings.SHOW_MENU_EXTERNAL_LINKS, "AXES_ENABLED": settings.AXES_ENABLED, "LOGIN_FORM_ALERT": settings.LOGIN_FORM_ALERT, } diff --git a/dje/templates/bootstrap_base.html b/dje/templates/bootstrap_base.html index c8e22d99..ce0a3ad3 100644 --- a/dje/templates/bootstrap_base.html +++ b/dje/templates/bootstrap_base.html @@ -15,6 +15,9 @@ {% if FAVICON_HREF %}{% endif %} + {% block side_menu %} + {% include 'navbar/side_menu.html' %} + {% endblock %}
{% block header %} {% include HEADER_TEMPLATE %} diff --git a/dje/templates/includes/footer.html b/dje/templates/includes/footer.html index 1df5875d..82ce82ee 100644 --- a/dje/templates/includes/footer.html +++ b/dje/templates/includes/footer.html @@ -1,11 +1,11 @@ {% load i18n %} -