Skip to content

Commit c56d8fb

Browse files
authored
feat: [five-c] User experience (UX) enhancements (#524)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 4938cd4 commit c56d8fb

25 files changed

Lines changed: 648 additions & 421 deletions

dejacode/settings.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ def gettext_noop(s):
355355
SITE_TITLE = env.str("SITE_TITLE", default="DejaCode")
356356
HEADER_TEMPLATE = env.str("HEADER_TEMPLATE", default="includes/header.html")
357357
FOOTER_TEMPLATE = env.str("FOOTER_TEMPLATE", default="includes/footer.html")
358+
SHOW_MENU_EXTERNAL_LINKS = env.bool("SHOW_MENU_EXTERNAL_LINKS", default=True)
358359

359360
GRAPPELLI_INDEX_DASHBOARD = "dje.dashboard.DejaCodeDashboard"
360361
GRAPPELLI_CLEAN_INPUT_TYPES = False
@@ -377,13 +378,6 @@ def gettext_noop(s):
377378
# An email address displayed in UI for users to reach the support team.
378379
DEJACODE_SUPPORT_EMAIL = env.str("DEJACODE_SUPPORT_EMAIL", default="")
379380

380-
# Enable this setting to display a "Tools" section in the navbar including
381-
# links to the "Requests" and "Reporting" views.
382-
SHOW_TOOLS_IN_NAV = env.bool("SHOW_TOOLS_IN_NAV", default=True)
383-
384-
# Set False to hide the "Product Portfolio" section in the navbar.
385-
SHOW_PP_IN_NAV = env.bool("SHOW_PP_IN_NAV", default=True)
386-
387381
# An integer specifying how many objects should be displayed per table whithin tabs.
388382
TAB_PAGINATE_BY = env.int("TAB_PAGINATE_BY", default=100)
389383

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ table.text-break thead {
141141
background-color: var(--bs-djc-blue-bg);
142142
height: 54px;
143143
}
144-
.navbar .offcanvas {
145-
background-color: var(--bs-djc-blue-bg) !important;
146-
}
147144
.navbar-nav .active>.nav-link,
148145
.navbar-nav .show>.nav-link
149146
.navbar-nav .nav-link.active,
@@ -153,11 +150,30 @@ table.text-break thead {
153150
text-underline-position: under;
154151
color: var(--bs-white);
155152
}
156-
.navbar #search-input::placeholder {
157-
color: var(--bs-gray-300);
153+
.navbar .offcanvas {
154+
background-color: var(--bs-djc-blue-bg) !important;
158155
}
159-
.navbar #search-form {
160-
width: 350px;
156+
.offcanvas {
157+
--bs-offcanvas-width: 280px;
158+
}
159+
.nav-chip {
160+
transition: background-color 0.15s ease, border-color 0.15s ease;
161+
}
162+
.nav-chip:hover,
163+
.nav-chip:focus-visible {
164+
background-color: rgba(255, 255, 255, 0.18) !important;
165+
border-color: rgba(255, 255, 255, 0.5) !important;
166+
}
167+
168+
/* -- Side menu -- */
169+
#side-menu .nav-pills .nav-link.active {
170+
color: #fff !important;
171+
}
172+
#side-menu .nav-link {
173+
text-decoration: none;
174+
}
175+
#side-menu .nav-pills .nav-link:not(.active):hover {
176+
background-color: var(--bs-tertiary-bg);
161177
}
162178

163179
/* -- Pagination -- */
Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,80 @@
11
/*!
2-
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
3-
* Copyright 2011-2023 The Bootstrap Authors
4-
* Licensed under the Creative Commons Attribution 3.0 Unported License.
5-
* https://getbootstrap.com/docs/5.3/customize/color-modes/#javascript
6-
*/
2+
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
3+
* Copyright 2011-2025 The Bootstrap Authors
4+
* Licensed under the Creative Commons Attribution 3.0 Unported License.
5+
*/
76

