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>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
/**
|
|
* 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';
|
|
}
|