feat: widget de acessibilidade flutuante + fix carrossel mobile
- AccessibilityWidget: botão fixo bottom-right com painel de tamanho de texto (A/A+/A++) e toggle alto contraste, aplicados via classes no <html> - layout.scss: remove override global de min-height em botões/links; mantém apenas focus-visible, skip-link, reduced-motion e classes do widget a11y - HomeView: carrossel — botão de ação movido para abaixo do texto (não mais ao lado) para evitar compressão do texto no mobile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b6d47ba8a
commit
4658d60ad0
@ -2,6 +2,7 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
||||||
import { applyTemplate, applySurface } from '@/config/theme.config'
|
import { applyTemplate, applySurface } from '@/config/theme.config'
|
||||||
|
import AccessibilityWidget from '@/components/common/AccessibilityWidget.vue'
|
||||||
|
|
||||||
const prefeitura = usePrefeituraStore()
|
const prefeitura = usePrefeituraStore()
|
||||||
|
|
||||||
@ -12,12 +13,9 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Skip link: invisível até receber foco via Tab — permite ao usuário
|
<a href="#main-content" class="skip-link">Pular para o conteúdo principal</a>
|
||||||
de teclado pular a navegação e ir direto ao conteúdo principal -->
|
|
||||||
<a href="#main-content" class="skip-link">
|
|
||||||
Pular para o conteúdo principal
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<Toast />
|
<Toast />
|
||||||
|
<AccessibilityWidget />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -25,21 +25,12 @@ body {
|
|||||||
|
|
||||||
/* ── Acessibilidade global ─────────────────────────────────────────── */
|
/* ── Acessibilidade global ─────────────────────────────────────────── */
|
||||||
|
|
||||||
/*
|
|
||||||
* Focus ring visível e de alto contraste.
|
|
||||||
* Usa currentColor para se adaptar a qualquer tema de cor.
|
|
||||||
* Nunca remover — usuários de teclado e leitores de tela dependem disso.
|
|
||||||
*/
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 3px solid currentColor;
|
outline: 3px solid currentColor;
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Skip link — oculto até receber foco via Tab.
|
|
||||||
* Permite que usuários de teclado pulem a navegação e vão direto ao conteúdo.
|
|
||||||
*/
|
|
||||||
.skip-link {
|
.skip-link {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: -100%;
|
top: -100%;
|
||||||
@ -60,47 +51,14 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Respeitar a preferência de usuário por menos movimento (vestibular,
|
|
||||||
* epilepsia, sensibilidade visual). Afeta autoplay do carousel e transições.
|
|
||||||
*/
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garante que transições de rota também param */
|
|
||||||
.page-enter-active,
|
.page-enter-active,
|
||||||
.page-leave-active {
|
.page-leave-active {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Alvo de toque mínimo (WCAG 2.5.5) ────────────────────────────── */
|
/* ── Widget de acessibilidade — classes dinâmicas ───────────────────── */
|
||||||
/*
|
html.a11y-font-lg { font-size: 18px; }
|
||||||
* Botões e links pequenos precisam de área clicável mínima de 44×44px
|
html.a11y-font-xl { font-size: 20px; }
|
||||||
* para usuários com dificuldade motora ou em telas touch.
|
html.a11y-contrast { filter: contrast(1.45) saturate(0.85); }
|
||||||
*/
|
|
||||||
button,
|
|
||||||
[role='button'],
|
|
||||||
a {
|
|
||||||
min-height: 44px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Exceção para elementos inline de parágrafo onde forçar altura quebra
|
|
||||||
* o layout (links dentro de texto corrido).
|
|
||||||
*/
|
|
||||||
p a,
|
|
||||||
li a,
|
|
||||||
.inline-link {
|
|
||||||
min-height: unset;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|||||||
102
src/components/common/AccessibilityWidget.vue
Normal file
102
src/components/common/AccessibilityWidget.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const aberto = ref(false)
|
||||||
|
const nivelFonte = ref(0) // 0 = normal, 1 = grande, 2 = extra-grande
|
||||||
|
const altoContraste = ref(false)
|
||||||
|
|
||||||
|
const opcoesFonte = [
|
||||||
|
{ nivel: 0, label: 'A', title: 'Texto normal' },
|
||||||
|
{ nivel: 1, label: 'A+', title: 'Texto grande' },
|
||||||
|
{ nivel: 2, label: 'A++', title: 'Texto extra-grande' },
|
||||||
|
]
|
||||||
|
|
||||||
|
watch(nivelFonte, (val) => {
|
||||||
|
document.documentElement.classList.remove('a11y-font-lg', 'a11y-font-xl')
|
||||||
|
if (val === 1) document.documentElement.classList.add('a11y-font-lg')
|
||||||
|
if (val === 2) document.documentElement.classList.add('a11y-font-xl')
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(altoContraste, (val) => {
|
||||||
|
document.documentElement.classList.toggle('a11y-contrast', val)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed bottom-6 right-6 z-50 flex flex-col items-end gap-3">
|
||||||
|
|
||||||
|
<!-- Painel -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 translate-y-2 scale-95"
|
||||||
|
enter-to-class="opacity-100 translate-y-0 scale-100"
|
||||||
|
leave-active-class="transition-all duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100 translate-y-0 scale-100"
|
||||||
|
leave-to-class="opacity-0 translate-y-2 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="aberto"
|
||||||
|
id="a11y-panel"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Opções de acessibilidade"
|
||||||
|
class="bg-white rounded-2xl shadow-2xl border border-slate-200 p-5 w-64 origin-bottom-right"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<i class="pi pi-eye text-primary text-sm" aria-hidden="true" />
|
||||||
|
<p class="text-xs font-bold text-slate-600 uppercase tracking-wide">Acessibilidade</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tamanho do texto -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<p class="text-sm font-semibold text-slate-700 mb-2">Tamanho do texto</p>
|
||||||
|
<div class="flex gap-2" role="group" aria-label="Escolha o tamanho do texto">
|
||||||
|
<button
|
||||||
|
v-for="op in opcoesFonte"
|
||||||
|
:key="op.nivel"
|
||||||
|
:title="op.title"
|
||||||
|
:aria-pressed="nivelFonte === op.nivel"
|
||||||
|
:class="[
|
||||||
|
'flex-1 py-2 rounded-lg border text-sm font-bold transition-colors',
|
||||||
|
nivelFonte === op.nivel
|
||||||
|
? 'bg-primary text-white border-primary'
|
||||||
|
: 'bg-white text-slate-600 border-slate-200 hover:border-primary/40 hover:text-primary'
|
||||||
|
]"
|
||||||
|
@click="nivelFonte = op.nivel"
|
||||||
|
>
|
||||||
|
{{ op.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alto contraste -->
|
||||||
|
<div class="flex items-center justify-between pt-4 border-t border-slate-100">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-slate-700">Alto contraste</p>
|
||||||
|
<p class="text-xs text-slate-400 mt-0.5">Melhora a legibilidade</p>
|
||||||
|
</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
v-model="altoContraste"
|
||||||
|
:aria-label="altoContraste ? 'Desativar alto contraste' : 'Ativar alto contraste'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Botão flutuante -->
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'w-12 h-12 rounded-full shadow-lg border flex items-center justify-center transition-all hover:scale-105',
|
||||||
|
aberto
|
||||||
|
? 'bg-primary text-white border-primary shadow-primary/30'
|
||||||
|
: 'bg-white text-slate-600 border-slate-200 hover:border-slate-300 hover:shadow-xl'
|
||||||
|
]"
|
||||||
|
:aria-label="aberto ? 'Fechar painel de acessibilidade' : 'Abrir painel de acessibilidade'"
|
||||||
|
:aria-expanded="aberto"
|
||||||
|
aria-controls="a11y-panel"
|
||||||
|
@click="aberto = !aberto"
|
||||||
|
>
|
||||||
|
<i class="pi pi-eye text-lg" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -285,15 +285,12 @@ function continuar() {
|
|||||||
<i :class="['pi', aviso.icone, 'text-lg', corAviso[aviso.cor].icone]" />
|
<i :class="['pi', aviso.icone, 'text-lg', corAviso[aviso.cor].icone]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p class="font-semibold text-slate-800 text-sm">{{ aviso.titulo }}</p>
|
<p class="font-semibold text-slate-800 text-sm">{{ aviso.titulo }}</p>
|
||||||
<p class="text-xs text-slate-600 mt-0.5 leading-relaxed">{{ aviso.descricao }}</p>
|
<p class="text-xs text-slate-600 mt-0.5 leading-relaxed">{{ aviso.descricao }}</p>
|
||||||
</div>
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="aviso.acao"
|
v-if="aviso.acao"
|
||||||
:to="aviso.acao.to"
|
:to="aviso.acao.to"
|
||||||
class="flex-shrink-0"
|
class="inline-block mt-3"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
:label="aviso.acao.label"
|
:label="aviso.acao.label"
|
||||||
@ -304,7 +301,6 @@ function continuar() {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user