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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<RouterView />
|
||||||
<Toast />
|
<Toast />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
/* Estilos globais de layout — sem @apply (regra do projeto) */
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: 'DM Sans', system-ui, sans-serif;
|
font-family: 'DM Sans', system-ui, sans-serif;
|
||||||
|
font-size: 16px; /* base mínimo para leitura confortável */
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@ -14,7 +13,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transições de rota */
|
/* ── Transições de rota ────────────────────────────────────────────── */
|
||||||
.page-enter-active,
|
.page-enter-active,
|
||||||
.page-leave-active {
|
.page-leave-active {
|
||||||
transition: opacity 0.18s ease;
|
transition: opacity 0.18s ease;
|
||||||
@ -23,3 +22,85 @@ body {
|
|||||||
.page-leave-to {
|
.page-leave-to {
|
||||||
opacity: 0;
|
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"
|
:value="valorFormatado"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
|
autocomplete="username"
|
||||||
class="w-full text-lg tracking-wide"
|
class="w-full text-lg tracking-wide"
|
||||||
size="large"
|
size="large"
|
||||||
|
aria-label="CPF ou CNPJ"
|
||||||
|
aria-required="true"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -2,18 +2,18 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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 flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-2 text-sm text-slate-500">
|
<div class="flex items-center gap-2 text-sm text-slate-600">
|
||||||
<i class="pi pi-building text-blue-600" />
|
<i class="pi pi-building text-primary" aria-hidden="true" />
|
||||||
<span>Portal do Contribuinte — ModumFiscal</span>
|
<span>Portal do Contribuinte — ModumFiscal</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 text-xs text-slate-400">
|
<nav aria-label="Links institucionais" class="flex items-center gap-4 text-xs text-slate-500">
|
||||||
<a href="#" class="hover:text-slate-600 transition-colors">Política de Privacidade</a>
|
<a href="#" class="hover:text-slate-700 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-700 transition-colors">Termos de Uso</a>
|
||||||
<a href="#" class="hover:text-slate-600 transition-colors">Acessibilidade</a>
|
<a href="#" class="hover:text-slate-700 transition-colors">Acessibilidade</a>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -14,14 +14,18 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
|||||||
<header class="bg-white border-b border-slate-200">
|
<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">
|
<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
|
<img
|
||||||
:src="logoSrc"
|
:src="logoSrc"
|
||||||
alt="Logo da Prefeitura"
|
:alt="`Logo de ${prefeitura.nomePrefeitura || 'ModumFiscal'}`"
|
||||||
class="h-9 w-auto object-contain flex-shrink-0"
|
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-xs text-slate-500 font-normal truncate">Portal do Contribuinte</p>
|
||||||
<p class="text-sm font-semibold text-slate-800 truncate">
|
<p class="text-sm font-semibold text-slate-800 truncate">
|
||||||
{{ prefeitura.nomePrefeitura || 'ModumFiscal' }}
|
{{ prefeitura.nomePrefeitura || 'ModumFiscal' }}
|
||||||
@ -29,10 +33,11 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
|||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</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
|
<RouterLink
|
||||||
:to="{ name: 'servicos' }"
|
:to="{ name: 'servicos' }"
|
||||||
class="text-sm text-slate-600 hover:text-primary transition-colors"
|
class="text-sm text-slate-600 hover:text-primary transition-colors"
|
||||||
|
:aria-current="$route.name === 'servicos' ? 'page' : undefined"
|
||||||
>
|
>
|
||||||
Serviços
|
Serviços
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@ -41,12 +46,12 @@ const logoSrc = computed(() => prefeitura.pathLogo || logoFallback)
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<template v-if="auth.isAuthenticated">
|
<template v-if="auth.isAuthenticated">
|
||||||
<RouterLink :to="{ name: 'painel' }">
|
<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>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<RouterLink :to="{ name: 'home' }">
|
<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>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col bg-slate-50">
|
<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">
|
<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">
|
<RouterLink
|
||||||
<div class="w-8 h-8 bg-blue-700 rounded-lg flex items-center justify-center">
|
:to="{ name: 'painel' }"
|
||||||
<i class="pi pi-building text-white text-sm" />
|
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>
|
</div>
|
||||||
<span class="font-semibold text-slate-800">Portal do Contribuinte</span>
|
<span class="font-semibold text-slate-800">Portal do Contribuinte</span>
|
||||||
</RouterLink>
|
</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
|
<RouterLink
|
||||||
v-for="item in navItems"
|
v-for="item in navItems"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
:to="{ name: 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"
|
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-blue-50 text-blue-700 font-medium"
|
active-class="bg-primary/10 text-primary font-semibold"
|
||||||
|
:aria-current="$route.name === item.name ? 'page' : undefined"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<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
|
<Button
|
||||||
label="Sair"
|
label="Sair"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
icon="pi pi-sign-out"
|
icon="pi pi-sign-out"
|
||||||
outlined
|
outlined
|
||||||
|
aria-label="Sair do portal"
|
||||||
@click="sair"
|
@click="sair"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 }">
|
<RouterView v-slot="{ Component }">
|
||||||
<Transition name="page" mode="out-in">
|
<Transition name="page" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
|
|||||||
@ -4,13 +4,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col bg-slate-50">
|
<div class="min-h-screen flex flex-col bg-slate-50">
|
||||||
<AppHeader />
|
<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 }">
|
<RouterView v-slot="{ Component }">
|
||||||
<Transition name="page" mode="out-in">
|
<Transition name="page" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,11 +2,14 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
||||||
|
import { useMotion } from '@/composables/useMotion'
|
||||||
import DocumentoInput from '@/components/auth/DocumentoInput.vue'
|
import DocumentoInput from '@/components/auth/DocumentoInput.vue'
|
||||||
import ServiceCard from '@/components/common/ServiceCard.vue'
|
import ServiceCard from '@/components/common/ServiceCard.vue'
|
||||||
|
|
||||||
import bgTutoia from '@/assets/images/bg-tutoia.jpeg'
|
import bgTutoia from '@/assets/images/bg-tutoia.jpeg'
|
||||||
|
|
||||||
|
const { prefersReducedMotion } = useMotion()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const prefeitura = usePrefeituraStore()
|
const prefeitura = usePrefeituraStore()
|
||||||
|
|
||||||
@ -152,7 +155,7 @@ function continuar() {
|
|||||||
Serviços municipais<br />
|
Serviços municipais<br />
|
||||||
<span class="text-white/80">na palma da mão</span>
|
<span class="text-white/80">na palma da mão</span>
|
||||||
</h1>
|
</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
|
Emita certidões, consulte débitos e acesse todos os serviços
|
||||||
da prefeitura sem precisar sair de casa.
|
da prefeitura sem precisar sair de casa.
|
||||||
</p>
|
</p>
|
||||||
@ -170,7 +173,7 @@ function continuar() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-white group-hover:text-white/80 transition-colors">{{ s.titulo }}</p>
|
<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>
|
</div>
|
||||||
<i class="pi pi-chevron-right text-white/30 text-xs ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
<i class="pi pi-chevron-right text-white/30 text-xs ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</li>
|
</li>
|
||||||
@ -178,7 +181,7 @@ function continuar() {
|
|||||||
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'servicos' }"
|
: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
|
Ver todos os serviços disponíveis
|
||||||
<i class="pi pi-arrow-right text-xs" />
|
<i class="pi pi-arrow-right text-xs" />
|
||||||
@ -254,15 +257,17 @@ function continuar() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── CAROUSEL DE AVISOS ────────────────────────────────────────── -->
|
<!-- ─── 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">
|
<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
|
<Carousel
|
||||||
:value="avisos"
|
:value="avisos"
|
||||||
:num-visible="1"
|
:num-visible="1"
|
||||||
:num-scroll="1"
|
:num-scroll="1"
|
||||||
:autoplay-interval="6000"
|
:autoplay-interval="prefersReducedMotion ? 0 : 6000"
|
||||||
circular
|
circular
|
||||||
class="aviso-carousel"
|
class="aviso-carousel"
|
||||||
|
aria-label="Avisos da prefeitura"
|
||||||
>
|
>
|
||||||
<template #item="{ data: aviso }">
|
<template #item="{ data: aviso }">
|
||||||
<div
|
<div
|
||||||
@ -306,11 +311,11 @@ function continuar() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── SERVIÇOS AUTENTICADOS ─────────────────────────────────────── -->
|
<!-- ─── 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 class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-slate-800">Área logada</h2>
|
<h2 id="servicos-logados-titulo" 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>
|
<p class="text-sm text-slate-600 mt-0.5">Serviços disponíveis após login</p>
|
||||||
</div>
|
</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">
|
<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" />
|
<i class="pi pi-lock text-xs" />
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const router = useRouter()
|
|||||||
const senha = ref('')
|
const senha = ref('')
|
||||||
const carregando = ref(false)
|
const carregando = ref(false)
|
||||||
const erro = ref('')
|
const erro = ref('')
|
||||||
|
const senhaId = 'campo-senha'
|
||||||
|
const erroId = 'erro-senha'
|
||||||
|
|
||||||
const docBruto = computed(() => (route.query.doc ?? '').replace(/\D/g, ''))
|
const docBruto = computed(() => (route.query.doc ?? '').replace(/\D/g, ''))
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ function trocarDocumento() {
|
|||||||
|
|
||||||
async function entrar() {
|
async function entrar() {
|
||||||
if (!senha.value) {
|
if (!senha.value) {
|
||||||
erro.value = 'Informe a senha.'
|
erro.value = 'Informe a senha para continuar.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
erro.value = ''
|
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="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
|
||||||
<div class="w-full max-w-md">
|
<div class="w-full max-w-md">
|
||||||
|
|
||||||
<!-- Card de login -->
|
|
||||||
<div class="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
|
||||||
|
|
||||||
<!-- Cabeçalho azul -->
|
<!-- Cabeçalho — h1 para hierarquia de heading correta -->
|
||||||
<div class="bg-gradient-to-r from-blue-700 to-blue-800 px-8 py-6">
|
<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="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" />
|
<i class="pi pi-lock text-white text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold">Acesso seguro</p>
|
<h1 class="text-white font-bold text-base leading-tight">Acesso seguro</h1>
|
||||||
<p class="text-blue-200 text-xs">Portal do Contribuinte</p>
|
<p class="text-white/80 text-xs mt-0.5">Portal do Contribuinte</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
|
||||||
<div class="px-8 py-8 space-y-6">
|
<div class="px-8 py-8 space-y-6">
|
||||||
|
|
||||||
<!-- Documento identificado -->
|
<!-- Documento identificado -->
|
||||||
<div>
|
<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
|
Entrando como
|
||||||
</p>
|
</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 justify-between bg-slate-50 border border-slate-200 rounded-xl px-4 py-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
<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-blue-700 text-sm" />
|
<i class="pi pi-id-card text-primary text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-mono font-medium text-slate-800 text-sm">{{ docFormatado }}</p>
|
<p class="font-mono font-semibold text-slate-800 text-sm">{{ docFormatado }}</p>
|
||||||
<p class="text-xs text-slate-500">{{ tipoDoc }}</p>
|
<p class="text-xs text-slate-600">{{ tipoDoc }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Alvo de 44px via py-3 px-3 -->
|
||||||
<button
|
<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"
|
@click="trocarDocumento"
|
||||||
>
|
>
|
||||||
<i class="pi pi-pencil text-xs" />
|
<i class="pi pi-pencil text-xs" aria-hidden="true" />
|
||||||
Trocar
|
Trocar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -97,26 +99,36 @@ async function entrar() {
|
|||||||
|
|
||||||
<!-- Senha -->
|
<!-- Senha -->
|
||||||
<div>
|
<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
|
Senha
|
||||||
</label>
|
</label>
|
||||||
<Password
|
<Password
|
||||||
v-model="senha"
|
v-model="senha"
|
||||||
|
:input-id="senhaId"
|
||||||
:feedback="false"
|
:feedback="false"
|
||||||
toggle-mask
|
toggle-mask
|
||||||
placeholder="Digite sua senha"
|
placeholder="Digite sua senha"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
input-class="w-full"
|
input-class="w-full"
|
||||||
size="large"
|
size="large"
|
||||||
|
:invalid="!!erro"
|
||||||
|
:input-props="{ 'aria-describedby': erro ? erroId : undefined, 'aria-invalid': !!erro || undefined }"
|
||||||
@keyup.enter="entrar"
|
@keyup.enter="entrar"
|
||||||
/>
|
/>
|
||||||
<p v-if="erro" class="mt-1.5 text-xs text-red-600 flex items-center gap-1">
|
<!-- aria-live="assertive": anuncia o erro imediatamente para leitores de tela -->
|
||||||
<i class="pi pi-exclamation-circle" />
|
<p
|
||||||
{{ erro }}
|
: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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botão entrar -->
|
|
||||||
<Button
|
<Button
|
||||||
label="Entrar"
|
label="Entrar"
|
||||||
icon="pi pi-sign-in"
|
icon="pi pi-sign-in"
|
||||||
@ -126,35 +138,35 @@ async function entrar() {
|
|||||||
@click="entrar"
|
@click="entrar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Links secundários -->
|
<div class="text-center space-y-1">
|
||||||
<div class="text-center space-y-2">
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'primeiro-acesso' }"
|
: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
|
Esqueci minha senha
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'credenciamento' }"
|
: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>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Voltar -->
|
<!-- Voltar — alvo de 44px via py-3 -->
|
||||||
<div class="text-center mt-6">
|
<div class="text-center mt-4">
|
||||||
<button
|
<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' })"
|
@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
|
Voltar à página inicial
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user