87
(() => {
9-
'use strict'
8+
'use strict'
109

1110
const getStoredTheme = () => localStorage.getItem('theme')
1211
const setStoredTheme = theme => localStorage.setItem('theme', theme)
1312

14-
const getPreferredTheme = () => {
15-
const storedTheme = getStoredTheme()
16-
if (storedTheme) {
17-
return storedTheme
18-
}
13+
const getPreferredTheme = () => {
14+
const storedTheme = getStoredTheme()
15+
if (storedTheme) {
16+
return storedTheme
17+
}
1918

20-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
21-
}
19+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
20+
}
2221

23-
const setTheme = theme => {
24-
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
25-
document.documentElement.setAttribute('data-bs-theme', 'dark')
26-
} else {
27-
document.documentElement.setAttribute('data-bs-theme', theme)
22+
const setTheme = theme => {
23+
if (theme === 'auto') {
24+
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
25+
} else {
26+
document.documentElement.setAttribute('data-bs-theme', theme)
27+
}
2828
}
29-
}
3029

31-
setTheme(getPreferredTheme())
30+
setTheme(getPreferredTheme())
3231

33-
const showActiveTheme = (theme, focus = false) => {
34-
const themeSwitcher = document.querySelector('#bd-theme')
32+
const showActiveTheme = (theme, focus = false) => {
33+
const themeSwitcher = document.querySelector('#bd-theme')
3534

36-
if (!themeSwitcher) {
37-
return
38-
}
35+
if (!themeSwitcher) {
36+
return
37+
}
3938

40-
const themeSwitcherText = document.querySelector('#bd-theme-text')
41-
const activeThemeIcon = document.querySelector('.theme-icon-active use')
42-
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
43-
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
39+
const themeSwitcherText = document.querySelector('#bd-theme-text')
40+
const activeThemeIcon = document.querySelector('.theme-icon-active use')
41+
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
42+
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
4443

45-
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
46-
element.classList.remove('active')
47-
element.setAttribute('aria-pressed', 'false')
48-
})
44+
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
45+
element.classList.remove('active')
46+
element.setAttribute('aria-pressed', 'false')
47+
})
4948

50-
btnToActive.classList.add('active')
51-
btnToActive.setAttribute('aria-pressed', 'true')
52-
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
53-
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
54-
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
49+
btnToActive.classList.add('active')
50+
btnToActive.setAttribute('aria-pressed', 'true')
51+
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
52+
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
53+
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
5554

56-
if (focus) {
57-
themeSwitcher.focus()
55+
if (focus) {
56+
themeSwitcher.focus()
57+
}
5858
}
59-
}
6059

61-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
62-
const storedTheme = getStoredTheme()
63-
if (storedTheme !== 'light' && storedTheme !== 'dark') {
64-
setTheme(getPreferredTheme())
65-
}
66-
})
60+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
61+
const storedTheme = getStoredTheme()
62+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
63+
setTheme(getPreferredTheme())
64+
}
65+
})
6766

68-
window.addEventListener('DOMContentLoaded', () => {
69-
showActiveTheme(getPreferredTheme())
67+
window.addEventListener('DOMContentLoaded', () => {
68+
showActiveTheme(getPreferredTheme())
7069

71-
document.querySelectorAll('[data-bs-theme-value]')
72-
.forEach(toggle => {
73-
toggle.addEventListener('click', () => {
74-
const theme = toggle.getAttribute('data-bs-theme-value')
75-
setStoredTheme(theme)
76-
setTheme(theme)
77-
showActiveTheme(theme, true)
70+
document.querySelectorAll('[data-bs-theme-value]')
71+
.forEach(toggle => {
72+
toggle.addEventListener('click', () => {
73+
const theme = toggle.getAttribute('data-bs-theme-value')
74+
setStoredTheme(theme)
75+
setTheme(theme)
76+
showActiveTheme(theme, true)
77+
})
7878
})
79-
})
80-
})
79+
})
8180
})()

dejacode/static/js/dejacode_main.js

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,73 @@ function setupHTMX() {
128128
});
129129
}
130130

