Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)

Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 09:41:13 -03:00
co-authored by Claude Sonnet 5
parent f9738e54da
commit 323912748f
179 changed files with 29312 additions and 22 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* Contraste WCAG AA (seção 6 do schema de arquitetura). Usado para
* garantir que a cor de marca de um tenant, quando usada como texto/ícone
* sobre fundo claro, continue legível — mesmo que o tenant tenha
* escolhido uma cor originalmente clara demais para isso.
*/
interface Rgb {
r: number;
g: number;
b: number;
}
function hexToRgb(hex: string): Rgb {
const normalized = hex.replace('#', '');
const full = normalized.length === 3 ? normalized.replace(/(.)/g, '$1$1') : normalized;
const value = parseInt(full, 16);
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
}
function rgbToHex({ r, g, b }: Rgb): string {
return `#${[r, g, b].map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, '0')).join('')}`;
}
function relativeLuminance({ r, g, b }: Rgb): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
const channel = c / 255;
return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
export function contrastRatio(hexA: string, hexB: string): number {
const lA = relativeLuminance(hexToRgb(hexA));
const lB = relativeLuminance(hexToRgb(hexB));
const lighter = Math.max(lA, lB);
const darker = Math.min(lA, lB);
return (lighter + 0.05) / (darker + 0.05);
}
const WCAG_AA_MIN_CONTRAST = 4.5;
/**
* Se `hex` já tem contraste AA (≥4.5:1) contra `background`, devolve ele
* mesmo. Senão, escurece progressivamente até passar (ou até chegar em
* preto). Não deixa a cor original para uso como fundo — só gera a
* variante seguro para texto/ícone.
*/
export function ensureAccessibleTextColor(hex: string, background = '#FFFFFF'): string {
if (contrastRatio(hex, background) >= WCAG_AA_MIN_CONTRAST) {
return hex;
}
const base = hexToRgb(hex);
for (let step = 1; step <= 20; step += 1) {
const factor = step / 20;
const darkened = rgbToHex({
r: Math.round(base.r * (1 - factor)),
g: Math.round(base.g * (1 - factor)),
b: Math.round(base.b * (1 - factor)),
});
if (contrastRatio(darkened, background) >= WCAG_AA_MIN_CONTRAST) {
return darkened;
}
}
return '#000000';
}