Files
DeskcommCRM/components/ui/button.tsx
T
Rafael César Rocha MonteiroandClaude Opus 5 72d2ed4c35 build(tailwind): migra para o Tailwind 4 com config em CSS
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
  `<alpha-value>` 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) <noreply@anthropic.com>
2026-08-26 01:48:00 -03:00

89 lines
3.2 KiB
TypeScript

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
/**
* Button — Sage design system.
* Variants:
* - primary (default): accent fill, branded CTA
* - secondary: surface-elevated com border, ação neutra
* - ghost: transparent, hover suave (toolbar/inline)
* - destructive: error fill (delete/cancel destrutivo)
* - outline: alias de secondary com background transparente (compat shadcn)
* - link: text-only com underline
* - default: alias de primary (compat shadcn)
*/
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2 whitespace-nowrap",
"rounded-sm font-medium",
"transition-[background-color,border-color,color,box-shadow,transform]",
"duration-fast ease-out",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-2 focus-visible:ring-offset-bg",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"active:translate-y-px",
].join(" "),
{
variants: {
variant: {
primary:
"bg-accent text-accent-foreground hover:bg-accent-hover shadow-xs",
default:
"bg-accent text-accent-foreground hover:bg-accent-hover shadow-xs",
secondary:
"bg-surface-elevated text-text border border-border hover:border-accent hover:text-accent",
outline:
"bg-transparent text-text border border-border hover:border-accent hover:text-accent",
ghost:
"bg-transparent text-text hover:bg-accent-soft hover:text-accent",
destructive:
"bg-error text-white hover:brightness-95 shadow-xs",
link:
"bg-transparent text-accent underline underline-offset-4 decoration-1 hover:decoration-2 h-auto p-0",
},
// Alturas de toque: abaixo de `lg` (mesmo corte que o resto da casca
// usa pra decidir "é celular/tablet, é mouse") toda variante bate os
// 44px recomendados pra alvo de toque; de `lg:` pra cima, onde quem
// aciona é cursor, volta pro tamanho compacto original — mudar isso
// globalmente pro app inteiro em telas grandes infla a densidade sem
// necessidade nenhuma. `lg` já nascia com 44px e não precisou mudar.
size: {
sm: "h-11 px-3 text-xs lg:h-8",
default: "h-11 px-4 text-sm lg:h-9",
md: "h-11 px-4 text-sm lg:h-9",
lg: "h-11 px-6 text-sm",
icon: "h-11 w-11 lg:h-9 lg:w-9",
},
},
defaultVariants: {
variant: "primary",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };