feat(web): foundation com brand JCS + AntD theme + Rafael painel placeholder

- Design tokens (CSS variables) espelhando brand.md v1.0:
  - Paleta JCS Blue #004a99 + estados funcionais
  - Plus Jakarta Sans Variable self-host (LGPD + perf)
  - Radius 12/20, sombra 0 4px 25px rgba(0,0,0,0.05)
  - Layout topbar 80 + sidebar 260 (brand.md canon)
- AntD ConfigProvider com tema JCS (cores, fonts, radius, shadow, motion)
- TanStack Router + Query setup com defaults conservadores
- AppShell desktop (Topbar + Sidebar) com tom canônico
- RafaelPainel placeholder com vocabulário canônico:
  meta de maio, clientes esfriando (OPENFRIOS 47 dias), próxima visita,
  comissão+FLEX, copy direta apple-inspired
- Logos copiadas para apps/web/public/
- Limpeza: removidos placeholders Nx (app.tsx, nx-welcome.tsx, styles.css)
- pt-BR locale (dayjs + AntD)
- Build OK: 878KB JS (vai code-splitar pós cockpits separados)

Refs: brand.md, design-artifacts/A-Product-Brief/03-visual-direction.md,
      design-artifacts/B-Trigger-Map/personas/02-rafael-representante.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 17:12:34 +00:00
parent 17c08e6392
commit 3a42723c71
23 changed files with 2037 additions and 893 deletions

View File

@@ -0,0 +1,27 @@
import { QueryClient } from '@tanstack/react-query';
/**
* QueryClient canônico do SAR.
* Defaults conservadores: refetch on focus desabilitado (Visual DNA: "sereno"),
* stale-while-revalidate para tempo real bater no Socket.IO, não em polling.
*/
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30s — Socket.IO atualiza antes na maioria dos casos
gcTime: 5 * 60_000, // 5min em cache antes de garbage collect
refetchOnWindowFocus: false, // evita "refresh fantasma" — Sandra/Daniel se distraem
refetchOnReconnect: true,
retry: (failureCount, error) => {
// 4xx não retry; 5xx até 2 retries
const status = (error as { status?: number })?.status;
if (status && status >= 400 && status < 500) return false;
return failureCount < 2;
},
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
},
mutations: {
retry: false, // mutations não retry automático — Idempotency-Key no servidor cuida disso
},
},
});

View File

@@ -0,0 +1,38 @@
import { createRouter, createRootRoute, createRoute, Outlet } from '@tanstack/react-router';
import { AppShell } from '../components/layout/AppShell';
import { RafaelPainel } from '../cockpits/rafael/RafaelPainel';
const rootRoute = createRootRoute({
component: () => (
<AppShell>
<Outlet />
</AppShell>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: RafaelPainel,
});
// Placeholder routes (cockpits a implementar)
const rafaelRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/rep',
component: RafaelPainel,
});
const routeTree = rootRoute.addChildren([indexRoute, rafaelRoute]);
export const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
});
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

140
apps/web/src/lib/theme.ts Normal file
View File

@@ -0,0 +1,140 @@
/**
* Tema canônico do AntD para o SAR.
* Espelha brand.md + design-artifacts/A-Product-Brief/03-visual-direction.md.
* NÃO alterar tokens primários (jcsBlue, font family) sem RFC.
*/
import type { ThemeConfig } from 'antd';
// Tokens canônicos sincronizados com tokens.css (manter espelhados manualmente
// ou centralizar quando build pipeline permitir).
export const brandTokens = {
jcsBlue: '#004a99',
jcsBlueHover: '#003a7a',
jcsBlueLight: '#e6eff8',
bgBody: '#f4f7fa',
textMain: '#2d3748',
textMuted: '#718096',
green: '#38a169',
orange: '#ed8936',
red: '#e53e3e',
whatsappGreen: '#25d366',
borderSubtle: '#e2e8f0',
} as const;
export const sarTheme: ThemeConfig = {
token: {
// === Cor primária ===
colorPrimary: brandTokens.jcsBlue,
colorPrimaryHover: brandTokens.jcsBlueHover,
colorPrimaryBg: brandTokens.jcsBlueLight,
colorPrimaryBgHover: brandTokens.jcsBlueLight,
// === Estados funcionais ===
colorSuccess: brandTokens.green,
colorWarning: brandTokens.orange,
colorError: brandTokens.red,
colorInfo: brandTokens.jcsBlue,
// === Background ===
colorBgBase: '#ffffff',
colorBgLayout: brandTokens.bgBody,
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
// === Texto ===
colorText: brandTokens.textMain,
colorTextSecondary: brandTokens.textMuted,
colorTextTertiary: brandTokens.textMuted,
colorTextQuaternary: brandTokens.textMuted,
// === Borda ===
colorBorder: brandTokens.borderSubtle,
colorBorderSecondary: brandTokens.borderSubtle,
// === Tipografia (Visual DNA: Plus Jakarta) ===
fontFamily:
"'Plus Jakarta Sans Variable', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
fontSize: 14, // baseline body
fontSizeSM: 13,
fontSizeLG: 16,
fontSizeXL: 18,
fontSizeHeading1: 32,
fontSizeHeading2: 24,
fontSizeHeading3: 20,
fontSizeHeading4: 18,
fontSizeHeading5: 16,
lineHeight: 1.5,
// === Radius (brand.md) ===
borderRadius: 12,
borderRadiusLG: 20,
borderRadiusSM: 6,
// === Box shadow (brand.md canon — sutil único) ===
boxShadow: '0 4px 25px rgba(0, 0, 0, 0.05)',
boxShadowSecondary: '0 4px 25px rgba(0, 0, 0, 0.05)',
boxShadowTertiary: '0 1px 2px rgba(0, 0, 0, 0.04)',
// === Motion (Visual DNA: sutis funcionais) ===
motionDurationFast: '0.15s',
motionDurationMid: '0.2s',
motionDurationSlow: '0.25s',
motionEaseInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
// === Wireframe off — usamos look próprio ===
wireframe: false,
},
components: {
Layout: {
headerHeight: 80, // brand.md
headerBg: '#ffffff',
bodyBg: brandTokens.bgBody,
siderBg: '#ffffff',
},
Menu: {
itemBorderRadius: 12,
itemHeight: 44,
itemMarginInline: 8,
itemSelectedBg: brandTokens.jcsBlueLight,
itemSelectedColor: brandTokens.jcsBlue,
itemHoverBg: brandTokens.jcsBlueLight,
itemHoverColor: brandTokens.jcsBlue,
},
Button: {
borderRadius: 12,
controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
fontWeight: 600,
primaryShadow: 'none',
defaultShadow: 'none',
},
Card: {
borderRadiusLG: 20,
paddingLG: 24,
headerBg: 'transparent',
},
Input: {
borderRadius: 12,
controlHeight: 40,
},
Select: {
borderRadius: 12,
controlHeight: 40,
},
Table: {
headerBg: brandTokens.bgBody,
headerSplitColor: 'transparent',
rowHoverBg: brandTokens.jcsBlueLight,
},
Tag: {
borderRadiusSM: 6,
},
Tooltip: {
borderRadius: 8,
},
Notification: {
borderRadiusLG: 12,
},
},
};