From 72d2ed4c3519d2ec8be21fcc5e893132850c74c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A9sar=20Rocha=20Monteiro?= Date: Wed, 26 Aug 2026 01:48:00 -0300 Subject: [PATCH] build(tailwind): migra para o Tailwind 4 com config em CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O `tailwind.config.ts` deixa de existir. O que era `theme.extend` virou um bloco `@theme inline` em `app/globals.css` — o modo `inline` é o que preserva o tema, porque o utilitário recebe o `var(--color-…)` literal em vez de uma cópia do valor claro. Os três blocos de token (`:root`, `[data-theme="light"]`, `[data-theme="dark"]`) ficam exatamente onde estavam: `extrairRegua` (lib/branding/contraste.ts) acha o bloco claro por igualdade EXATA de seletor, e `branding-tema-claro-escopavel` compara prop a prop os blocos claro e escuro. O que a migração destapou, e é o item de maior impacto visual: No v3, modificador de opacidade sobre cor declarada como `var(--…)` sem `` fazia o Tailwind NÃO EMITIR a regra — em silêncio. Todo token deste produto é `var(--color-*)`, então 62 classes distintas em 252 usos eram letra morta: `bg-destructive/10` não pintava fundo nenhum, `border-destructive/30` caía na cor neutra da regra global de borda, `text-muted-foreground/60` herdava a cor do pai. O v4 resolve opacidade por `color-mix()` e as 252 passaram a pintar. Medido contra um `tailwindcss@3.4.19` de descarte: alimentado com 7 classes, ele emitiu 4 — nenhuma das 3 com barra. Renomes mecânicos (58 `rounded` puro → `rounded-md`, 22 `outline-none` → `outline-hidden`, 4 `flex-shrink-0` → `shrink-0`). O `rounded` puro não podia ficar: o v3 remapeava o DEFAULT para `--radius-md` (8px) e o v4 tem um `rounded` embutido de 0.25rem que nem `@utility` sobrescreve — o raio encolheria pela metade em 33 arquivos, sem erro nenhum. Guardas novas em `tests/unit/tailwind-tokens.test.ts`, porque a migração troca um arquivo que o TypeScript conferia por um que ninguém confere: reprova config em JS de volta, `:root` embrulhado em `@layer` (que ligaria a auto-referência que o `@theme inline` emite), token referenciado que não existe, pasta de UI fora do `@source`, e a volta dos três utilitários renomeados. `branding-rampa` fatiava o CSS com `indexOf('[data-theme="dark"]')` e quebrou: o `@custom-variant` pôs essa string no topo do arquivo. Passa a recortar o `:root` por casamento de chaves. Provado na tela, em instalação fresca (baseline.sql + bootstrap-owner + Supabase local), medindo por `getComputedStyle` e nunca a olho — onboarding completo em claro e escuro, 0 erro de console. Sonda em `tests/sonda-tailwind-4.ts`, capturas e medições em `evidence/tailwind-4/`. NÃO verificado: o build da imagem Docker. Os binários musl de `@tailwindcss/oxide` e `lightningcss` estão no lockfile para linux x64 e arm64, mas docker.io está inalcançável nesta máquina — quem prova é o `imagens-ok` no PR. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 12 + .prettierrc | 3 +- AGENTS.md | 2 +- CLAUDE.md | 3 +- .../inbox/_components/AdminSidePanel.tsx | 2 +- .../(protected)/incidents/[id]/_client.tsx | 2 +- app/admin/(protected)/tenants/[id]/layout.tsx | 2 +- app/admin/(protected)/users/[id]/_client.tsx | 2 +- .../[id]/_components/FollowupFlowPicker.tsx | 4 +- .../agents/[id]/_components/FunisDoAgente.tsx | 2 +- .../[id]/_components/HandoffKeywordsInput.tsx | 8 +- .../[id]/_components/RunDetailDrawer.tsx | 2 +- .../ai/agents/[id]/_components/RunTrace.tsx | 6 +- .../ai/agents/[id]/_components/TestPanel.tsx | 2 +- .../ai/agents/[id]/_components/ToolPicker.tsx | 2 +- .../agents/[id]/_components/TriggerEditor.tsx | 4 +- app/app/ai/routers/[id]/_client.tsx | 6 +- app/app/ai/runs/_components/ExecucoesDeIa.tsx | 2 +- app/app/integrations/nuvemshop/page.tsx | 8 +- app/app/kanban/loading.tsx | 2 +- app/design/README.md | 9 +- app/globals.css | 225 ++++- components.json | 2 +- components/admin/ImpersonateButton.tsx | 2 +- components/admin/audit/AuditFiltersAdmin.tsx | 4 +- components/admin/dashboard/AlertItem.tsx | 2 +- .../admin/platform-admins/DBAOnlyNotice.tsx | 4 +- components/admin/tenants/HealthCard.tsx | 2 +- components/ai/SystemPromptEditor.tsx | 2 +- components/auth/MfaEnrollModal.tsx | 4 +- components/auth/RecoveryCodesPanel.tsx | 4 +- components/auth/TOTPInput.tsx | 2 +- components/connections/CanalOficialClient.tsx | 2 +- .../connections/CanalParceiroClient.tsx | 2 +- components/connections/PreviaDaDefinicao.tsx | 2 +- components/connections/TemplatesClient.tsx | 2 +- .../connections/TemplatesParceiroClient.tsx | 4 +- components/contacts/ImportContactsDialog.tsx | 10 +- components/inbox/CRMSidePanel.tsx | 2 +- components/inbox/Composer.tsx | 4 +- components/inbox/InboxLayout.tsx | 9 +- components/inbox/JanelaFechadaAviso.tsx | 2 +- components/inbox/MessageBubble.tsx | 6 +- components/inbox/ShortcutsHelpDialog.tsx | 2 +- components/kanban/ConversaSlot.tsx | 2 +- components/kanban/NextActionSlot.tsx | 4 +- components/kanban/ReactivationSlot.tsx | 4 +- components/kanban/ScoreSlot.tsx | 2 +- components/shell/CommandPalette.tsx | 2 +- components/shell/SearchTrigger.tsx | 2 +- components/ui/badge.tsx | 2 +- components/ui/button.tsx | 2 +- components/ui/dialog.tsx | 2 +- components/ui/dropdown-menu.tsx | 8 +- components/ui/input.tsx | 2 +- components/ui/popover.tsx | 2 +- components/ui/select.tsx | 4 +- components/ui/sheet.tsx | 2 +- components/ui/switch.tsx | 2 +- components/ui/tabs.tsx | 4 +- components/ui/textarea.tsx | 2 +- docs/design-system/01-foundation-tokens.md | 5 +- docs/design-system/03-typography.md | 2 +- .../screen-flow/07-responsive-strategy.md | 4 +- docs/testing/user-journey-map.md | 36 + evidence/tailwind-4/01-login-claro.png | Bin 0 -> 17723 bytes evidence/tailwind-4/01-login-escuro.png | Bin 0 -> 17658 bytes .../02-onboarding-welcome-claro.png | Bin 0 -> 96648 bytes .../02-onboarding-welcome-escuro.png | Bin 0 -> 95991 bytes .../tailwind-4/03-onboarding-funil-claro.png | Bin 0 -> 129952 bytes .../tailwind-4/03-onboarding-funil-escuro.png | Bin 0 -> 128663 bytes .../tailwind-4/04-onboarding-equipe-claro.png | Bin 0 -> 63640 bytes .../04-onboarding-equipe-escuro.png | Bin 0 -> 63355 bytes .../tailwind-4/05-onboarding-ia-claro.png | Bin 0 -> 174556 bytes .../tailwind-4/05-onboarding-ia-escuro.png | Bin 0 -> 171349 bytes .../06-onboarding-whatsapp-claro.png | Bin 0 -> 76744 bytes .../06-onboarding-whatsapp-escuro.png | Bin 0 -> 75691 bytes evidence/tailwind-4/07-conta-claro.png | Bin 0 -> 96648 bytes evidence/tailwind-4/07-conta-escuro.png | Bin 0 -> 95991 bytes evidence/tailwind-4/08-seguranca-claro.png | Bin 0 -> 96648 bytes evidence/tailwind-4/08-seguranca-escuro.png | Bin 0 -> 95991 bytes .../tailwind-4/09-legal-privacidade-claro.png | Bin 0 -> 211963 bytes .../09-legal-privacidade-escuro.png | Bin 0 -> 210843 bytes evidence/tailwind-4/medicoes.json | 430 ++++++++++ package.json | 4 +- pnpm-lock.yaml | 782 ++++++++---------- postcss.config.mjs | 6 +- tailwind.config.ts | 198 ----- tests/sonda-tailwind-4.ts | 175 ++++ tests/unit/branding-rampa.test.ts | 19 +- tests/unit/tailwind-tokens.test.ts | 206 +++++ 91 files changed, 1558 insertions(+), 742 deletions(-) create mode 100644 evidence/tailwind-4/01-login-claro.png create mode 100644 evidence/tailwind-4/01-login-escuro.png create mode 100644 evidence/tailwind-4/02-onboarding-welcome-claro.png create mode 100644 evidence/tailwind-4/02-onboarding-welcome-escuro.png create mode 100644 evidence/tailwind-4/03-onboarding-funil-claro.png create mode 100644 evidence/tailwind-4/03-onboarding-funil-escuro.png create mode 100644 evidence/tailwind-4/04-onboarding-equipe-claro.png create mode 100644 evidence/tailwind-4/04-onboarding-equipe-escuro.png create mode 100644 evidence/tailwind-4/05-onboarding-ia-claro.png create mode 100644 evidence/tailwind-4/05-onboarding-ia-escuro.png create mode 100644 evidence/tailwind-4/06-onboarding-whatsapp-claro.png create mode 100644 evidence/tailwind-4/06-onboarding-whatsapp-escuro.png create mode 100644 evidence/tailwind-4/07-conta-claro.png create mode 100644 evidence/tailwind-4/07-conta-escuro.png create mode 100644 evidence/tailwind-4/08-seguranca-claro.png create mode 100644 evidence/tailwind-4/08-seguranca-escuro.png create mode 100644 evidence/tailwind-4/09-legal-privacidade-claro.png create mode 100644 evidence/tailwind-4/09-legal-privacidade-escuro.png create mode 100644 evidence/tailwind-4/medicoes.json delete mode 100644 tailwind.config.ts create mode 100644 tests/sonda-tailwind-4.ts create mode 100644 tests/unit/tailwind-tokens.test.ts diff --git a/.gitignore b/.gitignore index e638584ec..cbeb8d275 100644 --- a/.gitignore +++ b/.gitignore @@ -99,7 +99,19 @@ loop/run-session.sh # graphify (análise local de knowledge graph) graphify-out/ + +# Lockfiles de OUTRO gerenciador. O `pnpm-lock.yaml` é o único versionado, e +# não é preferência de estilo: os pisos de segurança do projeto vivem em +# `pnpm.overrides` do package.json, que o npm e o bun NÃO leem. Um `bun install` +# aqui move `overrides` para o topo do package.json (formato bun/npm), o pnpm +# deixa de enxergá-lo, e o `pnpm-lock.yaml` perde o bloco `overrides` inteiro — +# `path-to-regexp@6.1.0` volta para a árvore com `pnpm audit` ainda verde. +# Aconteceu em 2026-08-26. Ignorar o lock não impede o estrago; impede que ele +# viaje no commit sem ninguém notar. package-lock.json +bun.lock +bun.lockb +yarn.lock .e2e-invite.json .e2e-owner.json .e2e-invite-url.json diff --git a/.prettierrc b/.prettierrc index e28b33552..66fc63d05 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,5 +7,6 @@ "useTabs": false, "arrowParens": "always", "endOfLine": "lf", - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./app/globals.css" } diff --git a/AGENTS.md b/AGENTS.md index 0e5a72d47..7ca48a535 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ quebra no clone fresco é um bug de produto, não um detalhe de ambiente. ## Stack (CONFIRMADO em `package.json`) -Next.js 16 (App Router) · React 19 · TypeScript 6 estrito · Tailwind 3 · +Next.js 16 (App Router) · React 19 · TypeScript 6 estrito · Tailwind 4 · shadcn/ui · Supabase (Postgres + Auth + Realtime + Storage) · Upstash Redis · Vercel AI Gateway (`@ai-sdk/anthropic|openai|google`) · WAHA Plus (engine NOWEB) · Zod 4 · Vitest 4 · Playwright 1 · Sentry 10. diff --git a/CLAUDE.md b/CLAUDE.md index 33226430e..83018c9bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ DeskcommCRM é um sistema operacional de vendas open source com agentes de IA na ## Stack canônica -- **Frontend:** Next.js 16 App Router (Turbopack) + React 19 + TypeScript 6 estrito + Tailwind + shadcn/ui (style: `new-york`, neutral) +- **Frontend:** Next.js 16 App Router (Turbopack) + React 19 + TypeScript 6 estrito + Tailwind 4 (config em CSS — ver abaixo) + shadcn/ui (style: `new-york`, neutral) - **Backend:** Next.js Route Handlers (mesmo repo); workers via `event_log` table + cron - **DB:** Supabase (Postgres). RLS em toda tabela tenant-aware. Extensions: `uuid-ossp`, `pgcrypto`, `vector` - **Auth:** Supabase Auth via `@supabase/ssr`. Cookie SameSite=Strict, HttpOnly, Secure @@ -181,6 +181,7 @@ DeskcommCRM é um sistema operacional de vendas open source com agentes de IA na | `docs/business-rules/` | Regras de negócio fora do código | | `docs/research/reference-synthesis.md` | Arquitetura herdada do curso WAHA | | `tasks/todo.md` | Workflow de construção atual | +| `app/globals.css` | **Tailwind 4 é CSS-first: não existe `tailwind.config.ts`.** Tokens em `:root` / `[data-theme]`, ponte token → utilitário no `@theme inline`, alcance do scanner nos `@source`. Vigiado por `tests/unit/tailwind-tokens.test.ts` | | `lib/api/wrappers.ts` | `ok()`, `fail()`, tipos `ApiSuccess` / `ApiError` | | `lib/api/errors.ts` | Códigos de erro canônicos | | `lib/env.ts` | Validação Zod das env vars (lança no startup se faltar crítica) | diff --git a/app/admin/(protected)/inbox/_components/AdminSidePanel.tsx b/app/admin/(protected)/inbox/_components/AdminSidePanel.tsx index 744fbb4c7..015c03067 100644 --- a/app/admin/(protected)/inbox/_components/AdminSidePanel.tsx +++ b/app/admin/(protected)/inbox/_components/AdminSidePanel.tsx @@ -44,7 +44,7 @@ export function AdminSidePanel({ data }: Props) {
{maskEmail(contact.email)}
)} {contact.is_blocked && ( - + Bloqueado )} diff --git a/app/admin/(protected)/incidents/[id]/_client.tsx b/app/admin/(protected)/incidents/[id]/_client.tsx index c2da9651e..1d9c67f98 100644 --- a/app/admin/(protected)/incidents/[id]/_client.tsx +++ b/app/admin/(protected)/incidents/[id]/_client.tsx @@ -164,7 +164,7 @@ export function IncidentDetailClient({ id }: IncidentDetailClientProps) { key={entry.id} className="flex items-start gap-2 text-xs" > - +
{entry.action} diff --git a/app/admin/(protected)/tenants/[id]/layout.tsx b/app/admin/(protected)/tenants/[id]/layout.tsx index b6fcecd66..854a10ddc 100644 --- a/app/admin/(protected)/tenants/[id]/layout.tsx +++ b/app/admin/(protected)/tenants/[id]/layout.tsx @@ -92,7 +92,7 @@ export default async function TenantDetailLayout({ {org?.display_name ?? id} {org?.slug && ( - + {org.slug} )} diff --git a/app/admin/(protected)/users/[id]/_client.tsx b/app/admin/(protected)/users/[id]/_client.tsx index 785dfa2bc..25b06ca00 100644 --- a/app/admin/(protected)/users/[id]/_client.tsx +++ b/app/admin/(protected)/users/[id]/_client.tsx @@ -246,7 +246,7 @@ export function UserDetailClient({ id }: UserDetailClientProps) {
{recent_audit.map((entry) => (
- +
diff --git a/app/app/ai/agents/[id]/_components/FollowupFlowPicker.tsx b/app/app/ai/agents/[id]/_components/FollowupFlowPicker.tsx index 15e200368..5067f32af 100644 --- a/app/app/ai/agents/[id]/_components/FollowupFlowPicker.tsx +++ b/app/app/ai/agents/[id]/_components/FollowupFlowPicker.tsx @@ -58,11 +58,11 @@ export function FollowupFlowPicker({ value, onChange, disabled }: Props) { return (