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:
27
apps/web/src/lib/query-client.ts
Normal file
27
apps/web/src/lib/query-client.ts
Normal 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
|
||||
},
|
||||
},
|
||||
});
|
||||
38
apps/web/src/lib/router.tsx
Normal file
38
apps/web/src/lib/router.tsx
Normal 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
140
apps/web/src/lib/theme.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user