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
16 changes: 14 additions & 2 deletions frontend/docs/design-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,18 @@ Escala do design system:
| `data-lg` | 32 / 36 px | 400 | Valor em destaque no simulador, sempre com unidade |
| `data` | 20 / 24 px | 400 | Valor dentro de campo e unidade em sufixo |

Regras: **nunca texto abaixo de 14 px**; texto corrido de 16 px, ampliável com o zoom do navegador; campos de formulário com 16 px ou mais (o iOS dá zoom automático em campos menores); títulos com `letter-spacing: 0.01em`.
Regras: **nunca texto abaixo de 0,875 rem (~14 px)**; texto corrido de 1 rem (~16 px); campos de formulário com 1 rem ou mais (o iOS dá zoom automático em campos menores); títulos com `letter-spacing: 0.01em`.

Tamanhos de fonte no código usam **sempre** as variáveis `--fs-*` em `rem` (nunca px direto), para que o texto escale com a preferência de fonte do navegador — WCAG 1.4.4 (AA). Variáveis definidas em `src/styles/tokens.css`:

| Variável | Valor rem | Valor px de referência | Uso |
|---|---|---|---|
| `--fs-xs` | `0.75rem` | ~12 px | Kicker, metadados mínimos |
| `--fs-sm` | `0.875rem` | ~14 px | Dicas, legendas, rótulos menores |
| `--fs-base` | `1rem` | ~16 px | Texto corrido e campos |
| `--fs-lg` | `1.125rem` | ~18 px | Texto de destaque |
| `--fs-xl` | `1.25rem` | ~20 px | Subtítulos |
| `--fs-2xl` | `1.5rem` | ~24 px | Código de verificação nas caixas |

### 3.5 Espaço

Expand Down Expand Up @@ -258,7 +269,7 @@ Em `src/shared/ui`, com testes de acessibilidade em `tests/shared/ui/ui.test.tsx

| Componente | Props principais | Notas |
|---|---|---|
| `Botao` | `variante` (`primario` \| `secundario` \| `ghost`), `bloco`, `carregando` | `type="button"` por padrão; `carregando` desabilita e mostra estado |
| `Botao` | `variante` (`primario` \| `secundario` \| `ghost` \| `perigo`), `bloco`, `carregando` | `type="button"` por padrão; `carregando` desabilita e mostra estado; `perigo` reservado para ações dentro de `Dialogo` destrutivo |
| `BotaoLink` | `variante`, `bloco`, `to` | Link com cara de botão (react-router) |
| `Alerta` | `intencao` (`info` \| `atencao` \| `perigo` \| `sucesso`), `titulo`, `acao` | `role="alert"` só no perigo |
| `Campo` | `rotulo`, `dica`, `erro`, `complemento` | Liga rótulo, dica e erro por `aria-describedby`; `aria-invalid` no erro |
Expand All @@ -270,6 +281,7 @@ Em `src/shared/ui`, com testes de acessibilidade em `tests/shared/ui/ui.test.tsx
| `Marca` | `para` | Folha e nome, link para a página inicial |
| `FolhasDecorativas` | `className` | Folhas do painel verde, `aria-hidden` |
| `TelaCarregando` | — | `<output>` "Carregando…" |
| `Dialogo` | `gatilho`, `titulo`, `descricao?`, `acoes?`, `aberto?`, `aoFechar?` | **Radix UI Dialog**: captura foco, ESC fecha, `aria-labelledby`/`aria-describedby`, overlay inerte, animação com `prefers-reduced-motion` |

Padrões que nasceram nas features (em `src/features`):

Expand Down
106 changes: 106 additions & 0 deletions frontend/src/shared/ui/Dialogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Dialog } from 'radix-ui'
import type { ReactNode } from 'react'

/**
* Ícone "X" inline — decorativo, sem dependência extra de Icones.tsx.
* aria-hidden porque o botão já tem aria-label.
*/
function IconeFechar() {
return (
<svg
className="tf-icon"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
d="M6 6l12 12M6 18L18 6"
/>
</svg>
)
}