131+
function setupSearchModal() {
132+
const searchForm = document.getElementById('search-form');
133+
const searchInput = document.getElementById('search-input');
134+
const searchModal = document.getElementById('search-modal');
135+
136+
if (!searchModal) return;
137+
138+
// Scope selector buttons
139+
if (searchForm) {
140+
document.querySelectorAll('.search-scope-btn').forEach(button => {
141+
button.addEventListener('click', () => {
142+
document.querySelectorAll('.search-scope-btn').forEach(b => b.classList.remove('active'));
143+
button.classList.add('active');
144+
searchForm.setAttribute('action', button.dataset.scopeAction);
145+
searchInput.focus();
146+
});
147+
});
148+
}
149+
150+
// Autofocus input when modal opens
151+
searchModal.addEventListener('shown.bs.modal', () => {
152+
searchInput.focus();
153+
searchInput.select();
154+
});
155+
156+
// Move focus out before aria-hidden is restored to avoid accessibility warning
157+
searchModal.addEventListener('hide.bs.modal', () => {
158+
if (document.activeElement) document.activeElement.blur();
159+
});
160+
161+
// Keyboard shortcuts: Ctrl/Cmd+K and / to open the modal
162+
document.addEventListener('keydown', (event) => {
163+
const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable;
164+
const modalInstance = bootstrap.Modal.getOrCreateInstance(searchModal);
165+
166+
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
167+
event.preventDefault();
168+
modalInstance.show();
169+
} else if (event.key === '/' && !isTyping) {
170+
event.preventDefault();
171+
modalInstance.show();
172+
}
173+
});
174+
}
175+
176+
function setupThemeSwitcher() {
177+
// Reflects the active theme on the dropdown buttons since Bootstrap's own
178+
// script targets its docs-specific markup and skips ours.
179+
const getActiveTheme = () => localStorage.getItem('theme') || 'auto';
180+
181+
const markActiveTheme = theme => {
182+
document.querySelectorAll('[data-bs-theme-value]').forEach(btn => {
183+
const isActive = btn.getAttribute('data-bs-theme-value') === theme;
184+
btn.classList.toggle('active', isActive);
185+
btn.setAttribute('aria-pressed', isActive);
186+
});
187+
};
188+
189+
markActiveTheme(getActiveTheme());
190+
191+
document.querySelectorAll('[data-bs-theme-value]').forEach(btn => {
192+
btn.addEventListener('click', () => {
193+
markActiveTheme(btn.getAttribute('data-bs-theme-value'));
194+
});
195+
});
196+
}
197+
131198
document.addEventListener('DOMContentLoaded', () => {
132199
NEXB = {};
133200
NEXB.client_data = JSON.parse(document.getElementById("client_data").textContent);
@@ -157,17 +224,11 @@ document.addEventListener('DOMContentLoaded', () => {
157224
document.body.appendChild(overlay);
158225
}
159226

160-
// Search selection in the header
161-
$('#search-selector-list a').click(function(event) {
162-
event.preventDefault();
163-
$('#search-form').attr('action', $(this).attr('href'));
164-
$('#search-selector-content').html($(this).html());
165-
$('#search-input').focus();
166-
});
167-
168227
setupTooltips();
169228
setupPopovers();
170229
setupSelectionCheckboxes();
171230
setupBackToTop();
172231
setupHTMX();
232+
setupSearchModal();
233+
setupThemeSwitcher();
173234
});

dje/context_processors.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ def dejacode_context(request):
2020
"SITE_TITLE": settings.SITE_TITLE,
2121
"HEADER_TEMPLATE": settings.HEADER_TEMPLATE,
2222
"FOOTER_TEMPLATE": settings.FOOTER_TEMPLATE,
23-
"SHOW_PP_IN_NAV": settings.SHOW_PP_IN_NAV,
24-
"SHOW_TOOLS_IN_NAV": settings.SHOW_TOOLS_IN_NAV,
23+
"SHOW_MENU_EXTERNAL_LINKS": settings.SHOW_MENU_EXTERNAL_LINKS,
2524
"AXES_ENABLED": settings.AXES_ENABLED,
2625
"LOGIN_FORM_ALERT": settings.LOGIN_FORM_ALERT,
2726
}

