- Created static/css/components/sidebar.css (200 lines) * Navigation layout, active states, badges * Theme switcher button - Created static/css/components/navbar.css (30 lines) * Menu toggle button * Dropdown avatar customization - Removed ~95 lines of inline styles from sidebar.html - Removed ~23 lines of inline styles from navbar.html - All tests passing (17 OK)
165 lines
6.9 KiB
HTML
165 lines
6.9 KiB
HTML
{% load static req_tags %}
|
||
|
||
<link rel="stylesheet" href="{% static 'css/components/sidebar.css' %}">
|
||
|
||
<div id="sidebar"
|
||
class="offcanvas offcanvas-start offcanvas-lg"
|
||
tabindex="-1" aria-labelledby="sidebarLabel"
|
||
data-bs-backdrop="false" data-bs-scroll="true">
|
||
|
||
<div class="offcanvas-header border-bottom d-lg-none">
|
||
<h5 class="offcanvas-title" id="sidebarLabel">Меню</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||
</div>
|
||
|
||
<div class="offcanvas-body d-flex flex-column h-100 px-0">
|
||
<nav class="side-nav">
|
||
<a href="{% url 'feed:feed_list' %}"
|
||
class="nav-link {% if request.resolver_match.url_name == 'feed_list' %}active{% endif %}"
|
||
{% if request.resolver_match.url_name == 'feed_list' %}aria-current="page"{% endif %}>
|
||
<span class="side-icon"><i class="bi-megaphone"></i></span>
|
||
<span>Новости</span>
|
||
</a>
|
||
<a href="{% url 'employees:employees_list' %}"
|
||
class="nav-link {% if request.resolver_match.url_name == 'employees_list' %}active{% endif %}"
|
||
{% if request.resolver_match.url_name == 'employees_list' %}aria-current="page"{% endif %}>
|
||
<span class="side-icon"><i class="bi-people"></i></span>
|
||
<span>Сотрудники</span>
|
||
</a>
|
||
<a href="{% url 'employees:department_list' %}"
|
||
class="nav-link {% if request.resolver_match.url_name == 'department_list' %}active{% endif %}"
|
||
{% if request.resolver_match.url_name == 'department_list' %}aria-current="page"{% endif %}>
|
||
<span class="side-icon"><i class="bi-diagram-3"></i></span>
|
||
<span>Отделы</span>
|
||
</a>
|
||
<a href="{% url 'requests:request_list' %}"
|
||
class="nav-link {% if request.resolver_match.url_name == 'request_list' %}active{% endif %}"
|
||
{% if request.resolver_match.url_name == 'request_list' %}aria-current="page"{% endif %}>
|
||
<span class="side-icon"><i class="bi-clipboard-check"></i></span>
|
||
<span>Заявления</span>
|
||
</a>
|
||
|
||
{% with total=chat_unread_total|default:0 %}
|
||
<a href="{% url 'communications:chat_list' %}"
|
||
class="nav-link {% if request.resolver_match.app_name == 'communications' %}active{% endif %}"
|
||
{% if request.resolver_match.app_name == 'communications' %}aria-current="page"{% endif %}
|
||
aria-label="Чаты{% if total|add:0 > 0 %}, непрочитанных {{ total }}{% endif %}">
|
||
<span class="side-icon icon-wrap">
|
||
<i class="bi-chat-dots"></i>
|
||
{% if total|add:0 > 0 %}
|
||
<span class="nav-unread" id="sidebarChatBadge" data-count="{{ total }}">{{ total }}</span>
|
||
{% else %}
|
||
<span class="nav-unread d-none" id="sidebarChatBadge" data-count="0">0</span>
|
||
{% endif %}
|
||
</span>
|
||
<span>Чаты</span>
|
||
</a>
|
||
{% endwith %}
|
||
<a href="{% url 'documents:document_list' %}"
|
||
class="nav-link {% if request.resolver_match.url_name == 'document_list' %}active{% endif %}"
|
||
{% if request.resolver_match.url_name == 'document_list' %}aria-current="page"{% endif %}>
|
||
<span class="side-icon"><i class="bi-file-earmark-text"></i></span>
|
||
<span>Документы</span>
|
||
</a>
|
||
|
||
</nav>
|
||
|
||
<div class="mt-auto pt-3 small">
|
||
<button type="button" class="btn-theme js-theme-btn" id="themeBtnSidebar"
|
||
aria-label="Сменить тему" title="Сменить тему">
|
||
<i class="bi bi-circle-half" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// компактный апдейтер бейджа "Чаты" (форматирует 99+)
|
||
(function(){
|
||
const badge = document.getElementById('sidebarChatBadge');
|
||
if (!badge) return;
|
||
function render(n){
|
||
const v = Number(n)||0;
|
||
badge.dataset.count = String(v);
|
||
badge.textContent = v > 99 ? '99+' : String(v);
|
||
badge.classList.toggle('d-none', v <= 0);
|
||
}
|
||
render(badge.dataset.count);
|
||
|
||
// глобальное событие для real-time апдейта
|
||
window.addEventListener('chats:unread-total', (e)=> render(e?.detail?.total));
|
||
|
||
// fallback: суммируем [data-unread-count] на страницах списка чатов
|
||
document.addEventListener('DOMContentLoaded', ()=>{
|
||
const rows = document.querySelectorAll('[data-unread-count]');
|
||
if (!rows.length) return;
|
||
const total = Array.from(rows).reduce((acc, el)=> acc + (Number(el.textContent)||0), 0);
|
||
render(total);
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
(function(){
|
||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||
const root = document.documentElement;
|
||
const MODES = ['light','auto','dark'];
|
||
const ICONS = { light:'bi-sun-fill', auto:'bi-circle-half', dark:'bi-moon-stars' };
|
||
const LABEL = { light:'Светлая', auto:'Авто', dark:'Темная' };
|
||
|
||
// какие-то проекты любят хранить 'theme' в LS — используем тот же ключ
|
||
function getSavedMode(){ return localStorage.getItem('theme') || 'auto'; }
|
||
function setSavedMode(m){ try{ localStorage.setItem('theme', m); }catch(_){} }
|
||
|
||
function effective(mode){ return mode === 'auto' ? (media.matches ? 'dark' : 'light') : mode; }
|
||
|
||
function applyMode(mode, persist=true){
|
||
const eff = effective(mode);
|
||
root.setAttribute('data-bs-theme', eff);
|
||
if (persist) setSavedMode(mode);
|
||
updateButtons(mode);
|
||
}
|
||
|
||
function updateButtons(mode){
|
||
document.querySelectorAll('.js-theme-btn').forEach(btn=>{
|
||
const icon = btn.querySelector('.bi');
|
||
const lab = btn.querySelector('.mode-label');
|
||
// сброс иконок
|
||
if (icon){
|
||
icon.className = 'bi ' + (ICONS[mode] || ICONS.auto);
|
||
}
|
||
if (lab){ lab.textContent = LABEL[mode] || LABEL.auto; }
|
||
btn.dataset.mode = mode;
|
||
btn.title = `Тема: ${LABEL[mode]}. Нажмите, чтобы переключить`;
|
||
btn.setAttribute('aria-label', `Тема: ${LABEL[mode]}. Нажмите, чтобы переключить`);
|
||
});
|
||
}
|
||
|
||
function nextMode(cur){
|
||
const i = MODES.indexOf(cur); return MODES[(i >= 0 ? i : 0) + 1 === MODES.length ? 0 : i + 1];
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', ()=>{
|
||
const start = getSavedMode();
|
||
applyMode(start, false);
|
||
|
||
document.body.addEventListener('click', (e)=>{
|
||
const btn = e.target.closest('.js-theme-btn'); if (!btn) return;
|
||
e.preventDefault();
|
||
const cur = btn.dataset.mode || getSavedMode() || 'auto';
|
||
const nxt = nextMode(cur);
|
||
applyMode(nxt, true);
|
||
});
|
||
|
||
// если системная тема поменялась и у нас «Авто», нужно обновить
|
||
media.addEventListener('change', ()=>{ if ((localStorage.getItem('theme') || 'auto') === 'auto'){ applyMode('auto', false); } });
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
|
||
|