Menu de acessibilidade

This commit is contained in:
RafaeL_Monteiro 2025-10-07 22:40:14 -03:00
parent fe455a256a
commit 4cca0a53ea
7 changed files with 396 additions and 50 deletions

View File

@ -1,6 +1,6 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { useState } from "react";
// Suas páginas
import Login from "./pages/Login";
import Register from "./pages/Register";
import Forgot from "./pages/ForgotPassword";
@ -10,29 +10,19 @@ import PerfilFinanceiro from "./perfis/perfil_financeiro/PerfilFinanceiro";
import Perfiladm from "./perfis/Perfil_adm/Perfiladm";
import PerfilMedico from "./perfis/Perfil_medico/PerfilMedico";
// Componentes globais de acessibilidade
import VlibrasWidget from "./components/VlibrasWidget";
import BotaoAcessibilidade from "./components/botaoacessibilidade.jsx";
function App() {
// O estado controla qual view mostrar: false = Landing Page, true = Dashboard
const [isInternalView, setIsInternalView] = useState(false);
// const [isSecretaria, setIsSecretaria] = useState(false);
const handleEnterSystem = () => {
setIsInternalView(true);
};
const handleExitSystem = () => {
setIsInternalView(false);
};
// if (isSecretaria) {
// return <PerfilSecretaria onLogout={() => setIsSecretaria(false)} />;
// }
// Se não estiver na visualização interna, retorna a LandingPage.
if (!isInternalView) {
return (
return (
<Router>
<VlibrasWidget />
<BotaoAcessibilidade />
<Routes>
<Route path="/" element={<LandingPage onEnterSystem={handleEnterSystem}/>} />
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgotPassword" element={<Forgot />} />
@ -43,14 +33,8 @@ function App() {
<Route path="*" element={<h2>Página não encontrada</h2>} />
</Routes>
</Router>
)
}
// Se estiver na visualização interna, retorna o PerfilSecretaria
return (
// Passamos a função de saída (logout)
<PerfilSecretaria onLogout={handleExitSystem} />
);
}
export default App;
export default App;

View File

@ -0,0 +1,62 @@
// src/components/VlibrasWidget.jsx
import React, { useEffect } from 'react';
const VlibrasWidget = () => {
useEffect(() => {
// Cria o elemento div principal do Vlibras
const vwDiv = document.createElement('div');
vwDiv.setAttribute('vw', '');
vwDiv.classList.add('enabled');
vwDiv.id = 'vlibras-div'; // 🔹 ADICIONADO: ID para remoção segura
const vwAccessButton = document.createElement('div');
vwAccessButton.setAttribute('vw-access-button', '');
vwAccessButton.classList.add('active');
const vwPluginWrapper = document.createElement('div');
vwPluginWrapper.setAttribute('vw-plugin-wrapper', '');
const vwPluginTopWrapper = document.createElement('div');
vwPluginTopWrapper.classList.add('vw-plugin-top-wrapper');
vwPluginWrapper.appendChild(vwPluginTopWrapper);
vwDiv.appendChild(vwAccessButton);
vwDiv.appendChild(vwPluginWrapper);
document.body.appendChild(vwDiv);
// Adiciona o script principal do Vlibras
const script = document.createElement('script');
script.src = 'https://vlibras.gov.br/app/vlibras-plugin.js';
script.async = true;
script.id = 'vlibras-script'; // 🔹 ADICIONADO: ID para remoção segura
script.onload = () => {
// Inicializa o widget após o script carregar
// Certifica-se que a API está disponível globalmente
if (window.VLibras) {
new window.VLibras.Widget('https://vlibras.gov.br/app');
}
};
document.body.appendChild(script);
// 🔹 ATUALIZADO: Função de limpeza para remover os elementos pelos IDs
return () => {
const existingVwDiv = document.getElementById('vlibras-div');
if (existingVwDiv) {
document.body.removeChild(existingVwDiv);
}
const existingScript = document.getElementById('vlibras-script');
if (existingScript) {
document.body.removeChild(existingScript);
}
};
}, []); // O array vazio [] garante que o useEffect rode apenas uma vez
return null; // Este componente não renderiza nada visualmente
};
export default VlibrasWidget;

View File

@ -0,0 +1,198 @@
/* --- ESTILO PARA ESCONDER O BOTÃO ORIGINAL DO VLIBRAS --- */
[vw-access-button] {
display: none !important;
}
/* --- ESTILOS GERAIS DO COMPONENTE --- */
.container-acessibilidade {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99998;
display: flex;
flex-direction: column;
align-items: center;
}
.botao-flutuante-acessibilidade {
position: relative;
z-index: 2; /* Acima do menu */
background: linear-gradient(45deg, #007bff, #0056b3);
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
cursor: pointer;
box-shadow: 0 5px 15px rgba(0, 91, 179, 0.4);
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease;
margin-top: 15px; /* Distância do menu */
}
.botao-flutuante-acessibilidade:hover {
transform: scale(1.1);
box-shadow: 0 8px 20px rgba(0, 91, 179, 0.5);
}
/* --- ESTILOS DO MENU "BALÃO" --- */
.menu-opcoes {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
padding: 8px;
width: 280px;
z-index: 1; /* Abaixo do botão principal */
border: 1px solid #e9ecef;
/* Animação */
transform-origin: bottom center;
transform: translateY(10px) scale(0.95);
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.menu-opcoes.aberto {
transform: translateY(0) scale(1);
opacity: 1;
visibility: visible;
}
.menu-titulo {
font-size: 14px;
font-weight: 600;
color: #6c757d;
padding: 8px 12px;
border-bottom: 1px solid #f1f3f5;
margin-bottom: 5px;
transition: color 0.2s ease, border-bottom-color 0.2s ease;
}
/* --- ESTILOS DOS BOTÕES E DA CHECKBOX NO MENU --- */
.menu-opcoes button,
.checkbox-label-button {
display: flex;
align-items: center;
gap: 12px;
background-color: transparent;
border: none;
padding: 12px;
text-align: left;
cursor: pointer;
font-size: 16px;
color: #212529;
width: 100%;
border-radius: 8px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.menu-opcoes button:hover,
.checkbox-label-button:hover {
background-color: #f8f9fa;
}
/* --- ESTILO DO INTERRUPTOR (CHECKBOX) --- */
.checkbox-label-button {
justify-content: space-between;
}
.checkbox-label-button input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
position: relative;
width: 44px;
height: 24px;
background-color: #ced4da;
border-radius: 12px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
.checkbox-label-button input[type="checkbox"]::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: transform 0.3s ease-in-out;
}
.checkbox-label-button input[type="checkbox"]:checked {
background-color: #0d6efd;
}
.checkbox-label-button input[type="checkbox"]:checked::before {
transform: translateX(20px);
}
/* --- ✨ NOVOS ESTILOS PARA O CONTROLE DE FONTE ✨ --- */
.font-size-control {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
color: #212529;
transition: color 0.2s ease;
border-top: 1px solid #f1f3f5;
margin-top: 5px;
}
.font-size-label {
font-size: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.font-size-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.font-size-buttons button {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-weight: bold;
font-size: 16px;
background-color: #e9ecef;
color: #495057;
border: 1px solid #dee2e6;
border-radius: 6px;
width: 36px;
height: 32px;
padding: 0;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease, color 0.2s ease;
}
.font-size-buttons button:hover {
background-color: #dee2e6;
}
.font-size-buttons button:disabled {
background-color: #f8f9fa;
color: #adb5bd;
cursor: not-allowed;
border-color: #f1f3f5;
}
.font-size-display {
font-size: 14px;
font-weight: 600;
color: #495057;
min-width: 45px;
text-align: center;
transition: color 0.2s ease;
}

View File

@ -0,0 +1,101 @@
import React, { useState, useEffect, useRef } from 'react';
import './botaoacessibilidade.css'; // Importando o CSS
function BotaoAcessibilidade() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isReadOnHoverActive, setIsReadOnHoverActive] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
const lastSpokenTargetRef = useRef(null);
useEffect(() => {
if (!isReadOnHoverActive) {
window.speechSynthesis.cancel();
return;
}
const handleMouseOver = (event) => {
const target = event.target;
if (target && target !== lastSpokenTargetRef.current && target.innerText) {
const text = target.innerText.trim();
if (text.length > 0 && ['P', 'H1', 'H2', 'H3', 'BUTTON', 'A', 'LI', 'LABEL'].includes(target.tagName)) {
lastSpokenTargetRef.current = target;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'pt-BR';
window.speechSynthesis.speak(utterance);
}
}
};
document.body.addEventListener('mouseover', handleMouseOver);
return () => {
document.body.removeEventListener('mouseover', handleMouseOver);
window.speechSynthesis.cancel();
};
}, [isReadOnHoverActive]);
const handleVlibrasClick = () => {
const originalVlibrasButton = document.querySelector('[vw-access-button]');
if (originalVlibrasButton) {
originalVlibrasButton.click();
} else {
alert("O Vlibras não pôde ser ativado.");
}
setIsMenuOpen(false);
};
const handleReadAloud = () => {
const selectedText = window.getSelection().toString().trim();
if (selectedText) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(selectedText);
utterance.lang = 'pt-BR';
window.speechSynthesis.speak(utterance);
} else {
alert("Por favor, selecione um texto para ler em voz alta.");
}
setIsMenuOpen(false);
};
return (
<div className={`container-acessibilidade ${isDarkMode ? 'dark-mode' : ''}`}>
<div className={`menu-opcoes ${isMenuOpen ? 'aberto' : ''}`}>
<div className="menu-titulo">Acessibilidade</div>
<label htmlFor="darkModeCheckbox" className="checkbox-label-button">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
Modo Escuro
<input
type="checkbox"
id="darkModeCheckbox"
checked={isDarkMode}
onChange={() => setIsDarkMode(!isDarkMode)}
/>
</label>
<label htmlFor="readOnHoverCheckbox" className="checkbox-label-button">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path></svg>
Leitura instantânea
<input
type="checkbox"
id="readOnHoverCheckbox"
checked={isReadOnHoverActive}
onChange={() => setIsReadOnHoverActive(!isReadOnHoverActive)}
/>
</label>
<button onClick={handleVlibrasClick}>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0" /><path d="M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2" /><path d="M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8" /><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2.3" /></svg>
Traduzir para LIBRAS
</button>
</div>
<button
className="botao-flutuante-acessibilidade"
onClick={() => setIsMenuOpen(!isMenuOpen)}
title="Menu de Acessibilidade"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30" fill="white">
<path d="M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z" />
</svg>
</button>
</div>
);
}
export default BotaoAcessibilidade;

View File

@ -7,10 +7,10 @@ import { AuthProvider } from "./components/utils/AuthProvider";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
// <React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>,
// </React.StrictMode>,
);

View File

@ -10,13 +10,13 @@ function TableDoctor() {
const [search, setSearch] = useState("");
const [filtroAniversariante, setFiltroAniversariante] = useState(false);
// estados do modal
// estados do modal
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDoctorId, setSelectedDoctorId] = useState(null);
// Função para excluir médicos
const deleteDoctor = async (id) => {
const authHeader = getAuthorizationHeader()
console.log(id, 'teu id')
@ -56,11 +56,11 @@ function TableDoctor() {
// Buscar médicos da API
useEffect(() => {
const authHeader = getAuthorizationHeader()
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
console.log(authHeader, 'aqui autorização')
var myHeaders = new Headers();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", `${authHeader}`);
var requestOptions = {
@ -75,14 +75,14 @@ function TableDoctor() {
.catch(error => console.log('error', error));
}, []);
// Filtrar médicos pelo campo de pesquisa e aniversariantes
const medicosFiltrados = medicos.filter(
// CORREÇÃO AQUI: Verificamos se 'medicos' é um array antes de filtrar.
const medicosFiltrados = Array.isArray(medicos) ? medicos.filter(
(medico) =>
`${medico.nome} ${medico.cpf} ${medico.email} ${medico.telefone}`
.toLowerCase()
.includes(search.toLowerCase()) &&
(filtroAniversariante ? ehAniversariante(medico.data_nascimento) : true)
);
) : []; // Se não for um array, usamos um array vazio como fallback.
return (
<>
@ -180,7 +180,7 @@ function TableDoctor() {
</button>
</Link>
{/* Editar */}
{/* Editar */}
<Link to={`${medico.id}/edit`}>
<button
className="btn btn-sm"
@ -231,7 +231,7 @@ function TableDoctor() {
</section>
</div>
{/* Modal de confirmação de exclusão */}
{/* Modal de confirmação de exclusão */}
{showDeleteModal && (
<div
className="modal fade show"

View File

@ -15,7 +15,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const [filtroVIP, setFiltroVIP] = useState(false);
const [filtroAniversariante, setFiltroAniversariante] = useState(false);
// estados do modal
// estados do modal
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null);
@ -100,11 +100,11 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
// Requisição inicial para buscar pacientes
useEffect(() => {
const authHeader = getAuthorizationHeader()
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
console.log(authHeader, 'aqui autorização')
var myHeaders = new Headers();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", `${authHeader}`);
var requestOptions = {
@ -131,7 +131,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
);
};
const pacientesFiltrados = pacientes.filter((paciente) => {
// CORREÇÃO AQUI: Verificamos se 'pacientes' é um array antes de filtrar.
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
const textoCompletoPaciente = `${paciente.nome} ${paciente.cpf} ${paciente.email} ${paciente.telefone}`.toLowerCase();
const passaBusca = textoCompletoPaciente.includes(search.toLowerCase());
const passaVIP = filtroVIP ? paciente.vip === true : true;
@ -141,7 +142,7 @@ const pacientesFiltrados = pacientes.filter((paciente) => {
: true;
return passaBusca && passaVIP && passaConvenio && passaAniversario;
});
}) : []; // Se não for um array, usamos um array vazio como fallback.
return (
@ -345,7 +346,7 @@ const pacientesFiltrados = pacientes.filter((paciente) => {
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
@ -364,7 +365,7 @@ const pacientesFiltrados = pacientes.filter((paciente) => {
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
@ -373,7 +374,7 @@ const pacientesFiltrados = pacientes.filter((paciente) => {
Cancelar
</button>
<button
type="button"
className="btn btn-danger"