export type DialogoProps = {
/** Elemento que abre o diálogo (trigger). */
gatilho: ReactNode
/** Título visível do diálogo — anuncia o contexto ao leitor de tela. */
titulo: string
/** Descrição opcional abaixo do título. */
descricao?: string
/** Conteúdo principal do diálogo. */
children: ReactNode
/** Botões de ação no rodapé. */
acoes?: ReactNode
/** Estado controlado — se undefined, o componente gerencia o próprio estado. */
aberto?: boolean
/** Callback ao tentar fechar (ESC, clique na overlay ou botão X). */
aoFechar?: () => void
}

/**
* Diálogo acessível construído sobre o Radix UI Dialog.
*
* - Captura o foco ao abrir e o devolve ao gatilho ao fechar.
* - ESC fecha o diálogo.
* - Título em `<h2>` ligado ao painel via `aria-labelledby`.
* - Descrição opcional ligada via `aria-describedby`.
* - Overlay bloqueia interação com o fundo.
* - Animação respeita `prefers-reduced-motion` via `base.css`.
*
* Contraste do overlay: rgba(11,31,21,0.6) — suficiente para indicar estado modal
* sem depender só da cor (o foco é capturado e o restante da página fica inerte).
*/
export function Dialogo({
gatilho,
titulo,
descricao,
children,
acoes,
aberto,
aoFechar,
}: DialogoProps) {
const controlado = aberto !== undefined

return (
<Dialog.Root
open={controlado ? aberto : undefined}
onOpenChange={controlado ? (v) => { if (!v) aoFechar?.() } : undefined}
modal
>
<Dialog.Trigger asChild>{gatilho}</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Overlay className="tf-dialogo-overlay" />

<Dialog.Content className="tf-dialogo" aria-modal="true">
<div className="tf-dialogo__painel">
<header className="tf-dialogo__cabecalho">
<Dialog.Title className="tf-dialogo__titulo">{titulo}</Dialog.Title>
<Dialog.Close
className="tf-dialogo__fechar"
aria-label="Fechar diálogo"
onClick={aoFechar}
>
<IconeFechar />
</Dialog.Close>
</header>

{descricao && (
<Dialog.Description className="tf-dialogo__descricao">
{descricao}
</Dialog.Description>
)}

<div className="tf-dialogo__corpo">{children}</div>

{acoes && <div className="tf-dialogo__rodape">{acoes}</div>}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
7 changes: 5 additions & 2 deletions frontend/src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ body {
min-height: 100dvh;
background: var(--surface-100);
color: var(--ink);
font: 400 16px/24px var(--font-sans);
/* var(--fs-base) = 1rem: escala com a preferência de fonte do navegador (WCAG 1.4.4). */
font: 400 var(--fs-base) / 1.5 var(--font-sans);
}

h1,
h2,
h3,
p {
p,
dl,
dd {
margin: 0;
}

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,18 @@
--font-sans: 'Ubuntu', system-ui, 'Segoe UI', sans-serif;
--font-mono: 'Ubuntu Mono', ui-monospace, Menlo, Consolas, monospace;

/*
* Escala de tamanho de fonte em rem (base: 16 px padrão do navegador).
* Usar SEMPRE as variáveis --fs-*; nunca escreva rem ou px diretamente em componentes.
* Com zoom de 200 % o navegador dobra o rem e nenhum texto some nem causa rolagem horizontal.
* Critério WCAG 1.4.4 — Redimensionamento de texto (AA).
*/
--fs-xs: 0.75rem; /* ~12 px — mínimo absoluto; só kicker e metadados */
--fs-sm: 0.875rem; /* ~14 px — dicas, legendas, rótulos menores */
--fs-base: 1rem; /* ~16 px — texto corrido e campos de formulário */
--fs-lg: 1.125rem; /* ~18 px — texto de destaque, body-lg */
--fs-xl: 1.25rem; /* ~20 px — subtítulos */
--fs-2xl: 1.5rem; /* ~24 px — códigos de verificação em caixas */

color-scheme: light;
}
Loading
Loading