gabrielb caf25236b5 feat: dark mode completo via widget de acessibilidade
- main.css: @variant dark com seletor .app-dark (alinhado com PrimeVue darkModeSelector)
- AccessibilityWidget: novo toggle "Modo escuro" no painel; preferências persistidas no localStorage (fonte, contraste, escuro)
- PublicLayout/PortalLayout: dark:bg-slate-950/900, dark:border, dark:text em todos os elementos
- AppHeader/AppFooter: dark variants em bg, border, textos e links
- ServiceCard: usa cores primary em vez de blue hardcoded; dark variants completos
- HomeView: dark nos avisos do carrossel (bg coloridos suavizados), card de login, seção de serviços autenticados e CTA
- LoginView: dark no card, campo documento, label senha, links e botão voltar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:42:35 -03:00

173 lines
7.5 KiB
Vue

<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
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, ''))
const docFormatado = computed(() => {
const d = docBruto.value
if (d.length <= 11) {
return d
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2')
}
return d
.replace(/(\d{2})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1/$2')
.replace(/(\d{4})(\d{1,2})$/, '$1-$2')
})
const tipoDoc = computed(() => docBruto.value.length > 11 ? 'CNPJ' : 'CPF')
function trocarDocumento() {
router.push({ name: 'home' })
}
async function entrar() {
if (!senha.value) {
erro.value = 'Informe a senha para continuar.'
return
}
erro.value = ''
carregando.value = true
// Integração com Keycloak PKCE — a ser implementado
setTimeout(() => {
carregando.value = false
erro.value = 'Integração com Keycloak ainda não configurada nesta fase.'
}, 800)
}
</script>
<template>
<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="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<!-- 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" aria-hidden="true">
<i class="pi pi-lock text-white text-lg" />
</div>
<div>
<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>
<div class="px-8 py-8 space-y-6">
<!-- Documento identificado -->
<div>
<p class="text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wide mb-2">
Entrando como
</p>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-xl px-4 py-3">
<div class="flex items-center gap-3">
<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-semibold text-slate-800 dark:text-slate-100 text-sm">{{ docFormatado }}</p>
<p class="text-xs text-slate-600 dark:text-slate-300">{{ tipoDoc }}</p>
</div>
</div>
<!-- Alvo de 44px via py-3 px-3 -->
<button
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" aria-hidden="true" />
Trocar
</button>
</div>
</div>
<!-- Senha -->
<div>
<label :for="senhaId" class="block text-sm font-semibold text-slate-700 dark:text-slate-200 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"
/>
<!-- 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>
<Button
label="Entrar"
icon="pi pi-sign-in"
class="w-full"
size="large"
:loading="carregando"
@click="entrar"
/>
<div class="text-center space-y-1">
<RouterLink
:to="{ name: 'primeiro-acesso' }"
class="block text-sm text-primary font-medium py-2 hover:underline"
>
Esqueci minha senha
</RouterLink>
<RouterLink
:to="{ name: 'credenciamento' }"
class="block text-sm text-slate-600 dark:text-slate-400 py-2 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
Ainda não tem acesso? <span class="text-primary font-semibold">Credenciar-se</span>
</RouterLink>
</div>
</div>
</div>
<!-- Voltar alvo de 44px via py-3 -->
<div class="text-center mt-4">
<button
class="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
@click="router.push({ name: 'home' })"
>
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
Voltar à página inicial
</button>
</div>
</div>
</div>
</template>