Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
@import './styles/global.css';
@import './styles/reset.css';
@import './styles/typography.css';
@import './styles/tableCommons.css'
@import './styles/tableCommons.css';
@import './styles/dark-mode.css';
1 change: 1 addition & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
import 'bootstrap-icons/font/bootstrap-icons.css';
import App from './App.jsx'
import './scripts/darkModeToggle.js'

createRoot(document.getElementById('root')).render(
<StrictMode>
Expand Down
146 changes: 146 additions & 0 deletions src/scripts/darkModeToggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Dark Mode Toggle - Sistema Independente
*
* Script vanilla JavaScript que:
* - Cria um botão no navbar com ícone Bootstrap Icons
* - Gerencia o estado do tema (light/dark)
* - Persiste a escolha no localStorage
* - Detecta preferência do sistema
* - Não interfere com código React existente
*/

(function() {
'use strict';

// Chave para localStorage
const STORAGE_KEY = 'theme-preference';

// Função para obter o tema inicial
function getInitialTheme() {
// Primeiro, verifica localStorage
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return saved;
}

// Depois, verifica preferência do sistema
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}

// Padrão: light
return 'light';
}

// Função para aplicar o tema
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(STORAGE_KEY, theme);
updateToggleButton(theme);
}

// Função para alternar tema
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'light';
const next = current === 'light' ? 'dark' : 'light';
applyTheme(next);
}

// Função para criar o botão de toggle
function createToggleButton() {
// Verifica se já existe
if (document.getElementById('dark-mode-toggle')) {
return;
}

// Encontra o navbar
const navbar = document.querySelector('.navbar-nav:last-child');
if (!navbar) {
console.warn('Navbar não encontrado, botão não foi criado');
return;
}

// Cria o item de lista
const li = document.createElement('li');
li.className = 'nav-item';

// Cria o botão
const button = document.createElement('button');
button.id = 'dark-mode-toggle';
button.setAttribute('aria-label', 'Alternar modo escuro');
button.innerHTML = '<i class="bi bi-moon-stars-fill"></i>';
Comment on lines +68 to +71
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button has an aria-label attribute but it's static ("Alternar modo escuro"). Consider using aria-pressed attribute to indicate the current state of the toggle button for better accessibility. The button should have aria-pressed="false" in light mode and aria-pressed="true" in dark mode to communicate the toggle state to screen readers.

Copilot uses AI. Check for mistakes.

// Event listener
button.addEventListener('click', toggleTheme);

// Adiciona ao navbar
li.appendChild(button);
navbar.appendChild(li);
}

// Função para atualizar o ícone do botão
function updateToggleButton(theme) {
const button = document.getElementById('dark-mode-toggle');
if (button) {
const icon = theme === 'light'
? '<i class="bi bi-moon-stars-fill"></i>'
: '<i class="bi bi-sun-fill"></i>';
const title = theme === 'light' ? 'Ativar modo escuro' : 'Ativar modo claro';
button.innerHTML = icon;
button.setAttribute('title', title);
Comment on lines +88 to +90
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateToggleButton function updates the title attribute but should also update the aria-label to match the current state. Additionally, consider adding aria-pressed attribute here to indicate whether dark mode is currently active (true) or inactive (false) for better accessibility support.

Suggested change
const title = theme === 'light' ? 'Ativar modo escuro' : 'Ativar modo claro';
button.innerHTML = icon;
button.setAttribute('title', title);
const title = theme === 'light' ? 'Ativar modo escuro' : 'Ativar modo claro';
const ariaLabel = title; // Use the same string for aria-label
const ariaPressed = theme === 'dark' ? 'true' : 'false';
button.innerHTML = icon;
button.setAttribute('title', title);
button.setAttribute('aria-label', ariaLabel);
button.setAttribute('aria-pressed', ariaPressed);

Copilot uses AI. Check for mistakes.
}
}

// Listener para mudanças na preferência do sistema
function watchSystemTheme() {
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
// Só aplica se não houver preferência salva
if (!localStorage.getItem(STORAGE_KEY)) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
}
}

// Inicialização
function init() {
// Aplica tema inicial imediatamente (antes do DOM carregar)
const initialTheme = getInitialTheme();
document.documentElement.setAttribute('data-theme', initialTheme);

// Função para tentar criar o botão (com retry)
function tryCreateButton(attempts = 0) {
const navbar = document.querySelector('.navbar-nav:last-child');
if (navbar) {
createToggleButton();
updateToggleButton(initialTheme);
} else if (attempts < 20) {
// Tenta novamente após 100ms
setTimeout(() => tryCreateButton(attempts + 1), 100);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tryCreateButton function will attempt to find the navbar up to 20 times with 100ms intervals (total 2 seconds). However, if the navbar structure changes or the selector .navbar-nav:last-child doesn't match the actual DOM structure, this will silently fail after all attempts. Consider adding a console warning or error after all attempts are exhausted to help with debugging, or verify that this selector accurately matches your navbar structure.

Suggested change
setTimeout(() => tryCreateButton(attempts + 1), 100);
setTimeout(() => tryCreateButton(attempts + 1), 100);
} else {
console.warn(
"[DarkModeToggle] Failed to find navbar with selector '.navbar-nav:last-child' after 20 attempts. " +
"Button not created. Please check if the selector matches your DOM structure."
);

Copilot uses AI. Check for mistakes.
}
}

// Quando o DOM estiver pronto, tenta criar o botão
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
tryCreateButton();
watchSystemTheme();
});
} else {
tryCreateButton();
watchSystemTheme();
}
}

// Executa inicialização
init();

// Expõe funções globalmente (opcional, para debug)
window.darkMode = {
toggle: toggleTheme,
set: applyTheme,
get: () => document.documentElement.getAttribute('data-theme')
};
Comment on lines +140 to +145
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The window.darkMode object is exposed globally for debugging purposes, but this could lead to unintended side effects if other code modifies these functions or if there are naming conflicts. Consider either removing this exposure in production code, or namespacing it more specifically (e.g., window.__DESCRIPTUM_DARK_MODE__) to avoid potential conflicts.

Copilot uses AI. Check for mistakes.
})();
Loading
Loading