dje/templates/bootstrap_base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
{% if FAVICON_HREF %}<link rel="icon" href="{{ FAVICON_HREF }}">{% endif %}
1616
</head>
1717
<body class="d-flex flex-column h-100 {% block bodyclass %}{% endblock %}">
18+
{% block side_menu %}
19+
{% include 'navbar/side_menu.html' %}
20+
{% endblock %}
1821
<header>
1922
{% block header %}
2023
{% include HEADER_TEMPLATE %}

dje/templates/includes/footer.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{% load i18n %}
2-
<footer class="footer mt-auto bg-body-tertiary text-body-secondary">
2+
<footer class="footer mt-auto bg-body-tertiary">
33
<div class="container-fluid p-3 p-md-5">
44
<ul class="list-unstyled">
55
<li><a href="/">Home</a></li>{# No trans for Home on purpose #}
66
<li><a href="https://dejacode.readthedocs.io/en/latest/" target="_blank" rel="noreferrer">{% trans 'Documentation' %}</a></li>
77
<li><a href="https://www.nexb.com/resources/#support" target="_blank" rel="noreferrer">{% trans 'Support' %}</a></li>
8-
<li><a href="https://www.nexb.com/dejacode/" target="_blank" rel="noreferrer">{% trans 'About' %}</a></li>
8+
<li><a href="https://aboutcode.org/dejacode/" target="_blank" rel="noreferrer">{% trans 'About' %}</a></li>
99
<li><a href="https://github.com/aboutcode-org/dejacode" target="_blank" rel="noreferrer">{% trans 'Source Code' %}</a></li>
1010
<li><a href="https://github.com/aboutcode-org/dejacode/releases" target="_blank" rel="noreferrer">{% trans 'Releases' %}</a></li>
1111
<li><a href="https://scancode-licensedb.aboutcode.org/agpl-3.0.html" target="_blank" rel="noreferrer">{% trans 'License' %}</a></li>

dje/templates/includes/header.html

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,2 @@
1-
{% extends 'includes/navbar_header.html' %}
2-
{% load i18n %}
3-
4-
{% block nav-brand %}
5-
<div class="navbar-nav navbar-brand">
6-
<div class="nav-item dropdown">
7-
<a class="nav-link dropdown-toggle d-inline" href="#" data-bs-toggle="dropdown" aria-expanded="false">
8-
{{ SITE_TITLE }}
9-
</a>
10-
<div class="dropdown-menu position-absolute">
11-
<a class="dropdown-item" href="/">Home</a>{# No trans for Home on purpose #}
12-
<div class="dropdown-divider"></div>
13-
<a class="dropdown-item" href="https://dejacode.readthedocs.io/en/latest/" target="_blank" rel="noreferrer">{% trans 'Documentation' %}</a>
14-
<a class="dropdown-item" href="https://www.nexb.com/resources/#support" target="_blank" rel="noreferrer">{% trans 'Support' %}</a>
15-
<a class="dropdown-item" href="https://www.nexb.com/dejacode/" target="_blank" rel="noreferrer">{% trans 'About' %}</a>
16-
<div class="dropdown-divider"></div>
17-
<a class="dropdown-item" href="https://github.com/aboutcode-org/dejacode" target="_blank" rel="noreferrer">{% trans 'Source Code' %}</a>
18-
<a class="dropdown-item" href="https://github.com/aboutcode-org/dejacode/releases" target="_blank" rel="noreferrer">{% trans 'Releases' %}</a>
19-
<a class="dropdown-item" href="https://scancode-licensedb.aboutcode.org/agpl-3.0.html" target="_blank" rel="noreferrer">{% trans 'License' %}</a>
20-
<div class="dropdown-divider"></div>
21-
<h6 class="dropdown-header">v{{ DEJACODE_VERSION }}</h6>
22-
</div>
23-
</div>
24-
</div>
25-
{% endblock %}
1+
{% extends 'navbar/navbar_header.html' %}
2+
{% load i18n %}

0 commit comments

Comments
 (0)