feat(a11y): implementa acessibilidade WCAG 2.1 AA em todo o portal público
- layout.scss: focus-visible ring, skip link, prefers-reduced-motion, touch targets ≥44px, font-size base 16px - App.vue: skip link + aplica tema dinâmico no onMounted (após PrimeVue inicializado) - composables/useMotion.js: detecta prefers-reduced-motion com listener reativo - HomeView: aria-label em seções, carrossel respeita reduced-motion, contraste de texto melhorado - LoginView: h1 correto, label+for+aria-describedby no campo senha, role=alert no erro, touch targets nos botões - DocumentoInput: aria-label, aria-required, autocomplete=username - AppHeader: aria-label no logo e nav, aria-current na rota ativa, aria-hidden nos ícones decorativos - AppFooter: role=contentinfo, nav com aria-label, contraste text-slate-400→500, ícone decorativo aria-hidden - PortalLayout: role=banner, aria-label no nav, aria-current nas rotas, aria-live no nome do usuário - PublicLayout: tabindex=-1 no main para receber foco via skip link Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5a7f4ba07a
commit
6b6d47ba8a
@ -12,6 +12,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Skip link: invisível até receber foco via Tab — permite ao usuário
|
||||
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 />
|
||||
<Toast />
|
||||
</template>
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
/* Estilos globais de layout — sem @apply (regra do projeto) */
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 16px; /* base mínimo para leitura confortável */
|
||||
}
|
||||
|
||||
#app {
|
||||
@ -14,7 +13,7 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Transições de rota */
|
||||
/* ── Transições de rota ────────────────────────────────────────────── */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
@ -23,3 +22,85 @@ body {
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
outline: 3px solid currentColor;
|
||||
outline-offset: 3px;
|
||||
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 {
|
||||
position: fixed;
|
||||
top: -100%;
|
||||
left: 1rem;
|
||||
z-index: 9999;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
transition: top 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
*,
|
||||
*::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-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Alvo de toque mínimo (WCAG 2.5.5) ────────────────────────────── */
|
||||
/*
|
||||
* Botões e links pequenos precisam de área clicável mínima de 44×44px
|
||||
* para usuários com dificuldade motora ou em telas touch.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@ -44,8 +44,11 @@ function onInput(e) {
|
||||
:value="valorFormatado"
|
||||
:placeholder="placeholder"
|
||||
inputmode="numeric"
|
||||
autocomplete="username"
|
||||
class="w-full text-lg tracking-wide"
|
||||
size="large"
|
||||
aria-label="CPF ou CNPJ"
|
||||
aria-required="true"
|
||||
@input="onInput"
|
||||
/>
|
||||
<span
|
||||
|
||||
@ -2,18 +2,18 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="bg-white border-t border-slate-200 mt-auto">
|
||||
<footer class="bg-white border-t border-slate-200 mt-auto" role="contentinfo">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 text-sm text-slate-500">
|
||||
<i class="pi pi-building text-blue-600" />
|
||||
<div class="flex items-center gap-2 text-sm text-slate-600">
|
||||
<i class="pi pi-building text-primary" aria-hidden="true" />
|
||||
<span>Portal do Contribuinte — ModumFiscal</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-xs text-slate-400">
|
||||
<a href="#" class="hover:text-slate-600 transition-colors">Política de Privacidade</a>
|
||||
<a href="#" class="hover:text-slate-600 transition-colors">Termos de Uso</a>
|
||||
<a href="#" class="hover:text-slate-600 transition-colors">Acessibilidade</a>
|
||||
</div>
|
||||
<nav aria-label="Links institucionais" class="flex items-center gap-4 text-xs text-slate-500">
|
||||
<a href="#" class="hover:text-slate-700 transition-colors">Política de Privacidade</a>
|
||||
<a href="#" class="hover:text-slate-700 transition-colors">Termos de Uso</a>
|
||||
<a href="#" class="hover:text-slate-700 transition-colors">Acessibilidade</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -14,14 +14,18 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
||||
<header class="bg-white border-b border-slate-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
|
||||
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-3 min-w-0">
|
||||
<RouterLink
|
||||
:to="{ name: 'home' }"
|
||||
class="flex items-center gap-3 min-w-0"
|
||||
:aria-label="`Ir para a página inicial — ${prefeitura.nomePrefeitura || 'ModumFiscal'}`"
|
||||
>
|
||||
<img
|
||||
:src="logoSrc"
|
||||
alt="Logo da Prefeitura"
|
||||
:alt="`Logo de ${prefeitura.nomePrefeitura || 'ModumFiscal'}`"
|
||||
class="h-9 w-auto object-contain flex-shrink-0"
|
||||
/>
|
||||
|
||||
<div class="leading-tight min-w-0">
|
||||
<div class="leading-tight min-w-0" aria-hidden="true">
|
||||
<p class="text-xs text-slate-500 font-normal truncate">Portal do Contribuinte</p>
|
||||
<p class="text-sm font-semibold text-slate-800 truncate">
|
||||
{{ prefeitura.nomePrefeitura || 'ModumFiscal' }}
|
||||
@ -29,10 +33,11 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-4">
|
||||
<nav aria-label="Navegação principal" class="hidden md:flex items-center gap-4">
|
||||
<RouterLink
|
||||
:to="{ name: 'servicos' }"
|
||||
class="text-sm text-slate-600 hover:text-primary transition-colors"
|
||||
:aria-current="$route.name === 'servicos' ? 'page' : undefined"
|
||||
>
|
||||
Serviços
|
||||
</RouterLink>
|
||||
@ -41,12 +46,12 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
||||
<div class="flex items-center gap-3">
|
||||
<template v-if="auth.isAuthenticated">
|
||||
<RouterLink :to="{ name: 'painel' }">
|
||||
<Button label="Meu Painel" icon="pi pi-user" size="small" />
|
||||
<Button label="Meu Painel" icon="pi pi-user" size="small" aria-label="Ir para o meu painel" />
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<RouterLink :to="{ name: 'home' }">
|
||||
<Button label="Entrar" icon="pi pi-sign-in" size="small" />
|
||||
<Button label="Entrar" icon="pi pi-sign-in" size="small" aria-label="Entrar no portal" />
|
||||
</RouterLink>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
28
src/composables/useMotion.js
Normal file
28
src/composables/useMotion.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
/**
|
||||
* Detecta a preferência do usuário por movimento reduzido.
|
||||
* Usa a media query `prefers-reduced-motion` e reage a mudanças em tempo real.
|
||||
* Uso: const { prefersReducedMotion } = useMotion()
|
||||
*/
|
||||
export function useMotion() {
|
||||
const prefersReducedMotion = ref(false)
|
||||
|
||||
let mq
|
||||
|
||||
function onChange(e) {
|
||||
prefersReducedMotion.value = e.matches
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mq = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
prefersReducedMotion.value = mq.matches
|
||||
mq.addEventListener('change', onChange)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
mq?.removeEventListener('change', onChange)
|
||||
})
|
||||
|
||||
return { prefersReducedMotion }
|
||||
}
|
||||
@ -21,42 +21,50 @@ function sair() {
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-slate-50">
|
||||
<header class="bg-white border-b border-slate-200 sticky top-0 z-40">
|
||||
<header class="bg-white border-b border-slate-200 sticky top-0 z-40" role="banner">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<RouterLink :to="{ name: 'painel' }" class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-blue-700 rounded-lg flex items-center justify-center">
|
||||
<i class="pi pi-building text-white text-sm" />
|
||||
<RouterLink
|
||||
:to="{ name: 'painel' }"
|
||||
class="flex items-center gap-3"
|
||||
aria-label="Ir para o painel principal"
|
||||
>
|
||||
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<i class="pi pi-building text-white text-sm" aria-hidden="true" />
|
||||
</div>
|
||||
<span class="font-semibold text-slate-800">Portal do Contribuinte</span>
|
||||
</RouterLink>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-1">
|
||||
<nav aria-label="Menu do contribuinte" class="hidden md:flex items-center gap-1">
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:to="{ name: item.name }"
|
||||
class="px-3 py-2 rounded-lg text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition-colors"
|
||||
active-class="bg-blue-50 text-blue-700 font-medium"
|
||||
class="px-4 py-2 rounded-lg text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition-colors"
|
||||
active-class="bg-primary/10 text-primary font-semibold"
|
||||
:aria-current="$route.name === item.name ? 'page' : undefined"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="hidden sm:block text-sm text-slate-600">{{ auth.nomeUsuario }}</span>
|
||||
<span class="hidden sm:block text-sm text-slate-600" aria-live="polite">
|
||||
{{ auth.nomeUsuario }}
|
||||
</span>
|
||||
<Button
|
||||
label="Sair"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
icon="pi pi-sign-out"
|
||||
outlined
|
||||
aria-label="Sair do portal"
|
||||
@click="sair"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<main id="main-content" tabindex="-1" class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8 outline-none">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="page" mode="out-in">
|
||||
<component :is="Component" />
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-slate-50">
|
||||
<AppHeader />
|
||||
<main class="flex-1">
|
||||
|
||||
<!-- tabindex="-1" permite que o skip link mova o foco para cá -->
|
||||
<main id="main-content" tabindex="-1" class="flex-1 outline-none">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="page" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</main>
|
||||
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
||||
import { useMotion } from '@/composables/useMotion'
|
||||
import DocumentoInput from '@/components/auth/DocumentoInput.vue'
|
||||
import ServiceCard from '@/components/common/ServiceCard.vue'
|
||||
|
||||
import bgTutoia from '@/assets/images/bg-tutoia.jpeg'
|
||||
|
||||
const { prefersReducedMotion } = useMotion()
|
||||
|
||||
const router = useRouter()
|
||||
const prefeitura = usePrefeituraStore()
|
||||
|
||||
@ -152,7 +155,7 @@ function continuar() {
|
||||
Serviços municipais<br />
|
||||
<span class="text-white/80">na palma da mão</span>
|
||||
</h1>
|
||||
<p class="text-white/65 text-base mb-8 leading-relaxed max-w-md">
|
||||
<p class="text-white/80 text-base mb-8 leading-relaxed max-w-md">
|
||||
Emita certidões, consulte débitos e acesse todos os serviços
|
||||
da prefeitura sem precisar sair de casa.
|
||||
</p>
|
||||
@ -170,7 +173,7 @@ function continuar() {
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-white group-hover:text-white/80 transition-colors">{{ s.titulo }}</p>
|
||||
<p class="text-xs text-white/50">{{ s.descricao }}</p>
|
||||
<p class="text-xs text-white/70">{{ s.descricao }}</p>
|
||||
</div>
|
||||
<i class="pi pi-chevron-right text-white/30 text-xs ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</li>
|
||||
@ -178,7 +181,7 @@ function continuar() {
|
||||
|
||||
<RouterLink
|
||||
:to="{ name: 'servicos' }"
|
||||
class="inline-flex items-center gap-2 text-sm text-white/60 hover:text-white transition-colors font-medium"
|
||||
class="inline-flex items-center gap-2 text-sm text-white/80 hover:text-white transition-colors font-medium"
|
||||
>
|
||||
Ver todos os serviços disponíveis
|
||||
<i class="pi pi-arrow-right text-xs" />
|
||||
@ -254,15 +257,17 @@ function continuar() {
|
||||
</section>
|
||||
|
||||
<!-- ─── CAROUSEL DE AVISOS ────────────────────────────────────────── -->
|
||||
<section class="bg-white border-b border-slate-100">
|
||||
<section class="bg-white border-b border-slate-100" aria-label="Avisos e comunicados">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- autoplay desativado quando prefers-reduced-motion está ativo -->
|
||||
<Carousel
|
||||
:value="avisos"
|
||||
:num-visible="1"
|
||||
:num-scroll="1"
|
||||
:autoplay-interval="6000"
|
||||
:autoplay-interval="prefersReducedMotion ? 0 : 6000"
|
||||
circular
|
||||
class="aviso-carousel"
|
||||
aria-label="Avisos da prefeitura"
|
||||
>
|
||||
<template #item="{ data: aviso }">
|
||||
<div
|
||||
@ -306,11 +311,11 @@ function continuar() {
|
||||
</section>
|
||||
|
||||
<!-- ─── SERVIÇOS AUTENTICADOS ─────────────────────────────────────── -->
|
||||
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12" aria-labelledby="servicos-logados-titulo">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-800">Área logada</h2>
|
||||
<p class="text-sm text-slate-500 mt-0.5">Serviços disponíveis após login</p>
|
||||
<h2 id="servicos-logados-titulo" class="text-xl font-bold text-slate-800">Área logada</h2>
|
||||
<p class="text-sm text-slate-600 mt-0.5">Serviços disponíveis após login</p>
|
||||
</div>
|
||||
<span class="hidden sm:flex items-center gap-1.5 bg-primary/8 text-primary text-xs font-semibold px-3 py-1.5 rounded-full border border-primary/15">
|
||||
<i class="pi pi-lock text-xs" />
|
||||
|
||||
@ -8,6 +8,8 @@ const router = useRouter()
|
||||
const senha = ref('')
|
||||
const carregando = ref(false)
|
||||
const erro = ref('')
|
||||
const senhaId = 'campo-senha'
|
||||
const erroId = 'erro-senha'
|
||||
|
||||
const docBruto = computed(() => (route.query.doc ?? '').replace(/\D/g, ''))
|
||||
|
||||
@ -34,7 +36,7 @@ function trocarDocumento() {
|
||||
|
||||
async function entrar() {
|
||||
if (!senha.value) {
|
||||
erro.value = 'Informe a senha.'
|
||||
erro.value = 'Informe a senha para continuar.'
|
||||
return
|
||||
}
|
||||
erro.value = ''
|
||||
@ -51,45 +53,45 @@ async function entrar() {
|
||||
<div class="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
|
||||
<!-- Card de login -->
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
|
||||
|
||||
<!-- Cabeçalho azul -->
|
||||
<div class="bg-gradient-to-r from-blue-700 to-blue-800 px-8 py-6">
|
||||
<!-- Cabeçalho — h1 para hierarquia de heading correta -->
|
||||
<div class="bg-gradient-to-r from-primary-700 to-primary-800 px-8 py-6 bg-primary">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center" aria-hidden="true">
|
||||
<i class="pi pi-lock text-white text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-semibold">Acesso seguro</p>
|
||||
<p class="text-blue-200 text-xs">Portal do Contribuinte</p>
|
||||
<h1 class="text-white font-bold text-base leading-tight">Acesso seguro</h1>
|
||||
<p class="text-white/80 text-xs mt-0.5">Portal do Contribuinte</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo -->
|
||||
<div class="px-8 py-8 space-y-6">
|
||||
|
||||
<!-- Documento identificado -->
|
||||
<div>
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-2">
|
||||
<p class="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">
|
||||
Entrando como
|
||||
</p>
|
||||
<div class="flex items-center justify-between bg-slate-50 border border-slate-200 rounded-xl px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i class="pi pi-id-card text-blue-700 text-sm" />
|
||||
<div class="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center" aria-hidden="true">
|
||||
<i class="pi pi-id-card text-primary text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-mono font-medium text-slate-800 text-sm">{{ docFormatado }}</p>
|
||||
<p class="text-xs text-slate-500">{{ tipoDoc }}</p>
|
||||
<p class="font-mono font-semibold text-slate-800 text-sm">{{ docFormatado }}</p>
|
||||
<p class="text-xs text-slate-600">{{ tipoDoc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Alvo de 44px via py-3 px-3 -->
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1 transition-colors"
|
||||
class="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary font-semibold px-3 py-3 rounded-lg hover:bg-primary/8 transition-colors"
|
||||
aria-label="Trocar documento de identificação"
|
||||
@click="trocarDocumento"
|
||||
>
|
||||
<i class="pi pi-pencil text-xs" />
|
||||
<i class="pi pi-pencil text-xs" aria-hidden="true" />
|
||||
Trocar
|
||||
</button>
|
||||
</div>
|
||||
@ -97,26 +99,36 @@ async function entrar() {
|
||||
|
||||
<!-- Senha -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1.5">
|
||||
<label :for="senhaId" class="block text-sm font-semibold text-slate-700 mb-1.5">
|
||||
Senha
|
||||
</label>
|
||||
<Password
|
||||
v-model="senha"
|
||||
:input-id="senhaId"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
placeholder="Digite sua senha"
|
||||
class="w-full"
|
||||
input-class="w-full"
|
||||
size="large"
|
||||
:invalid="!!erro"
|
||||
:input-props="{ 'aria-describedby': erro ? erroId : undefined, 'aria-invalid': !!erro || undefined }"
|
||||
@keyup.enter="entrar"
|
||||
/>
|
||||
<p v-if="erro" class="mt-1.5 text-xs text-red-600 flex items-center gap-1">
|
||||
<i class="pi pi-exclamation-circle" />
|
||||
<!-- aria-live="assertive": anuncia o erro imediatamente para leitores de tela -->
|
||||
<p
|
||||
:id="erroId"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
class="mt-2 text-sm text-red-700 font-medium flex items-center gap-1.5 min-h-[1.25rem]"
|
||||
>
|
||||
<template v-if="erro">
|
||||
<i class="pi pi-exclamation-circle text-sm" aria-hidden="true" />
|
||||
{{ erro }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Botão entrar -->
|
||||
<Button
|
||||
label="Entrar"
|
||||
icon="pi pi-sign-in"
|
||||
@ -126,35 +138,35 @@ async function entrar() {
|
||||
@click="entrar"
|
||||
/>
|
||||
|
||||
<!-- Links secundários -->
|
||||
<div class="text-center space-y-2">
|
||||
<div class="text-center space-y-1">
|
||||
<RouterLink
|
||||
:to="{ name: 'primeiro-acesso' }"
|
||||
class="block text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
class="block text-sm text-primary font-medium py-2 hover:underline"
|
||||
>
|
||||
Esqueci minha senha
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:to="{ name: 'credenciamento' }"
|
||||
class="block text-xs text-slate-500 hover:text-slate-700 transition-colors"
|
||||
class="block text-sm text-slate-600 py-2 hover:text-slate-800 transition-colors"
|
||||
>
|
||||
Ainda não tem acesso? Credenciar-se
|
||||
Ainda não tem acesso? <span class="text-primary font-semibold">Credenciar-se</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voltar -->
|
||||
<div class="text-center mt-6">
|
||||
<!-- Voltar — alvo de 44px via py-3 -->
|
||||
<div class="text-center mt-4">
|
||||
<button
|
||||
class="text-sm text-slate-500 hover:text-slate-700 transition-colors flex items-center gap-1.5 mx-auto"
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-800 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100"
|
||||
@click="router.push({ name: 'home' })"
|
||||
>
|
||||
<i class="pi pi-arrow-left text-xs" />
|
||||
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
|
||||
Voltar à página inicial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user