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