Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ body.synapse-login-page {
.synapse-trust li span { color: #d15c17; font-size: 1rem; }

/* Ajustes no Card de Login baseados no Figma */
.synapse-card { width: 480px; max-width: 480px; height: 667px; align-self: center; justify-self: end; padding: 3rem 2.25rem 2rem; border: 1px solid rgb(220 193 183 / 50%); border-radius: 24px; color: #2e1a10; background: #fff; box-shadow: 0 30px 70px -15px rgb(30 20 17 / 12%), 0 0 0 1px rgb(220 193 183 / 35%); }
.synapse-card { width: 480px; max-width: 480px; height: 600px; align-self: center; justify-self: end; padding: 3rem 2.25rem 2rem; border: 1px solid rgb(220 193 183 / 50%); border-radius: 24px; color: #2e1a10; background: #fff; box-shadow: 0 30px 70px -15px rgb(30 20 17 / 12%), 0 0 0 1px rgb(220 193 183 / 35%); }
.synapse-card-icon { display: grid; width: 2.7rem; height: 2.7rem; place-items: center; border: 1px solid #fce8db; border-radius: .5rem; color: #c65a0d; background: #fff4ed; }
.synapse-card-icon svg { width: 1.2rem; height: 1.2rem; }
.synapse-card h2 { margin: .95rem 0 .35rem; font-family: Georgia, Cambria, serif; font-size: clamp(1.65rem, 2vw, 2rem); font-weight: 400; letter-spacing: -.02em; }
Expand Down
Binary file removed frontend/public/favicon.ico
Binary file not shown.
Binary file added frontend/public/synapse.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,18 @@ body,
min-width: 100%;
min-height: 100%;
margin: 0;
overflow-x: clip;
}

/*
* O monitor de referência tem 1920px CSS (120rem a 16px). Um notebook com
* escala de 125% normalmente disponibiliza entre 1200px e 1600px CSS; ao
* tornar o rem proporcional nessa faixa, a composição ganha a mesma presença
* visual do desktop de referência e continua fluida, sem canvas fixo ou
* rolagem horizontal.
*/
@media (min-resolution: 1.25dppx) and (min-width: 75rem) and (max-width: 100rem) {
html {
font-size: calc(100vw / 110);
}
}
60 changes: 60 additions & 0 deletions frontend/src/components/TheProcessHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import TheProcessHeader from './TheProcessHeader.vue'

const opcoesDeMontagem = {
global: {
plugins: [createPinia()],
},
}

describe('TheProcessHeader', () => {
it('marca a entrada de regra como etapa atual', () => {
const conteiner = mount(TheProcessHeader, { ...opcoesDeMontagem, props: { etapaDaRota: 'voz-texto' } })

expect(conteiner.get('li[aria-current="step"]').text()).toContain('Voz/Texto')
})

it('associa os estados intermediários à simulação enquanto a conversa não possui tela própria', () => {
const conteiner = mount(TheProcessHeader, {
...opcoesDeMontagem,
props: { etapaDaRota: 'simulacao', statusDoJob: 'aguardando_confirmacao_parametros' },
})

expect(conteiner.get('li[aria-current="step"]').text()).toContain('Simulação')
expect(conteiner.text()).not.toContain('Conversa')
})

it('indica o salvamento em toda a tela de finalização, mesmo durante uma transição de status', () => {
const conteiner = mount(TheProcessHeader, {
...opcoesDeMontagem,
props: { etapaDaRota: 'salvar', statusDoJob: 'simulacao_inviavel' },
})

expect(conteiner.get('li[aria-current="step"]').text()).toContain('Salvar')
})

it('não exibe o progresso de uma nova regra no histórico', () => {
const conteiner = mount(TheProcessHeader, { ...opcoesDeMontagem, props: { etapaDaRota: null } })

expect(conteiner.find('[aria-label="Etapas do processo"]').exists()).toBe(false)
expect(conteiner.find('[aria-label^="Etapa atual:"]').exists()).toBe(false)
})

it('mantém um indicador textual acessível fora do desktop', () => {
const conteiner = mount(TheProcessHeader, { ...opcoesDeMontagem, props: { etapaDaRota: 'simulacao' } })
const indicador = conteiner.get('[aria-label="Etapa atual: Simulação"]')

expect(indicador.text()).toContain('Etapa: Simulação')
expect(indicador.classes()).toContain('xl:hidden')
})

it('usa um ícone reconhecível para abrir o menu de perfil', () => {
const conteiner = mount(TheProcessHeader, { ...opcoesDeMontagem, props: { etapaDaRota: 'voz-texto' } })
const menuDePerfil = conteiner.get('summary[aria-label="Abrir menu de perfil"]')

expect(menuDePerfil.find('svg').attributes('viewBox')).toBe('0 0 24 24')
expect(menuDePerfil.text()).not.toContain('U')
})
})
109 changes: 84 additions & 25 deletions frontend/src/components/TheProcessHeader.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { StatusJob } from '@/types/api'
import { usarStoreSessao } from '@/stores/session'

type EtapaDoProcesso = 'voz-texto' | 'simulacao' | 'salvar'

const props = defineProps<{
etapaDaRota: EtapaDoProcesso | null
statusDoJob?: StatusJob | null
}>()

const sessao = usarStoreSessao()
const saindoDaConta = ref(false)

const rotulosDasEtapas: Record<EtapaDoProcesso, string> = {
'voz-texto': 'Voz/Texto',
simulacao: 'Simulação',
salvar: 'Salvar',
}

const etapasPorStatus: Partial<Record<StatusJob, EtapaDoProcesso>> = {
aguardando_transcricao: 'voz-texto',
aguardando_confirmacao_parametros: 'simulacao',
gerando_regra: 'simulacao',
simulando: 'simulacao',
simulacao_inviavel: 'simulacao',
aguardando_decisao_usuario: 'simulacao',
liberado: 'salvar',
}

const etapaAtual = computed<EtapaDoProcesso | null>(() => {
if (props.statusDoJob === undefined) return props.etapaDaRota

if (props.etapaDaRota === 'salvar') {
return 'salvar'
}

return props.statusDoJob ? (etapasPorStatus[props.statusDoJob] ?? null) : null
})

const rotuloDaEtapaAtual = computed(() => (etapaAtual.value ? rotulosDasEtapas[etapaAtual.value] : ''))

function etapaEstaAtiva(etapa: EtapaDoProcesso): boolean {
return etapaAtual.value === etapa
}

async function sairDaConta(): Promise<void> {
saindoDaConta.value = true
try {
Expand All @@ -16,48 +56,67 @@ async function sairDaConta(): Promise<void> {
</script>

<template>
<header class="flex h-[clamp(2.5rem,7vh,4rem)] items-center gap-6 border-b border-[#ebd8cc] bg-[#fffaf7] px-6 lg:px-8">
<div class="flex shrink-0 gap-2" aria-label="Indicadores da janela">
<span class="size-3 rounded-full bg-[#ff625c]"></span>
<span class="size-3 rounded-full bg-[#ffbd2e]"></span>
<span class="size-3 rounded-full bg-[#27c840]"></span>
</div>
<header class="flex h-14 items-center gap-3 border-b border-[#ebd8cc] bg-[#fffaf7] px-4 sm:gap-6 sm:px-6 lg:h-16 lg:px-8">

<ol class="hidden min-w-0 flex-1 items-center justify-center gap-3 text-sm xl:flex" aria-label="Etapas do processo">
<li class="flex shrink-0 items-center gap-2.5 font-medium text-[#c2560b]">
<span class="grid size-[34px] place-items-center rounded-full border-[1.5px] border-[#c2560b]" aria-hidden="true">
<ol v-if="etapaAtual" class="hidden min-w-0 flex-1 items-center justify-center gap-3 text-sm xl:flex" aria-label="Etapas do processo">
<li
class="flex shrink-0 items-center gap-2.5"
:class="etapaEstaAtiva('voz-texto') ? 'font-medium text-[#c2560b]' : 'text-[#a79489]'"
:aria-current="etapaEstaAtiva('voz-texto') ? 'step' : undefined"
>
<span
class="grid size-[2.125rem] place-items-center rounded-full"
:class="etapaEstaAtiva('voz-texto') ? 'border-[0.09375rem] border-[#c2560b]' : 'border-[0.0625rem] border-[#e5d3c8]'"
aria-hidden="true"
>
<svg class="size-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="7.25" y="2.75" width="5.5" height="9.5" rx="2.75" /><path d="M4.75 9.5a5.25 5.25 0 0 0 10.5 0M10 14.75v2.5" /></svg>
</span>
Voz/Texto
</li>
<li class="h-px min-w-6 flex-1 bg-[#e5d3c8]" aria-hidden="true"></li>
<li class="flex shrink-0 items-center gap-2.5 text-[#a79489]">
<span class="grid size-[34px] place-items-center rounded-full border border-[#e5d3c8]" aria-hidden="true">
<svg class="size-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M3.25 6.25a2.5 2.5 0 0 1 2.5-2.5h8.5a2.5 2.5 0 0 1 2.5 2.5v5a2.5 2.5 0 0 1-2.5 2.5H8l-4.75 3.25z" /></svg>
</span>
Conversa
</li>
<li class="h-px min-w-6 flex-1 bg-[#e5d3c8]" aria-hidden="true"></li>
<li class="flex shrink-0 items-center gap-2.5 text-[#a79489]">
<span class="grid size-[34px] place-items-center rounded-full border border-[#e5d3c8]" aria-hidden="true">
<li
class="flex shrink-0 items-center gap-2.5"
:class="etapaEstaAtiva('simulacao') ? 'font-medium text-[#c2560b]' : 'text-[#a79489]'"
:aria-current="etapaEstaAtiva('simulacao') ? 'step' : undefined"
>
<span
class="grid size-[2.125rem] place-items-center rounded-full"
:class="etapaEstaAtiva('simulacao') ? 'border-[0.09375rem] border-[#c2560b]' : 'border-[0.0625rem] border-[#e5d3c8]'"
aria-hidden="true"
>
<svg class="size-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><circle cx="10" cy="10" r="7.25" /><path d="M8.25 7.1 13 10l-4.75 2.9z" /></svg>
</span>
Simulação
</li>
<li class="h-px min-w-6 flex-1 bg-[#e5d3c8]" aria-hidden="true"></li>
<li class="flex shrink-0 items-center gap-2.5 text-[#a79489]">
<span class="grid size-[34px] place-items-center rounded-full border border-[#e5d3c8]" aria-hidden="true">
<li
class="flex shrink-0 items-center gap-2.5"
:class="etapaEstaAtiva('salvar') ? 'font-medium text-[#c2560b]' : 'text-[#a79489]'"
:aria-current="etapaEstaAtiva('salvar') ? 'step' : undefined"
>
<span
class="grid size-[2.125rem] place-items-center rounded-full"
:class="etapaEstaAtiva('salvar') ? 'border-[0.09375rem] border-[#c2560b]' : 'border-[0.0625rem] border-[#e5d3c8]'"
aria-hidden="true"
>
<svg class="size-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M3.75 5.25a1.5 1.5 0 0 1 1.5-1.5h7.5l3 3v8a1.5 1.5 0 0 1-1.5 1.5h-9a1.5 1.5 0 0 1-1.5-1.5z" /><path d="M6.75 3.75v4h6.5M6.75 16.25v-4h6.5v4" /></svg>
</span>
Salvar
</li>
</ol>

<p
v-if="etapaAtual"
class="min-w-0 flex-1 truncate text-center text-xs font-medium text-[#6b564a] sm:text-sm xl:hidden"
:aria-label="`Etapa atual: ${rotuloDaEtapaAtual}`"
aria-live="polite"
>
Etapa: {{ rotuloDaEtapaAtual }}
</p>

<details class="relative ml-auto shrink-0">
<summary class="flex cursor-pointer list-none items-center gap-2 rounded-full border border-[#ead4c7] bg-white/80 py-1 pr-2 pl-1.5 text-sm text-[#5e473b] transition hover:bg-white [&::-webkit-details-marker]:hidden">
<span class="grid size-7 place-items-center rounded-full bg-[#6d4030] text-xs font-semibold text-white" aria-hidden="true">U</span>
<span class="hidden lg:block">Perfil</span>
<svg class="size-3 text-[#9c8174]" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M2 4.25h8L6 9z" /></svg>
<summary aria-label="Abrir menu de perfil" class="grid size-8 cursor-pointer list-none place-items-center rounded-full bg-[#6d4030] text-white transition hover:bg-[#512d20] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c2560b] [&::-webkit-details-marker]:hidden">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="8" r="3.25" /><path d="M5.75 20c.55-3.27 2.94-5.25 6.25-5.25s5.7 1.98 6.25 5.25" /></svg>
</summary>
<div class="absolute top-[calc(100%+.5rem)] right-0 z-10 w-52 rounded-lg border border-[#ead4c7] bg-white p-3 shadow-[0_12px_30px_-12px_rgba(60,30,15,0.35)]">
<p class="text-sm font-semibold text-[#2e1a10]">Perfil do usuário</p>
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/TheSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ withDefaults(
</script>

<template>
<aside class="flex min-h-full flex-col bg-[#3b2318] px-5 py-[clamp(.75rem,2.5vh,1.25rem)] text-[#f6eae3]">
<aside class="flex h-full min-h-0 flex-col overflow-y-auto bg-[#3b2318] p-5 text-[#f6eae3]">
<div class="flex items-center gap-3">
<img :src="simboloSynapse" alt="" class="size-9 object-contain" aria-hidden="true" />
<span class="font-serif text-xl font-bold tracking-tight">Synapse</span>
</div>

<RouterLink
to="/nova-regra"
class="mt-[clamp(.75rem,2vh,1.5rem)] flex h-[clamp(2.25rem,5vh,2.75rem)] items-center justify-center gap-2 rounded-[10px] bg-[#f26b0f] text-sm font-medium text-white transition hover:bg-[#d95c08]"
class="mt-6 flex h-11 items-center justify-center gap-2 rounded-[.625rem] bg-[#f26b0f] text-sm font-medium text-white transition hover:bg-[#d95c08]"
>
<svg class="size-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<path d="M8 3.5v9M3.5 8h9" />
Expand All @@ -41,11 +41,11 @@ withDefaults(

<section class="mt-4" aria-labelledby="titulo-recentes">
<h2 id="titulo-recentes" class="text-sm font-normal text-[#c7afa3]">Recentes</h2>
<ul class="mt-3 space-y-[clamp(.35rem,1.1vh,.625rem)]">
<ul class="mt-3 space-y-2">
<li v-for="regra in regrasRecentes" :key="regra.identificador">
<RouterLink
:to="`/jobs/${regra.identificador}`"
class="flex h-[clamp(2rem,4.5vh,2.5rem)] items-center rounded-[10px] bg-[#4d3125] px-4 text-sm text-[#eaddd5] transition hover:bg-[#5a3b2c]"
class="flex h-10 items-center rounded-[.625rem] bg-[#4d3125] px-4 text-sm text-[#eaddd5] transition hover:bg-[#5a3b2c]"
>
{{ regra.rotulo }}
</RouterLink>
Expand All @@ -58,15 +58,15 @@ withDefaults(

<nav class="mt-auto border-t border-[#5a3e31] pt-3" aria-label="Organização de regras">
<RouterLink to="/salvas" class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-sm text-[#d6bdb0] transition hover:bg-[#4d3125]" :class="{ 'bg-[#4d3125] text-white': secaoAtiva === 'salvas' }">
<svg class="size-[18px] shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<svg class="size-[1.125rem] shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="2.75" y="3.75" width="14.5" height="12.5" rx="2.25" />
<path d="M2.75 8h14.5M7.5 8v8.25" />
</svg>
<span class="flex-1">Salvas</span>
<span class="rounded-full bg-[#4d3125] px-2 py-0.5 text-xs text-[#d6bdb0]">{{ quantidadeSalvas }}</span>
</RouterLink>
<RouterLink to="/arquivadas" class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-sm text-[#d6bdb0] transition hover:bg-[#4d3125]" :class="{ 'bg-[#4d3125] text-white': secaoAtiva === 'arquivadas' }">
<svg class="size-[18px] shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<svg class="size-[1.125rem] shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="2.75" y="3.75" width="14.5" height="12.5" rx="2.25" />
<path d="M2.75 8h14.5M7.5 8v8.25" />
</svg>
Expand Down
26 changes: 14 additions & 12 deletions frontend/src/features/finish/views/FinishView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,20 +136,21 @@ onUnmounted(() => {
</script>

<template>
<div v-if="alerta" class="fixed top-5 z-50 px-6 py-3 rounded-lg border-2 shadow-md font-bold" :class="alerta.tipo === 'sucesso' ? 'bg-green-50 border-green-300 text-green-700' : 'bg-red-50 border-red-300 text-red-700'">
<div v-if="alerta" class="fixed top-5 left-1/2 z-50 -translate-x-1/2 rounded-lg border-2 px-6 py-3 font-bold shadow-md" :class="alerta.tipo === 'sucesso' ? 'bg-green-50 border-green-300 text-green-700' : 'bg-red-50 border-red-300 text-red-700'">
{{ alerta.mensagem }}
</div>
<div class="flex flex-row min-h-screen">
<TheSidebar class="hidden md:flex w-[15.3%]"/>
<div class="flex flex-col bg-[#fdf7f3] w-[84.7%] min-h-screen">
<TheProcessHeader class="mb-12" />
<div class="flex flex-col w-full justify-center items-center font-['Tinos']">
<div class="tela-de-negocio min-h-[100dvh] bg-[#fdf7f3] text-[#2e1a10] lg:grid lg:h-[100dvh] lg:grid-cols-[16rem_minmax(0,1fr)] lg:overflow-hidden">
<TheSidebar class="hidden lg:flex"/>
<main class="min-w-0 lg:min-h-0 lg:overflow-y-auto">
<div class="flex min-w-0 flex-col bg-[#fdf7f3]">
<TheProcessHeader etapa-da-rota="salvar" :status-do-job="job?.status ?? null" class="mb-12" />
<div class="flex w-full flex-col items-center justify-center px-4 font-['Tinos'] sm:px-6">
<div class="mb-7">
<h1 class="text-4xl font-bold text-center mb-3">Finalizar Regra</h1>
<h1 class="mb-3 text-center text-3xl font-bold sm:text-4xl">Finalizar Regra</h1>
<p class="text-center text-[#584237]">Revise os detalhes da regra extraída e confirme o salvamento.</p>
</div>
<div class="bg-white w-[70%] border-2 border-[#DFC0B2] rounded-xl flex flex-col mb-12">
<div class=" pt-8 px-8 pb-4">
<div class="mb-12 flex w-full max-w-4xl flex-col rounded-xl border-2 border-[#DFC0B2] bg-white">
<div class="px-4 pt-8 pb-4 sm:px-8">
<div>
<h2 class="font-bold text-2xl mb-2">{{ carregando ? 'Carregando regra...' : tituloRegra }}</h2>
<div class="flex flex-row gap-2">
Expand All @@ -166,7 +167,7 @@ onUnmounted(() => {
</div>
</div>
<hr class="self-center mb-4 border border-[#FFDBCD] w-[94%]">
<div class="mx-8 mb-8 p-6 bg-[#FFF1EC] border border-[#FFDBCD] rounded-lg text-[#584237]">
<div class="mx-4 mb-8 rounded-lg border border-[#FFDBCD] bg-[#FFF1EC] p-4 text-[#584237] sm:mx-8 sm:p-6">
<div v-if="carregando" class="flex flex-col items-center justify-center py-8 gap-3">
<div class="w-7 h-7 border-4 border-[#FFDBCD] border-t-[#F47521] rounded-full animate-spin"></div>
<p class="text-[#584237]">Carregando dados da regra...</p>
Expand Down Expand Up @@ -202,12 +203,13 @@ onUnmounted(() => {
</div>
</div>
</div>
<div class="flex flex-row items-center gap-4 mb-14">
<div class="mb-14 flex flex-col items-stretch gap-4 sm:flex-row sm:items-center">
<button type="button" class="text-[#9D4400] px-9 py-3 rounded-lg border-2 border-[#9D4400] font-bold cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" :disabled="acaoProcessando !== null || carregando || erroCarregamento || !podeArquivar" @click="executarAcao('arquivar')">{{ acaoProcessando === 'arquivar' ? 'Processando...' : 'Arquivar' }}</button>
<button type="button" class="bg-[#F47521] text-white font-bold px-9 py-3 rounded-lg cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" :disabled="acaoProcessando !== null || carregando || erroCarregamento || !podeSalvar" @click="executarAcao('salvar')">{{ acaoProcessando === 'salvar' ? 'Processando...' : 'Salvar' }}</button>
</div>
</div>
</div>
</main>
</div>
</template>

Expand All @@ -220,4 +222,4 @@ textarea,
select {
font-family: inherit;
}
</style>
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const emitir = defineEmits<{ 'update:valor': [valor: string] }>()
:value="valor"
:aria-invalid="Boolean(erro)"
:aria-describedby="erro ? `${id}-erro` : undefined"
class="h-[clamp(1.75rem,4vh,2.125rem)] w-full appearance-none rounded-[7px] border bg-[#faf4ef] pr-8 pl-3 text-sm text-[#2e1a10] outline-none transition focus:border-[#c2560b] focus:ring-2 focus:ring-[#f4dcc9]"
class="h-[2.125rem] w-full appearance-none rounded-[.4375rem] border bg-[#faf4ef] pr-8 pl-3 text-sm text-[#2e1a10] outline-none transition focus:border-[#c2560b] focus:ring-2 focus:ring-[#f4dcc9]"
:class="[erro ? 'border-[#c0392b]' : 'border-[#e3d3c9]', compacto ? '' : 'mt-2 h-11 bg-white']"
@change="emitir('update:valor', ($event.target as HTMLSelectElement).value)"
>
Expand Down
Loading
Loading