mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
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>
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
"use client";
|
|
import * as React from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Label } from "@/components/ui/label";
|
|
import { SYSTEM_PROMPT_PLACEHOLDERS } from "@/lib/ai/guardrails-schema";
|
|
|
|
interface Props {
|
|
value: string;
|
|
onChange: (next: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function SystemPromptEditor({ value, onChange, disabled }: Props) {
|
|
const ref = React.useRef<HTMLTextAreaElement | null>(null);
|
|
|
|
function insertPlaceholder(token: string) {
|
|
const el = ref.current;
|
|
if (!el) {
|
|
onChange(value + token);
|
|
return;
|
|
}
|
|
const start = el.selectionStart ?? value.length;
|
|
const end = el.selectionEnd ?? value.length;
|
|
const next = value.slice(0, start) + token + value.slice(end);
|
|
onChange(next);
|
|
// Restaura cursor após inserção (tick depois)
|
|
requestAnimationFrame(() => {
|
|
el.focus();
|
|
const pos = start + token.length;
|
|
el.setSelectionRange(pos, pos);
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_280px]">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="system_prompt">System prompt</Label>
|
|
<Textarea
|
|
id="system_prompt"
|
|
ref={ref}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
rows={18}
|
|
className="font-mono text-sm"
|
|
placeholder="Você é um assistente da loja. Responda com clareza e cordialidade…"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Mínimo 20 caracteres, máximo 10.000. Use placeholders para injetar contexto dinâmico.
|
|
</p>
|
|
</div>
|
|
|
|
<aside className="space-y-2 rounded-md border bg-muted/30 p-3">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Placeholders
|
|
</p>
|
|
<ul className="space-y-1.5">
|
|
{SYSTEM_PROMPT_PLACEHOLDERS.map((p) => (
|
|
<li key={p.token} className="flex flex-col gap-1">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<code className="rounded-md bg-background px-1.5 py-0.5 text-[11px]">{p.token}</code>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 px-2 text-xs"
|
|
onClick={() => insertPlaceholder(p.token)}
|
|
disabled={disabled}
|
|
>
|
|
Inserir
|
|
</Button>
|
|
</div>
|
|
<span className="text-[11px] text-muted-foreground">{p.description}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</aside>
|
|
</div>
|
|
);
|
|
}
|