# Changelog do @softize/opus

O que muda em cada versão — e, quando quebra, **o que fazer**. Regras de leitura:
minor = novidade compatível; major = breaking (a seção **Breaking** diz a migração).
Este arquivo viaja no pacote: num projeto, leia `node_modules/@softize/opus/CHANGELOG.md`.
Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
`manifest:check`) — eles apontam o que a mudança cobra do seu código.

## 11.1.0 — 2026-08-21

O Opus passa a distribuir a skill `write-product-communication` para orientar documentação,
interface, mensagens de erro, CLI, onboarding e textos explicativos a partir da situação do
leitor. A página antes dedicada apenas a microcopy agora reúne a referência canônica de
comunicação, incluindo escrita normativa e tratamento de erros.

O `opus setup` expande o conteúdo dessa referência ao materializar a skill em `.agents` e
`.claude`. As instruções permanentes e as skills de UI, actions, mudanças e upgrades passam a
acionar a orientação nos fluxos em que produzem texto. Testes de criação e setup confirmam que
projetos novos recebem o conteúdo completo, sem depender de um link externo.

A home, as páginas iniciais e a mensagem de sucesso do `opus check` começam a migração da voz
existente: apresentam contexto e efeito antes das regras, evitam slogans e informam a próxima
etapa em linguagem natural.

> As entradas 2.30.1–2.31.5 foram **reconstruídas do git** (o release publicava sem
> passar por aqui). Uma delas — a 2.31.5, que mudou o rem base — é visual GLOBAL e
> tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
> pra cobrir.

## 11.0.0 — 2026-08-21

**Breaking — deprecateds removidos na fronteira pública.** O major anterior manteve por
engano APIs cuja própria documentação prometia remoção na próxima versão; esta versão
fecha a migração em vez de carregar duas taxonomias indefinidamente.

- `AppShell`, `AppShellBar`, `AppShellTrigger`, `useAppShell`, `useSidebarSlot` e
  `SectionShell` (incluindo seus tipos `Section*`) saem. Componha chrome e seções com
  `Split`, `Pane`, `Sidebar`, `PaneHeader`, `PaneContent`, `PaneFooter` e `SidebarNav`.
- `ResizablePanelGroup`, `ResizablePanel` e `ResizableHandle` deixam de ser exports
  públicos. Use `<Split resizable>` e declare tamanho/limites nos `<Pane>`.
- `SidebarHeader`, `SidebarContent` e `SidebarFooter` saem; eram aliases de
  `PaneHeader`, `PaneContent` e `PaneFooter`.
- `DomainConfig.models` sai. Declare entidades em `DomainConfig.entities`; os comandos
  `opus db` e o gerador agora leem somente esse campo. O manifest deixa de emitir o array
  redundante `models`; os nomes continuam disponíveis em `entities[].name`.

`SidebarNav` passa a aceitar `subgroups`, cobrindo navegação contextual e a árvore de três
níveis da documentação sem um shell especializado. `ShellNav` agora deriva o estado
recolhido diretamente da `Sidebar`. O próprio `DocBrowser` foi migrado para a composição
canônica e serve como consumidor de referência.

**Migração:** substitua shells prontos pela composição `Split > Pane > Sidebar`; troque os
aliases `Sidebar*` pelos slots `Pane*`; envolva painéis ajustáveis em `<Split resizable>`;
renomeie `domain.models` para `domain.entities` e, ao consumir manifest, derive nomes de
`domain.entities.map(entity => entity.name)`.

## 10.0.0 — 2026-08-20

**Breaking — forma usa a escala do Tailwind e superfícies sempre carregam seu foreground.**

- `Card` volta a ser flat por padrão, alinhando a implementação ao contrato já documentado
  no componente e no catálogo. Consumidores que precisam de elevação devem declarar a
  intensidade explicitamente (`shadow-sm`, `shadow-md`, etc.).
- Forma deixa de criar uma taxonomia paralela por componente: `rounded-card` e
  `rounded-dialog` migram para `rounded-xl`; `rounded-popover`, para `rounded-md`. Os tokens
  `--radius-card/popover/dialog` saem, e `opus check` acusa classes e overrides antigos.
- Superfícies `card` e `popover` passam a exigir seu foreground no mesmo fragmento de
  classes. Dialog, Composer, Chat e o fundo nativo do Calendar foram corrigidos; `opus check`
  agora reprova `bg-card` sem `text-card-foreground` e `bg-popover` sem
  `text-popover-foreground`, evitando que a igualdade acidental com o foreground global
  esconda temas quebrados.

**Migração:** troque `rounded-card` e `rounded-dialog` por `rounded-xl`, e
`rounded-popover` por `rounded-md`. Remova overrides de `--radius-card`,
`--radius-dialog` e `--radius-popover`; ajuste `--radius` ou a escala `--radius-*` quando
necessário. Sempre declare `bg-card text-card-foreground` e
`bg-popover text-popover-foreground` juntos. `opus check` aponta todos esses casos.

## 9.1.1 — 2026-08-20

**`Ask` leva elicitação estruturada para `@softize/opus/ui/react`.** O componente
controlado apresenta de uma a quatro `AskQuestion`, deriva `AskAnswer[]` sem tipos
paralelos e suporta seleção única por radiogroup, seleção múltipla por pills e texto livre
sempre disponível. Descrições usam Tooltip; teclado, validação, estados `disabled`/`busy`
e submit acessível ficam no componente. Transporte, SSE, persistência e integração com
`ChatEvent` continuam sob responsabilidade do consumidor.

## 9.1.0 — 2026-08-19

**Observabilidade ganha uma porta vendor-neutral no runtime.** `ObservabilityAdapter`
executa actions e reactions dentro de um span ativo sem acoplar o core a OpenTelemetry.
`TraceContext` passa de forma opcional por contextos, resultados, `AuditRecord.traceContext`,
eventos e envelopes de job fornecidos pelo chamador;
falhas do adapter são lenient e nunca repetem nem substituem o resultado da action.

`AuditRecord.trace` e as colunas PostgreSQL legadas permanecem inalterados. O carrier novo
não inclui baggage arbitrário. Os drivers Fastify e Node oferecem propagação HTTP W3C opt-in
com `traceContext: 'w3c'`: `traceparent` v00 entra como parent e o contexto efetivo é emitido
na resposta; o default legado não lê nem escreve esses headers. Causalidade runtime→job
não depende mais de montagem manual do envelope: actions `background` são enfileiradas por
`execute()`, e o worker usa `executeJob()` para executar um span filho sem reenfileirar.
O carrier desserializado é revalidado por allowlist; o actor reidratado deve coincidir com o
envelope. BullMQ mapeia attempts, backoff nativo e prioridade sem simular opções incompatíveis.

`pgAudit({ traceContextColumns: true })` persiste opt-in em `trace_id`/`span_id`; sem a
opção, o INSERT legado continua com 17 colunas e não exige migration. Nomes customizados
permitem rollout gradual. Operações observáveis declaram `resultKind`: drivers devem
inspecionar `ActionResult.ok` para actions, pois erro de negócio resolve com `ok:false`.

**Driver OpenTelemetry opt-in.** O subpath
`@softize/opus/observability/opentelemetry` implementa a porta usando apenas a API OTel e
aceita `Tracer`, health e shutdown injetados. Provider, context manager, sampling, resource,
OTLP exporter e Collector continuam sob controle do app; nenhum backend acompanha o SDK.

## 9.0.9 — 2026-08-19

**O composer do `Chat` passa a navegar pelas mensagens já enviadas.** Com o campo vazio,
`↑` recupera a mensagem mais recente e continua pelas anteriores; `↓` avança e, depois da
mais nova, devolve o rascunho. Enquanto há texto sendo editado, as setas preservam a
navegação normal da textarea. O `Composer` expõe callbacks opcionais para shells que
queiram fornecer outro histórico, sem mudar o comportamento padrão do componente isolado.

## 9.0.8 — 2026-08-18

**Scrollbars nativas passam a usar o acabamento discreto como parte do tema base.** Todo
documento que importa `@softize/opus/ui/theme.css` recebe o indicador fino, revelado no
hover ou foco, inclusive em conteúdo renderizado por portal. A classe
`scrollbar-subtle` deixa de ser necessária; iframes continuam isolados e precisam importar
o tema no próprio documento.

## 9.0.7 — 2026-08-18

**Scrollbars nativas podem compartilhar um acabamento discreto e previsível.** A classe
`scrollbar-subtle` veste um scroller ou todo um shell: o indicador fino aparece ao passar
o ponteiro ou navegar com teclado pela região, sem depender da preferência global de
barras do macOS e sem adicionar listeners ao caminho de scroll. O `Chat` já adota o novo
acabamento.

## 9.0.6 — 2026-08-18

**As falas do usuário no `Chat` voltam ao fluxo normal da conversa.** Elas usam fundo
discreto, abraçam o conteúdo até 88% da largura e deixam de ter borda, sombra, pin no topo
e truncamento. O scroll não precisa mais medir qual turno está pinado.

## 9.0.5 — 2026-08-18

**Cards de usuário no `Chat` ficam planos enquanto percorrem o histórico.** A sombra passa
a indicar somente o turno efetivamente pinado no topo durante o scroll, em vez de elevar
todas as mensagens que apenas têm comportamento `sticky` disponível.

## 9.0.4 — 2026-08-18

**Digitar no `Chat` deixa de reprocessar o histórico inteiro.** O transcript agora é uma
fronteira memoizada separada do estado do composer, e cada bloco Markdown reaproveita o
resultado enquanto seu conteúdo não muda. Conversas longas mantêm o custo de digitação
constante; durante streaming, somente a mensagem alterada volta a passar pelo parser.
## 9.0.3 — 2026-08-12

**O pre-push ignora worktrees internos do Maestro.** Projetos sob `.maestro/` pertencem
à sessão que os criou e serão validados no próprio ciclo de commit; eles não entram na
descoberta de apps do checkout hospedeiro.

## 9.0.2 — 2026-08-12

**O pre-push entende monorepos.** Ele valida a materialização na raiz e executa as
convenções em cada projeto marcado por `opus.json`, sem exigir actions no diretório-raiz.
No repositório-fonte do SDK, a suíte continua sendo dona das fixtures negativas do linter.

## 9.0.1 — 2026-08-12

**A garantia de materialização volta a ser local.** `opus setup` deixa de criar o
workflow `.github/workflows/opus.yml`; o hook composto de pre-push e `opus check`
continuam sendo os gates reproduzíveis do pacote. Ao atualizar, o setup remove o workflow
anterior quando ele ainda está íntegro e gerenciado pelo Opus, preservando qualquer edição
local para revisão explícita.

## 9.0.0 — 2026-08-12

**Chrome denso passa a compartilhar a mesma régua visual.** Três primitives deixam de
depender de coincidências do call site:

- `TabsList` volta a aceitar uma altura fornecida por `className`; no variant `line`, o
  indicador ancora na borda da própria lista, inclusive quando ela preenche uma faixa mais
  alta que o size padrão.
- `Select variant="ghost"` abraça ícone, rótulo e chevron como um botão de toolbar. O
  rótulo ainda pode encolher e truncar, mas não cresce para empurrar o chevron à borda;
  agora ele também respeita a régua `default` (36px) / `sm` (32px).
- `Button`, `Select`, `Toggle`, `ToggleGroup`, `InputGroup` e `ButtonGroup` aceitam
  `shape="pill"`. Forma fica separada de intenção (`variant`) e altura (`size`), e grupos
  preservam o raio somente nas extremidades externas — inclusive quando contêm `Select`.
- o botão de envio do `Composer` usa a geometria pill do próprio `Button`.
- `Select variant="outline"` oferece a borda de um controle sem o `min-width` de campo:
  ícone, rótulo e chevron ficam juntos em uma única linha em toolbars e composers; rótulos
  longos truncam em vez de empurrar o chevron para uma segunda linha.
- superfícies elevadas voltam a usar `border border-border`; `ring-edge` e seu token são
  removidos, deixando `ring-*` para foco e estados transitórios.
- a elevação volta ao vocabulário de intensidade do Tailwind (`shadow-sm`, `shadow-md`,
  `shadow-lg` etc.), com a composição clara e difusa controlada pelo tema do Opus.
  Na 9.0.0, `Card`/`Composer`, menus/popovers e modais passaram a usar respectivamente
  `sm`, `md` e `lg` como defaults; os aliases por papel e as variáveis intermediárias
  `--elevation-*` saíram. O default do `Card` foi posteriormente removido na 10.0.0.
- `shadow-lg` ganha spread negativo e menos opacidade: mantém alcance, mas deixa de
  engrossar visualmente a borda no ponto de contato com a superfície.
- toda a escala passa a declarar a receita diretamente em `--shadow-*`, preservando a
  API nativa de modifiers do Tailwind, como `shadow-xl/30`.

**Migração:** os tokens públicos de elevação por papel foram removidos porque forma e
intensidade agora são ortogonais. Troque `shadow-card/popover/dialog` por
`shadow-sm/md/lg`; troque `ring-1 ring-edge` por `border border-border` e `border-edge`
por `border-border`. Overrides de `--elevation-*` devem migrar para o `--shadow-*`
correspondente. `opus check` acusa esses usos, que de outra forma perderiam o efeito
visual sem erro de compilação.

Também remova overrides locais de altura, posição do indicador e largura mínima que
existiam apenas para compensar os defeitos corrigidos nesta versão.

## 8.9.1 — 2026-08-04

**A migração deixa de esconder o hook Opus no Git.** `opus setup` agora remove também a
exclusão local legada e específica de `.claude/hooks/opus-check-on-stop.mjs`, preservando
as preferências realmente locais. Isso fecha o caso em que o check passava na máquina que
já tinha o hook, mas o arquivo não entrava no commit para um clone novo.

## 8.9.0 — 2026-08-04

**O Opus passa a materializar e versionar suas próprias skills e garantias.** `opus setup`
agora projeta as cinco skills do SDK para Claude e Codex, instala o hook Stop, participa do
pre-push composto, cria o gate `opus.yml` e mantém um bloco próprio em `AGENTS.md` e
`CLAUDE.md`. Tudo viaja no Git e `opus check` recusa drift entre o pacote instalado,
`base.json` e os arquivos comitados. O Maestro deixa de ser requisito operacional.

O catálogo é `implement-opus-change`, `create-opus-action`, `test-opus-action`,
`build-opus-ui` e `upgrade-opus`. `create-opus-action` gera o split atual
`defineContract` + `bindAction`; a skill legada `create-action`, que ainda emitia handler
inline, foi removida.

**Migração:** depois do bump, rode `pnpm exec opus setup`, remova do índice local quaisquer
exclusões mais amplas de `.claude/` ou `.agents/` que o setup reportar e comite `base.json`,
as duas projeções de skills, hooks, instruções, pre-push e workflow. O marcador per-app
`opus.json` passa de `base/baseVersion` para `package/version`.

## 8.6.6 — 2026-07-30

**O pacote passa a ser publicado no npm PÚBLICO.** O registry próprio
(`registry.softize.com.br`) vivia na VPS, e a VPS não volta — em vez de reconstruir o
serviço, ele sai da infra. Consequência prática boa: máquina nova instala **sem token
nenhum**, o que é o que faz um servidor poder ser destruído e recriado à vontade.

Nos projetos, apague a linha `@softize:registry=` do `.npmrc` — sem ela o npm público
é o padrão, e é onde o pacote está.

**Nome de cliente saiu do que é publicado.** Como o pacote virou público, as menções a um
cliente real no CHANGELOG, no `shellnav.md`, no `section-shell`, no `router` e no
`theme.css` viraram **Empresa X** — o placeholder que a documentação já usava. Nenhum
comportamento muda; é texto. O repositório segue privado, então o histórico não vai junto.

## 8.6.5 — 2026-07-28

**`<Chat>`: a fala do usuário ganha teto de altura.** O card do usuário é `sticky` — gruda
no topo enquanto o turno está em vista. Sem teto, uma mensagem longa (o caso normal quando
a primeira fala é um enunciado de tarefa inteiro) ocupava a tela toda e ficava lá, cobrindo
a resposta. Agora corta em `max-h-40`, com esmaecido e um **Mostrar mais**.

O expandido TAMBÉM tem teto, e ele sai do scroll container (60% da altura dele, medida),
não da viewport: soltar a altura devolvia o defeito atrás de um clique, e um teto em `vh`
devolvia o mesmo num `<Chat>` embutido em painel mais baixo que a tela — nos dois casos o
botão de fechar sai de vista e não volta enquanto o turno estiver em cima.

**Vale pra todo consumidor no bump** — é o comportamento do componente, não há prop pra
desligar. Ganchos novos: `data-slot` `chat-user`, `chat-user-body` e `chat-scroll`.

Um contrato que já existia de fato passa a ter consequência: **o `<Chat>` precisa de altura
limitada pelo pai**. Sem isso o container passa a ser dimensionado pelo conteúdo e o teto
se realimenta. Se o seu `<Chat>` está num pai de altura automática, limite-o no bump.

## 8.6.4 — 2026-07-26

**`Split resizable` volta a respeitar percentuais.** `react-resizable-panels` v4
interpreta valores numéricos como pixels; o adaptador agora envia
`initialSize`/`minSize`/`maxSize` com a unidade `%` explícita. Um pane declarado com
`initialSize={30}` deixa de nascer com 30 px e volta a ocupar 30% do split.

## 8.6.3 — 2026-07-26

**Rail colapsado segue a Empresa X.** `SidebarItem` usa um botão centralizado de
`size-9` quando a sidebar está recolhida, em vez de ocupar toda a largura disponível.
Ícone, área ativa e raio ficam idênticos ao padrão que originou o rail.

## 8.6.2 — 2026-07-26

**Docs de `Sidebar` migradas para slots de pane.** Os exemplos agora usam
`PaneHeader`, `PaneContent` e `PaneFooter`, e explicam a compatibilidade temporária dos
aliases `Sidebar*`.

## 8.6.1 — 2026-07-26

**Cabeçalho, conteúdo e rodapé pertencem ao pane, não à sidebar.** Entram
`PaneHeader`, `PaneContent` e `PaneFooter`: os três organizam qualquer pane, inclusive
um `Sidebar` encaixado em um `Split`. `SidebarHeader`, `SidebarContent` e
`SidebarFooter` continuam exportados como aliases deprecated para uma migração gradual.

## 8.6.0 — 2026-07-26

**Estrutura composicional de tela: `Split`/`Pane` e `Sidebar`.** `Split` concentra a
divisão horizontal ou vertical e recebe `resizable`; `Pane` define os espaços. `Sidebar`
cuida apenas do chrome de navegação, com `SidebarHeader`, `SidebarContent`,
`SidebarFooter`, `SidebarNav` e modo `collapsed`. `AppShell`, `SectionShell` e
`Resizable*` passam a estar marcados como deprecated nas docs, com os caminhos de migração.

**`opusDesign` deixa de perder domínio novo — watcher mais largo, 404 que se cura e entry
glob.** O episódio real: numa sessão de design o agente criou um domínio, levou 404 e
concluiu "precisa de restart" (não precisava — faltou registrar no entry). Três fixes
matam a classe:

- **`add`/`unlink` invalidam sempre** (sob o root, fora node_modules/.git): arquivo novo
  nunca importado não está no module graph — e é exatamente ele que um entry glob precisa
  enxergar. `change` mantém o critério do graph. Rebuild segue lazy; over-invalidation é
  barata.
- **404 que se cura e ensina.** Request sob o `/api` sem rota força UM rebuild por ciclo
  de staleness (mutex no boot — sem rebuild-storm de 404 legítimo) e re-tenta o match;
  persistindo, o envelope vem com mensagem-guia: action não registrada no runtime —
  confira o entry (path incluso) ou use entry glob. O driver node ganhou
  `hasRoute(method, pathname)` no handle pra viabilizar o pre-match.
- **`entry` aceita glob** (ex.: `'src/domains/*/contract.ts'`; pode misturar com entries
  estáticos no array): re-expandido a CADA rebuild — domínio novo entra no ar ao salvar o
  arquivo, sem editar entry nem reiniciar o dev server. Dos módulos casados entram os
  exports (default e named) com shape de registrável; schemas/helpers exportados junto
  ficam de fora. Matcher próprio sem dep nova: `*` casa um segmento, `**` qualquer
  profundidade. Entry estático segue funcionando exatamente como antes.

## 8.5.1 — 2026-07-24

**Um X só de clear (fantasma) e o trailing-botão balanceado no canto.** Dois acertos de
consistência nos adornos de campo:

- **O clear do Select vira X fantasma.** Era um badge circular CHEIO (`bg-muted-foreground`,
  X branco) — destoava de todo o resto dos adornos, que são ghost-muted no slot. Agora é um
  X `text-muted-foreground hover:text-foreground`, igual ao remover-chip do multi e ao que um
  `trailing` de limpar renderia num Input. Um tratamento de X, não três.
- **Trailing que é BOTÃO encosta no canto (~6px), não flutua a 12px.** Um botão-ícone num
  campo `h-9` tem 6px de folga em cima/embaixo; a da direita era o `px-3` do texto (12px),
  deixando o botão torto. Agora o slot trailing puxa `has-[button]:-mr-1.5` (Input sempre;
  Select só no buscável — com chevron o `gap` fica) pra o inset direito casar com o vertical.
  Mesmo balanceamento do `InputGroup`. Trailing de texto puro segue alinhado ao texto (12px).

## 8.5.0 — 2026-07-24

**`Input` adornado (`icon`/`trailing`) passa pro modelo flex — o mesmo do `InputGroup`/`Select`.**
Antes o adorno era SOBREPOSTO (`absolute`) e o texto abria espaço com `pl-9`/`pr-9`; a
distância do trailing à borda (6px) divergia da do Select (12px). Agora, com adorno, a
BORDA e o anel de foco moram no WRAPPER (disparados pelo `:focus-visible` do `<input>`
interno — foco de teclado), o `<input>` fica sem borda e CRESCE (`flex-1`), e os adornos
são irmãos ao lado: o `px-3`/`gap` posicionam sozinhos (trailing a 12px, casando com o
Select). Some a conta de `pl-9`/`pr-9`. **Nota de call site:** o `className` de um Input
ADORNADO agora estiliza o CAMPO (o wrapper — largura, raio, fundo, fonte), como no Select;
a fonte/cor cascateiam pro texto digitado. O Input CRU (sem `icon`/`trailing`) segue com o
`className` no próprio `<input>`, borda e ref nele — zero mudança. O adorno era novidade
recente (8.3.x) e só a barra de endereço do Maestro o usava.

**O preview de design vira in-process: driver `server/node` + plugin `opusDesign()`.** A
Trilha A do isolamento preview×prod (ADR 0004 da softize): desenhar tela não pode depender
de backend de pé — nem, pior, falar com prod por um proxy esquecido no vite.config.

- **`@softize/opus/server/node`** — `nodeServer()`: `ServerAdapter` connect-style sem
  framework. Devolve `{ adapter, handler }`: o adapter vai no `createRuntime({ server })`;
  o handler `(req, res, next?)` encaixa em qualquer stack node — middlewares do vite,
  express, `http.createServer` direto. Mesmo protocolo do driver fastify (helpers
  compartilhados de `@softize/opus/server`): rotas por convenção, envelopes, status por
  categoria, endpoints health/ready/openapi.
- **`@softize/opus/vite`** — `opusDesign()`: com `vite --mode design`, carrega o entry
  (`./opus.config.ts` por default, via SSR do vite — aliases do app valem), registra as
  actions num runtime local (SÓ actions: reactions/schedules ficam de fora — o runtime de
  design é request/response) e serve `/api` dentro do próprio dev server, com
  `OPUS_MODE=design` fazendo os `mockHandler` responderem e `designAuth` dando a sessão
  fixa (`{ id: 'design' }`). Qualquer `server.proxy` do config é DESLIGADO — prefixo
  ex-proxy sem cobertura responde 503 em envelope (`design.no_backend`), nunca vaza pra
  fora. `stubs` por prefixo cobrem o que não é opus (ex.: `/api/auth`, podem streamar);
  `GET /__opus/design` é a sonda do Maestro (versão, contagem de actions/mocks, stubs);
  HMR reconstrói o runtime quando qualquer módulo carregado muda. `devBackend: true`
  monta o `/api` real também no dev normal (sem modo design).
- **`runAction` (testing) respeita o modo design.** Mesma régua do `execute()` (fonte
  única): com `OPUS_MODE=design` e `mockHandler` presente, o mock roda; sem o env, o
  handler real. Contrato puro (sem handler) vira TESTÁVEL em design — os testes-de-contrato
  da sessão de design deixam de ser impossíveis; fora de design, erro claro
  (`testing.no_handler`) em vez de estouro obscuro.
- **O esqueleto do `opus create` nasce contract-first.** `dev:design` vira
  `vite --mode design`, o vite.config traz `opusDesign({ devBackend: true })` (dev normal
  serve o handler real via /api; design roteia pro mock) e o domínio-exemplo mostra o
  split canônico: `defineContract` com `mockHandler` alimentado pelo schema (`fakeMany`)
  + `bindAction` com o handler in-memory — o App consome o CONTRATO via `useListAction`
  (fim do "chama o handler direto, sem servidor ainda"), e o teste cobre o bound E o
  contrato puro em design. O plugin também marca `@softize/opus` como `ssr.noExternal`:
  o pacote embarca TS source e, externalizado, o node recusa TS sob node_modules no
  load SSR do entry.
- **`options: { kind: 'dictionary', ref }` agora resolve.** O `TbdlibProvider` ganha
  `dicts` (`Record<ref, DictLike>` — o `DictType` do `t.dict` encaixa direto) e a UI
  resolve a origem declarada no contrato: no `ActionList`, filtro e chips mostram os
  labels do vocabulário; no `ActionForm`, a precedência é prop `options` do campo >
  `fieldOptions` do form > `options` do FieldSpec (dictionary/static) > **meta do
  `t.dict` no schema** (zero-config: campo dict — ou multiselect de dict — resolve
  value→label pela meta que viaja no contrato, sem registry) > chaves cruas do z.enum.
  Campo texto com opções declaradas vira single-select por-id. Habilitador por baixo:
  `attachLogicalType`/`getLogicalType` moveram pro core (o `@softize/opus/schema`
  re-exporta — API igual) porque a fronteira SPA só deixa a UI tocar ui/lib/core.

**Diálogos imperativos: o trio `dialog`.** `dialog.alert` (Promise<void>, reconhecimento
obrigatório) · `dialog.confirm` (Promise<boolean>, agora com slot `body` pra corpo próprio) ·
`dialog.prompt` (Promise<string|null>, um input). O `confirm()` vira o namespace `dialog` —
`window.alert/confirm/prompt` são globais do browser e um import esquecido cai no nativo;
`window.dialog` não existe. Tudo sobre o mesmo `AlertDialog` (role=alertdialog, não fecha
fora) e a mesma fila; `confirm()` e `<ConfirmHost/>` seguem como aliases de `dialog.confirm`
e `<DialogHost/>` (zero migração). A doc do AlertDialog passa a mostrar o caso manual real
(mais de duas ações), já que o `body` cobriu o "corpo próprio".

## 8.4.1 — 2026-07-24

**O tom da ação `trailing` mora no SLOT, não no call site.** Uma ação no fim do campo é
SECUNDÁRIA — então o `text-muted-foreground` passou pro slot `trailing` (do Input e do
Select), e o botão ghost herda. Antes cada call site sprinklava `className="text-muted-foreground"`
no botão (inerte quando o slot já daria o tom); agora o trailing nasce muted e acende no
hover pelo próprio ghost, sem cor no call site.

## 8.4.0 — 2026-07-24

**`Button` ganha `icon-xs` (24px) — a ação DENTRO de um campo.** Fecha a escala dos botões
só-ícone: `icon` (36) · `icon-sm` (32) · `icon-xs` (24). O `icon-xs` é o par do `icon-xs`
que o `InputGroupButton` já tinha — pra um clear/mostrar-senha no `trailing` do Input/Select
caber folgado num h-9. Antes o exemplo virava `className="size-6"` (override na mão, fora
da escala); agora é `size="icon-xs"`.

Doc afinada junto: o exemplo de "limpar" do `InputGroup` deixou de ensinar a fazer à mão o
que o `<Input trailing={…} />` já resolve — o InputGroup agora mostra o caso que é DELE
(ícone leading + botão no fim juntos, a busca do ActionList). Uma ação simples é prop;
adornos que se acumulam, InputGroup.

## 8.3.2 — 2026-07-24

**`Input` ganha `trailing` — fecha a simetria com o Select.** O 8.3.0 unificou o LEADING
(`icon` no Input, Select e Alert); este fecha o TRAILING, que estava pela metade (Select
fazia ação-no-fim por prop, Input só por `InputGroup`). Agora `<Input trailing={<Button/>} />`,
o par do `icon`. Regra: **um ícone e/ou uma ação → props (`icon`/`trailing`); mais de um
adorno junto, prefixo de texto ou adorno em bloco → `InputGroup`.** Sem breaking.

(Saiu como 8.3.2 — o `release patch` num momento de versionamento cruzado com outra sessão;
por conteúdo é feature, valeria um minor, mas está publicada e é o que vale.)

## 8.3.1 — 2026-07-24

**Destructive legível no escuro + a ação do Alert no lugar.** Duas correções que vieram
de olhar o dark com atenção:

- **`--destructive` do dark clareia (`hsl(0 62.8% 30.6%)` → `hsl(0 72% 58%)`).** O valor
  antigo era calibrado só pra FUNDO (com texto branco por cima), mas menu, alert, field
  e erros de form usam `text-destructive` como FOREGROUND — e um vermelho de 30,6% de
  luminosidade sobre o popover dava ~1,7:1, abaixo do AA (medido). Agora passa como texto
  e alinha o destructive ao que o success já fazia (`dark:text-emerald-400` pula pro tom
  claro). **Efeito nos fundos sólidos** (Button/Badge/toast destructive): ficam mais vivos
  no dark — o branco por cima segue perfeitamente legível (validado no visual), e o
  resultado é consistente com o light e com o shadcn novo. Light **não muda** (só o bloco
  `.dark` foi tocado).
- **Ação do Alert alinha na coluna de conteúdo.** No Alert com ícone (grid de 2 colunas),
  `AlertTitle`/`AlertDescription` têm `col-start-2`, mas a AÇÃO (children depois da frase,
  ex.: um `<Button>Reiniciar</Button>`) não tinha — caía na coluna do ícone (~1rem) e era
  espremida pra esquerda. Agora vai num wrapper `col-start-2`, alinhada sob o texto.

## 8.3.0 — 2026-07-24

**`Input` ganha `icon` — adorno de campo vira prop, unificado com Select e Alert.** O ícone
leading passa a ser a MESMA prop nos três: `<Input icon={<Building2 />} />`. Antes o Input só
tinha o `InputGroup` (compositivo, verboso, divergente do Select/Alert); agora adorno simples
é prop e o `InputGroup` fica pro avançado (botão no fim, prefixo, bloco, múltiplos). Sem
breaking: prop opcional, o `<input>` cru segue igual sem ela.

## 8.2.1 — 2026-07-24

**Remove a auto-dependência `file:` do `package.json` do próprio opus (publish quebrado).**
Uma sessão paralela injetou `"@softize/opus": "file:…/scratchpad/opus.tgz"` no package.json
do pacote (provável `pnpm add` do tarball local pra testar), e isso foi publicado da 8.0.3 até
a 8.2.0: todo consumidor que instalava puxava um `file:` pra um scratchpad que só existia
naquela máquina — deploy quebrava com ENOENT. 8.2.1 sai limpo.

## 8.2.0 — 2026-07-24

**`Select` ganha `trailing`.** Um slot pra uma ação custom DENTRO do controle, no fim (antes
do chevron) — um botão que age sobre o valor escolhido, em vez de ficar como irmão solto ao
lado do campo. O clique no trailing **não** abre a lista (o slot para a propagação pro
controle). Pareia com o `icon` (leading) que já existia.

## 8.1.0 — 2026-07-24

**`Tabs` ganha `size`, `Button` ganha `icon-sm` — o `sm` que faltava pra fileira densa.**
Button e Select tinham `sm` (h-8, 32px); o `Tabs` era h-9 cravado e o `Button` só-ícone só
tinha `icon` (36px). Numa toolbar/header onde os controles são `sm`, o segmento de abas e um
botão de ícone ficavam 4px mais altos que os irmãos — e a saída era override de altura na
mão, que foge da escala do sistema.

- `<Tabs size="sm">` baixa a lista pra h-8 (o size flui pra TabsList por contexto). Isto
  **ejetou** o Tabs (o shadcn não tem size aqui) — divergência declarada no lock.
- `<Button size="icon-sm">` é o quadrado de 32px (`size-8`), par do `sm`.

Sem breaking: props novas, opcionais. Quem quiser uma fileira toda em 32px agora usa só
props de size, sem tocar em altura.

## 8.0.4 — 2026-07-24

**A "dançada" do menu, resolvida DE VERDADE — o culpado era o botão.** A pista final
veio do João: o `active:scale-95` do Button (toque da casa, com `transition-all`).
No clique HUMANO — que pressiona e segura ~100ms, diferente do clique sintético dos
testes — o menu abre e se ancora enquanto o gatilho ainda está ENCOLHENDO; o
posicionador do Radix acompanha o rect a cada frame, então o painel recém-aberto
**persegue o botão** em passos de 0,5px (~2px de deriva) e desanda na soltura.
Medido com clique segurado; com o scale removido, âncora imóvel do press à soltura.

- **Press do Button vira `active:brightness-90`.** Feedback de clique continua
  (escurece de leve), mas NÃO-GEOMÉTRICO: o rect não muda, nenhum popover ancorado
  (Menu, Popover, Select…) tem por que dançar. Regra que fica: press feedback em
  gatilho não pode mexer em geometria.
- **A entrada do menu volta a fade + zoom ancorado** (a escolha da 8.0.2 — sem
  slide — fica; o fade-only experimental não chegou a publicar). O `will-change`
  da 8.0.3 também fica: animação no compositor é robustez de graça.
- As 8.0.2/8.0.3 não eram a causa, mas seguem valendo como melhorias: coreografia
  mais simples e animação imune a stall de main thread na 1ª abertura.

## 8.0.3 — 2026-07-24

**A "dançada" de verdade: era só na PRIMEIRA abertura — e agora tem causa medida e
correção.** A 8.0.2 simplificou a coreografia (decisão que fica), mas o sintoma
persistia, e o detalhe "só na primeira vez" fechou o diagnóstico: a 1ª abertura da
página paga a inicialização única (portal + focus-scope + scroll-lock + primeiro
raster do painel), que trava o main thread NO MEIO dos 150ms — medido em Chrome com
janela real/Retina: **congelamento de até 219ms** com o menu parado no meio do voo,
que completava depois com um tranco. Da 2ª abertura em diante, tudo quente → suave.
É por isso que nenhum teste sintético "médio" pegava.

- **Correção: `will-change: transform, opacity` no conteúdo do menu e do submenu.**
  Promove a layer ao compositor ANTES de animar — com a animação rodando no
  compositor, o main thread pode travar que ela segue tocando. Medido pós-fix
  (4 rodadas, browser frio): zero stalls no meio da animação, cadência 16-17ms.
  O resto do custo de init vira só ~50ms até o primeiro paint — e como o fade começa
  em opacity 0, esse início nem é visível.
- Custo: uma layer composited por menu aberto — irrelevante nesse tamanho.
- Nota: Popover/Select/Tooltip têm a mesma anatomia de animação e podem herdar o
  mesmo tratamento — ficam pra quando (se) o sintoma aparecer lá; Select e Popover
  são componentes TRAVADOS (byte-fiéis), então a mudança lá passa por ejetar.

## 8.0.2 — 2026-07-24

**Menu abre sem "dançada" + a doc ganha o clique-direito.** Dois refinamentos no Menu:

- **Entrada sem slide.** A coreografia herdada do shadcn compunha TRÊS movimentos em
  150ms — descer 7,5px (slide) + inchar 5% (zoom) + fade — e o conjunto lia como
  "dançada" na abertura. Investigação instrumentada (posição frame a frame, câmera
  lenta, screencast em velocidade real, reopen no meio do animate-out) confirmou: não
  há pulo de posição nem bug de layout — a impressão vinha da própria coreografia.
  Agora a entrada é **fade + zoom ancorado no gatilho** (o que o Radix Themes faz);
  a saída já era simétrica e fica como está. Vale pro conteúdo do menu E do submenu.
- **Doc: "Contexto: clique direito".** O papel do ContextMenu (aposentado na 5.0.0)
  documentado com preview funcional: gatilho CONTROLADO ancorado no ponteiro
  (`onContextMenu` guarda a posição; um span `position: fixed` invisível naquele ponto
  ancora o conteúdo). A página do Menu agora cobre o caso que justificou a unificação.

## 8.0.1 — 2026-07-24

**Menu: o hover do item destructive volta a tingir o fundo.** A decisão de 17/jul
(padrão Notion: hover muda SÓ a cor do texto/ícone, bg segue o accent) foi testada no
uso real e revertida: **no dark, texto destructive sobre o accent do hover não lia bem**
— o vermelho perdia contraste justamente no item que mais precisa ser inconfundível.
Volta o comportamento anterior, byte-fiel ao que era antes da decisão:

- Em repouso o item segue NEUTRO (a decisão de 2.24.1, essa fica);
- No hover/focus, texto/ícone destructive **sobre `bg-destructive/10`**
  (dark: `bg-destructive/20`) — o fundo tingido devolve a leitura no escuro.

Visual, só no `<Menu>`; nenhuma API muda. O delta do lock registra o teste e a reversão
— se o padrão Notion voltar, volta sabendo por que saiu.

## 8.0.0 — 2026-07-24

**A densidade do markdown sai — por ora.** O `size` do `<Markdown>` e a regra
`.prose[data-size='sm']` do theme.css foram revertidos: no uso real quase toda mensagem
tem um parágrafo só, e ali o `[&>:first-child]:mt-0 [&>:last-child]:mb-0` já zerava tudo —
a regra só atuava nas mensagens com vários blocos, que são minoria. O que de fato apertava
o transcript era o vão ENTRE mensagens (resolvido no 7.2.0, que fica). Sobrar uma prop de
efeito quase nulo é pior que não ter: vira decisão a mais em cada call site.

Fica o que se pagou: **motor único** (markdown-it no chat e na doc), a correção do respiro
fantasma e a hierarquia de gaps do Chat.

Se a densidade voltar, volta com o problema entendido — e a discussão em aberto é se o
`prose` (régua de artigo) deve mesmo estar no chat, ou se ali cabe estilo próprio.

### Breaking

`MarkdownProps.size` saiu. Quem passava `size="sm"` só remove a prop — o render é o mesmo
de antes, já que a regra que dava efeito a ela também saiu.

## 7.2.0 — 2026-07-24

**Hierarquia no espaço do transcript.** O `Chat` usava o MESMO `gap-2` nos dois níveis:
entre turnos e entre as falas de dentro de um turno. Como o agente narra o que vai fazendo
("Vou ler o arquivo…", "A estrutura está correta…"), um turno junta oito falas curtas — e
com o gap igual nos dois níveis o transcript virava uma parede uniforme, sem começo nem fim
visíveis, gastando ~56px só de vão.

Agora: **`gap-1` dentro do turno** (as falas são um raciocínio contínuo, andam juntas) e
**`gap-3` entre turnos** (aí sim separa pergunta de pergunta). Num turno de oito falas são
~28px a menos, e de quebra dá pra bater o olho e ver onde cada turno começa.

Isto é o par da densidade do 7.1.0, e ataca o que aquela mudança NÃO alcançava: mensagem de
um parágrafo só já tinha margem interna zerada — o que ocupava espaço ali era o vão entre
mensagens, que é do Chat, não do markdown.

## 7.1.1 — 2026-07-24

**`@types/markdown-it` vai pra `dependencies`.** O pacote distribui o SOURCE (`.tsx`), então
o typecheck de quem consome atravessa nossos arquivos: com os tipos só em `devDependencies`,
todo app quebrava com `TS7016: Could not find a declaration file for module 'markdown-it'` —
e agora atinge todos, porque quem importa é o primitivo `Markdown`, que o `Chat` usa. Regra
que fica: **dependência que aparece em `import` de arquivo publicado precisa dos tipos em
`dependencies`**, não em dev.

## 7.1.0 — 2026-07-24

**Um motor de markdown só.** Havia dois: o `Markdown` (primitivo, usado pelo `Chat`) era um
parser de **regex à mão**, e a doc renderizava com **markdown-it**. Mesma mensagem, render
diferente conforme onde caísse — e o argumento que justificava o parser caseiro ("sem
dependência de runtime") não valia mais: o markdown-it já era `dependency` do pacote por
causa da doc. Agora o primitivo usa markdown-it, e a doc importa a **mesma instância** (não
duas com opções próprias, que divergiriam no primeiro edge case).

Em prática, o chat ganha o que o regex não cobria: itálico, listas aninhadas, referências,
ênfase combinada, todo o CommonMark. `html: false` segue valendo — tag HTML no fonte é
escapada, o que é o que torna seguro renderizar texto de usuário e de modelo.

**`<Markdown size="sm">`** encolhe o RESPIRO entre blocos mantendo o corpo em 14px — mesma
semântica do `size` no Button (encolhe o espaço, não a letra). O `<Chat>` passou a usar.
A régua do `prose` é de artigo: 16px em volta de cada parágrafo, 20px no bloco de código e
**40px em volta de cada `<hr>`** — numa bolha, uma linha divisória custa mais que a mensagem.

O espaçamento mora no `theme.css` (`.prose[data-size='sm']`), junto do resto da tipografia,
e o seletor é **universal** (`> *`), não uma lista de tags — a primeira versão desta regra
enumerava p/ul/ol/pre/blockquote/h1-h4/li e deixava passar justamente o `hr`, o maior de
todos. Elemento novo (table, figure, dl) entra sozinho.

### Corrigido

**O respiro fantasma nas mensagens.** As bolhas do `Chat` tentavam zerar a margem do
primeiro e do último bloco com `[&>*:first-child]:mt-0`, mas o primeiro filho da bolha é o
*wrapper* do `prose` — que não tem margem; quem tem é o `<p>` um nível abaixo. A
compensação nunca surtiu efeito, e cada mensagem carregava ~16px invisíveis em cima e
embaixo. Agora mora dentro do `Markdown`, onde alcança o bloco certo.

## 7.0.1 — 2026-07-23

**Ponteiros mortos pro `DeleteButton`.** Duas páginas (`confirm` e `alert-dialog`) ainda
mandavam usá-lo pra excluir por contrato, um release depois de ele sair. O gate de doc
(`content-scope`) só enxerga JSX nos previews — prosa que cita componente inexistente passa,
e essa é a terceira vez no dia que a varredura à mão é que pega.

## 7.0.0 — 2026-07-23

**O `DeleteButton` durou um release.** Ele entrou no 6.0.0 e sai agora: era o
`ActionTrigger` com uma lixeira dentro — mesmas props (`action`, `input`, `label`,
`onSuccess`, `className`), e o trigger já tinha `variant`, `size: 'icon'` e a confirmação
do contrato. Pior: o nome furava a convenção da casa, onde contract-driven com `action` na
primeira prop é a família `Action*`. Criar componente onde cabia prop é o que os releases 3
a 5 desfizeram — repeti o erro e desfaço.

As três coisas que só ele tinha subiram pro `ActionTrigger`:

- **`icon`** — o botão vira icon-only, com o rótulo no tooltip e no `aria-label`, visual
  discreto (vermelho no hover quando o contrato marca `destructive`) e clique que **não
  vaza** pro item em volta.
- **`itemLabel`** — nomeia o alvo na pergunta, entre aspas, antes da mensagem do contrato.
- **Erro de negócio falado** — a frase do servidor vence o rótulo genérico em
  `conflict`/`validation`/`not_found`; o resto cai no rótulo, pra não vazar
  `violates foreign key constraint …` pra quem só clicou num botão. Agora vale pra QUALQUER
  ação de item (arquivar, duplicar, reprocessar), não só excluir.

### Breaking

`DeleteButton` e `DeleteButtonProps` saíram.

```tsx
// Antes
<DeleteButton action={unitDelete} input={{ id: u.id }} itemLabel={u.name} />
// Depois
<ActionTrigger action={unitDelete} input={{ id: u.id }} icon={<Trash2 />} label="Excluir" itemLabel={u.name} />
```

**Confira se o contrato declara `confirm`.** O `DeleteButton` perguntava SEMPRE (montava a
caixa com textos default); o `ActionTrigger` segue a regra da casa — sem `action.confirm`
(ou a prop `confirm`), o disparo é **direto**. Um delete sem `confirm` declarado passaria a
apagar sem perguntar. A confirmação é spec: declare no contrato.

## 6.0.3 — 2026-07-23

**A página do AlertDialog mostra o caminho curto.** Ela dizia em texto que `confirm()` faz
a mesma pergunta em uma linha, mas não mostrava — quem chegasse por ali montava as 15
linhas assim mesmo. Agora tem preview vivo e clicável.

## 6.0.2 — 2026-07-23

**`confirm()` avisa da colisão com o `window.confirm`.** Esquecer o import não dá erro: o
TypeScript resolve pro global e o código passa a abrir a caixa cinza do sistema (que
webview suprime e não tem tema). A doc agora abre com esse aviso e sugere
`no-restricted-globals` no ESLint.

## 6.0.1 — 2026-07-23

**Import órfão que quebrava o typecheck de quem consome.** A migração da confirmação do
`ActionList` pro AlertDialog deixou `DialogDescription` importado sem uso — inofensivo
aqui, erro (`TS6133`) em todo app com `noUnusedLocals`, que é o caso dos nossos. O gate do
pacote não pegava porque o tsconfig DELE não tinha a regra: agora tem
(`noUnusedLocals` + `noUnusedParameters`), então o próximo órfão morre antes de publicar.

## 6.0.0 — 2026-07-23

**`confirm()` — a confirmação em uma linha.**

```tsx
if (await confirm({ title: 'Excluir a sessão?', action: 'Excluir', variant: 'destructive' })) …
```

Mesma forma do `toast` (superfície imperativa + host no shell), com a diferença de que esta
**responde** (`Promise<boolean>`). Nasceu de um levantamento constrangedor: a casa
confirmava exclusão de QUATRO jeitos — um `DeleteButton` copiado entre dois repos (116 e
130 linhas, já divergentes), dialogs montados à mão em três telas, um botão de "dois
cliques" e o confirm declarativo dos patterns. Todos porque montar a pergunta custava ~15
linhas de JSX.

Requer `<ConfirmHost />` no shell, ao lado do `<Toaster />`. Sem ele **lança** em vez de
devolver promise pendurada — clique sem efeito é o pior desfecho pra uma pergunta
destrutiva (mesma guarda do `onAsk` no driver de IA).

**`<DeleteButton action input />` sobe pro pacote.** É a cópia que existia nos dois repos,
na versão mais madura: allowlist de erro de NEGÓCIO (a frase do servidor só vence o rótulo
genérico em `conflict`/`validation`/`not_found` — erro inesperado traria
`violates foreign key constraint …` pro toast de quem só clicou em excluir).

**Toda confirmação da casa passa a ser o AlertDialog compacto.** `ActionTrigger` e
`ActionList` confirmavam com `Dialog` comum — agora usam `AlertDialog`: `role="alertdialog"`,
não fecha no clique fora. O compacto virou o desenho ÚNICO.

**`<Alert>` ganha forma curta:** `<Alert icon={<Info />} title="…" description="…" />` numa
linha; a composição segue pro conteúdo rico.

### Corrigido: o alert saía uma palavra por linha

`<Alert>Workspace Empresa X sincronizado.</Alert>` renderizava a frase quebrada, uma
palavra por linha — e a doc ensinava exatamente esse uso. Duas causas somadas: o alert era
SEMPRE `grid grid-cols-[0_1fr]`, então texto solto virava item anônimo na 1ª coluna, de
largura **zero**; e o `AlertDescription` era `grid justify-items-start`, onde cada nó de
uma frase interpolada (`Workspace {cliente} {nome} sincronizado.`) virava um item empilhado.
Agora o grid só existe quando há ícone, a descrição não é grid, e texto cru como filho cai
no slot de descrição sozinho.

### Breaking

**`size` saiu do `AlertDialogContent`** — o compacto (`max-w-xs`, footer em duas colunas) é
o único desenho. Quem passava `size="sm"` só apaga a prop; quem dependia do largo
(`sm:max-w-lg`) precisa decidir: confirmação curta cabe no compacto, e conteúdo que não
cabe provavelmente não era uma confirmação — é `Dialog`.

```tsx
// Antes                              // Depois
<AlertDialogContent size="sm">        <AlertDialogContent>
```

O `Alert` deixou de aceitar o atributo `title` do HTML (agora `title` é o título do alert).
Era tooltip nativo, que a casa já bania em favor do `Tooltip`.

## 5.0.1 — 2026-07-23

**Nome morto na doc, e o gate que faltava.** O preview do Composer ainda montava
`<DropdownMenu>` — transpila igual, e explodia no browser. Sete `whenToUse` do catálogo
também apontavam pra ele ("pra menu de ações, use DropdownMenu"), mandando quem lesse
importar um nome que não existe mais.

O `content-fences` provava que todo fence TRANSPILA; agora um irmão (`content-scope`)
prova que **todo componente citado num preview existe no scope** — foi ele que teria
pegado isto sozinho, e é o que cobre o próximo rename.

## 5.0.0 — 2026-07-23

**Cinco menus viram um: `Menu`.** O catálogo tinha `DropdownMenu`, `ContextMenu`,
`Menubar`, `NavigationMenu` e `Command` — e a decisão "qual dos cinco" caía na tela toda
vez. Medindo o uso real (softize + cliente + o próprio Opus): DropdownMenu em 4 telas,
Command como motor do Select e do IconPicker, e **zero** para os outros três.

- **Saem `ContextMenu` e `Menubar`**: é o mesmo menu com outro gatilho — o ContextMenu é o
  dropdown no botão direito; o Menubar são N dropdowns numa barra, vocabulário de app
  desktop (Arquivo/Editar/Ver) que nenhuma tela nossa tem.
- **Sai `NavigationMenu`**: mega-menu de LINKS de site (semântica `<nav>`, não
  `role="menu"`). Papel legítimo — que a casa não exerce.
- **`Command` fica**: não é menu, é busca por teclado (e é o motor do Select).
- **`DropdownMenu` vira `Menu`**: sem irmãos pra distinguir, o qualificador não
  distinguia de nada. "Dropdown" ainda por cima descreve como abre, não o que é — e no uso
  corrente costuma significar o `Select`.

Componente portado não se cria, se baixa: no dia que existir a primeira tela com menu de
contexto, `scripts/port-shadcn.mjs context-menu` traz o ContextMenu byte-fiel de volta em
um comando. Até lá, cada entrada a mais no catálogo só cobra pedágio na decisão.

### Breaking

`DropdownMenu*` → `Menu*` (rename puro, mesma API):

```tsx
// Antes                                   // Depois
<DropdownMenu>                             <Menu>
  <DropdownMenuTrigger asChild>              <MenuTrigger asChild>
  <DropdownMenuContent align="end">          <MenuContent align="end">
    <DropdownMenuItem …                        <MenuItem …
    <DropdownMenuCheckboxItem …                <MenuCheckboxItem …
    <DropdownMenuSeparator />                  <MenuSeparator />
```

Vale pros 15 subcomponentes (`Trigger`, `Content`, `Item`, `CheckboxItem`, `RadioGroup`,
`RadioItem`, `Label`, `Separator`, `Shortcut`, `Group`, `Portal`, `Sub`, `SubTrigger`,
`SubContent`). `data-slot`: `dropdown-menu-*` → `menu-*`.

`ContextMenu*`, `Menubar*` e `NavigationMenu*` **não existem mais**. Menu de contexto e
barra de menus se fazem com `Menu`; navegação de links, com `<nav>` + `Button`/`Link` (ou
re-porte o componente do registry shadcn se o caso for mesmo um mega-menu).

## 4.0.0 — 2026-07-23

**Sheet e Drawer eram o mesmo componente com dois motores.** Painel que desliza de uma
borda: o `Sheet` fazia isso com Radix Dialog, o `Drawer` com [vaul](https://vaul.emilkowal.ski/)
(que acrescenta gesto de arrastar e snap points). Mesmo papel, duas entradas no catálogo —
e a decisão "qual dos dois" caindo na tela toda vez. Prova de que não funcionava: o único
consumidor da casa importava o `Sheet` e batizou o wrapper local de `Drawer`.

Fica **um**, com o motor Radix (mesma família do Dialog: overlay, trap de foco, ESC) e o
nome **`Drawer`** — que é como MUI, Ant Design, Chakra e Mantine chamam o painel de borda;
"sheet" é vocabulário do shadcn/Apple. O `vaul` sai das dependências. Se um dia aparecer o
caso real de bottom sheet arrastável, ele volta como PROP do Drawer, não como componente
irmão.

Limpeza junto: `@radix-ui/react-select` continuava declarado desde o 3.0.0, sem ninguém
importar. Saiu.

### Breaking

`Sheet`, `SheetTrigger`, `SheetClose`, `SheetContent`, `SheetHeader`, `SheetFooter`,
`SheetTitle` e `SheetDescription` **saíram**: troque o prefixo por `Drawer*`. A API é a
mesma (`side`, `showCloseButton`, `open`/`onOpenChange`, `asChild`) — é rename, não
redesenho.

```tsx
// Antes                                  // Depois
<Sheet open={o} onOpenChange={setO}>      <Drawer open={o} onOpenChange={setO}>
  <SheetContent side="right">               <DrawerContent side="right">
    <SheetHeader><SheetTitle>…               <DrawerHeader><DrawerTitle>…
```

Quem usava o `Drawer` do vaul: a API de composição é a mesma, mas **`direction` virou
`side`**, e `DrawerPortal`/`DrawerOverlay` deixaram de ser exportados (o `DrawerContent`
já os inclui). O gesto de arrastar e os snap points não existem mais.

`data-slot`: `sheet-*` → `drawer-*`.

## 3.0.1 — 2026-07-23

**A lista do `variant="ghost"` ganha largura própria.** Ela seguia a largura do controle
(certo no campo de form, errado na barra do composer: o gatilho ali é do tamanho de uma
chave curta como `GB-42`, e a lista saía com 160px cortando o título inteiro). Agora o
ghost abre em `min-w-56 max-w-80` — o resto do modo default segue acompanhando o campo.

## 3.0.0 — 2026-07-23

**Um Select só.** Escolher em lista era QUATRO componentes com TRÊS APIs diferentes
(`Select` composicional do Radix, `NativeSelect` com `NativeSelectOption` como filho,
`Combobox` data-driven, `ComposerSelect` data-driven com outro nome de props) — e a
decisão "qual dos quatro" caía na tela, toda vez. Agora é **um componente, modos por
prop**: `<Select>` com `options` (dado, nunca JSX de item) + `searchable` · `multiple` ·
`native` · `variant="ghost"`. A régua que importava (achabilidade: lista longa precisa de
busca) virou UMA prop, não a escolha do componente.

**Corrigido junto: o `id` agora chega ao campo.** O cmdk sobrescrevia o `id` do input, e
o `htmlFor` do Label não achava controle nenhum — clicar no rótulo não focava. O Select
reaplica o próprio id; o `ActionFormField` perdeu o fallback que existia só por causa
disso.

**Nav da doc por categoria.** O catálogo de UI (`/ui`) deixou de ser uma lista alfabética
de 67 itens + uma seção "Patterns" à parte: são dez grupos (Estrutura, Actions, IA,
Formulário, Botões, Navegação, Exibição, Feedback, Sobreposição, Layout), e o item de
subgrupo rotulado agora **recua** sob o rótulo no `SectionShell` (três níveis na mesma
margem não eram hierarquia).

### Breaking

`Combobox`, `NativeSelect`, `NativeSelectOption`, `NativeSelectOptGroup`, `ComposerSelect`,
`SelectClear` e toda a API composicional do Radix (`SelectTrigger`, `SelectValue`,
`SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, `SelectSeparator`,
`SelectScrollUpButton`, `SelectScrollDownButton`) **saíram do pacote**. `ComboboxOption`
virou `SelectOption`.

```tsx
// Antes — Select do Radix (composicional)
<Select value={v} onValueChange={setV}>
  <SelectTrigger id="papel"><SelectValue placeholder="Selecione" /></SelectTrigger>
  <SelectContent>
    <SelectItem value="dev">Developer</SelectItem>
  </SelectContent>
</Select>
// Depois
<Select id="papel" value={v} onChange={setV} placeholder="Selecione"
  options={[{ value: 'dev', label: 'Developer' }]} />

// Antes — NativeSelect
<NativeSelect value={v} onChange={(e) => setV(e.target.value)}>
  <NativeSelectOption value="dev">Developer</NativeSelectOption>
</NativeSelect>
// Depois
<Select native value={v} onChange={setV} options={[{ value: 'dev', label: 'Developer' }]} />

// Antes — Combobox                    → Depois
<Combobox value={v} onChange={setV} options={o} />
<Select searchable value={v} onChange={setV} options={o} />

// Antes — ComposerSelect              → Depois
<ComposerSelect value={v} onChange={setV} options={o} />
<Select variant="ghost" value={v} onChange={setV} options={o} />
```

Ponto a ponto:

- **`onChange` recebe o VALUE**, não o evento — inclusive no `native` (era
  `e.target.value`).
- **Grupo é dado**: `group` na opção (vira `<optgroup>` no native, heading no custom);
  `SelectGroup`/`SelectLabel`/`SelectSeparator` não existem mais.
- **`clearable` é prop** (o `SelectClear` por composição saiu).
- **Item rico**: `label` é sempre string (campo, busca, a11y) e o JSX vai em `content`.
  `triggerLabel` (do ComposerSelect) segue na opção.
- **`defaultValue` saiu**: o Select é controlado (`value` + `onChange`).
- **Largura**: o trigger do Radix era `w-fit`; o Select é `min-w-40` e cresce com o pai —
  quem passava `w-full` em form pode tirar.
- **`data-slot`**: `combobox-*` → `select-*` (`select-control`, `select-input`,
  `select-item`, `select-chip`, `select-clear`, `select-toggle-all`). Teste E2E que mirava
  `[data-slot="select-trigger"]` passa a mirar `[data-slot="select-control"]`.
- **Visual**: o campo não-buscável agora é um input readOnly (mesma altura e moldura de
  antes); o Radix abria a lista alinhada ao item selecionado, o novo abre ancorado no
  campo.

Os patterns da casa (`ActionForm`, `ActionList`, filtros, `Composer`) já vêm migrados —
`fieldOptions`/`filterOptions` agora tipam `SelectOption[]`.

## 2.45.0 — 2026-07-23

**SectionShell: nav redimensionável (`resizable`).** A divisa arrastável que o `rail` do
AppShell já tinha desce pro terceiro nível — quando os rótulos da nav variam de tamanho,
o `w-56` fixo ora aperta ora sobra. Opt-in (default segue a coluna fixa); ligado, a
largura vira % (`navDefaultSize` 18, `navMinSize` 12).

**O Combobox volta pra régua de altura da casa** — e ganha `size`. Ele era o único
controle de form em `min-h-10`: numa toolbar ao lado de Button/Select/Input (todos h-9)
ficava visivelmente mais alto, "pegando carona" no flex. Agora o default é **`min-h-9`**
(a altura do DS) e existe **`size="sm"`** (h-8) pra barra de filtros densa — o mesmo par
que `Select` e `NativeSelect` já tinham. **Sem breaking de API** (prop opcional), mas é
**mudança VISUAL**: todo Combobox encolhe 4px por padrão. Se alguma tela dependia dos
40px, passe `className` com a altura desejada.

## 2.44.0 — 2026-07-23

**Ícone DENTRO do campo, uniforme.** `Select` (via `SelectTrigger`), `NativeSelect` e
`Combobox` ganham o prop **`icon`** — um ícone leading dentro do controle, herdando
`size-4` + `text-muted-foreground` como o resto (mesma linguagem do `InputGroupAddon`,
que já cobria os `<Input>` de texto). Nasceu de uma tela real construída no Maestro que
ficou com os ícones (calendário/prédio/pessoa) **jogados ao lado** dos filtros porque os
selects não tinham onde pôr — n≥2 (todo filtro faz isso). **Sem breaking** (prop
opcional; sem `icon`, nada muda). Uso: `<NativeSelect icon={<Building2 />}>…`,
`<SelectTrigger icon={<CalendarDays />}>`, `<Combobox icon={<Ticket />} … />`. Pros
inputs de texto, siga usando `InputGroup` + `InputGroupAddon align="inline-start"`.

## 2.43.0 — 2026-07-23

**Terceira leva: os promovidos por reincidência.** `<Truncate>`, marshalling de `t.json()`
no repo, e o `readOnlyContextPool` — a candidatura de segurança do "SQL escrito por LLM"
(n=2 na casa) vira primitivo da base. **Sem breaking** (exports/comportamentos aditivos;
ver a nota de migração do t.json()).

- **`<Truncate>` (novo primitivo, `ui/react`).** Texto truncado com tooltip SÓ quando
  transborda (medição do overflow via ResizeObserver, re-medida em resize) — aposenta a
  composição `block truncate` + `title` sempre presente, que mostra dica até em texto
  que não corta. `tooltip` sobrepõe o conteúdo da dica (default: os children). Requer
  `TooltipProvider` na raiz (o esqueleto já monta). Promovido por reincidência (Ai.tsx
  do Maestro, duas tabelas).
- **`kyselyRepo` marshalla campo `t.json()` (objeto→jsonb, guiado pela declaração).**
  A dupla `ColumnType<unknown, string, never>` + `JSON.stringify` na mão morre no
  caminho do repo: objeto E array são serializados na escrita (insert/update). O caso
  traiçoeiro era o ARRAY — o pg stringifica objeto plano sozinho, mas array vira
  literal de array do PG (errado pra jsonb) sem quebrar typecheck. **Migração:** quem
  já passava STRING pré-serializada segue valendo (string passa direto — sem
  double-encode); quem usava o repo com objeto ganha o conserto de graça. Query à mão
  (fora do repo) continua responsável pelo próprio stringify.
- **`readOnlyContextPool` (`@softize/opus/data/readonly-pool`).** SQL escrito por LLM
  com as QUATRO defesas dos ADRs 0003/0007 — todas no BANCO, nenhuma em regex: pool com
  teto (`max` do pool e/ou `maxConcurrent` com fila própria), `BEGIN TRANSACTION READ
  ONLY`, `SET LOCAL ROLE` por transação (alcance do contexto; roles/grants
  default-fechado são migração sua) e protocolo estendido (o SQL roda sempre com array
  de valores — multi-sentença é recusada pelo protocolo). Saindo, `DISCARD ALL` devolve
  a conexão limpa; limpeza que falha DESTRÓI a conexão (nunca volta suja ao pool).
  `statement_timeout` local por transação (default 15s). Zero-dep: aceita qualquer
  pool pg-like (`connect()`), sem dependência direta de `pg`.

## 2.42.1 — 2026-07-23

Destrava a publicação da 2.42.0, que o gate do release barrou ANTES do registry (a
tag existe; a versão nunca publicou — **instale esta**). Mesmo conteúdo, mais o conserto:

- **Import órfão reprovava o smoke do esqueleto.** A ampliação do `useTriggerAction`
  deixou `SimpleContract` importado sem uso em `ui/drivers/react.tsx`. O tsconfig do
  PACOTE não cobra unused — o do app escafoldado cobra (`noUnusedLocals`), e como o
  Opus ship source, o tsc do consumidor compila a base: o smoke do release
  (`opus create` + typecheck) barrou o tarball, exatamente como devia. Import
  removido; o smoke roda limpo de ponta a ponta.

## 2.42.0 — 2026-07-23

> ⚠️ **Não publicou** — o smoke do release barrou o tarball (import órfão; o
> diagnóstico está na 2.42.1). O conteúdo abaixo vale e saiu na **2.42.1**.

**Segunda leva do 1º app real: UI composável + audit pronto pra dado sensível.** Fecha
quatro issues do consumidor (GB). **Sem breaking** (tipos ampliados e opções/exports
novos, todos aditivos).

- **`useTriggerAction` aceita QUALQUER contrato.** Era tipado só pra `SimpleContract`,
  mas roda qualquer um (o hook usa name/kind e lê `invalidates`) — disparar um
  FormContract fora de form (modal composto que submete `role.update`) exigia
  `as unknown as SimpleContract<...>`. Agora o parâmetro é `ActionContract<TInput,
  TData>`; o cast morre. Nenhuma chamada existente muda.
- **`useActionFormContext()` (novo export, `ui/react`).** A válvula de escape pra
  CONTROLE CUSTOM no modo composição do `<ActionForm>`: campo com UI própria (a grade
  de permissões que forçou abandonar o form inteiro) participa do form do contrato —
  `form.watch`/`form.setValue`/`formState.errors` — mantendo zod-resolver, erro inline
  e submit do contrato. Type `ActionFormContextValue` exportado junto.
- **Redator global plugável nos sinks de audit (`redact` em `pgAudit`/`consoleAudit`) +
  `redactDeep` (`@softize/opus/audit`).** O pgAudit persistia input E output CRUS —
  `user.create`/`setPassword` gravaria senha em claro; o `AuditConfig.redact` por action
  só cobre dot-paths estáticos. O `redactDeep` é o redator recursivo por nome de chave
  (default `/password|passwd|secret|token|authorization|api[-_]?key|credential/i`, em
  qualquer profundidade, arrays inclusos): `pgAudit({ pool, redact: redactDeep })` e o
  sink fica utilizável com dado sensível. `redactRecord` e `SENSITIVE_KEY_PATTERN`
  exportados pra sink próprio reusar.
- **`AuditRecord` carrega `actionKind` e `actor.meta` populado.** Sink que filtra
  leitura (kind list/view = ruído) decidia recebendo os DOMAINS por fora pra montar Set
  de nomes — agora o record diz o kind. E `actor.meta` (que existia no tipo mas nunca
  vinha) chega com `name`/`email` do user resolvido, quando presentes — só os dois
  campos de identificação; o resto do `User` (shape aberto) não vaza pro log.

## 2.41.0 — 2026-07-23

**O `opus check` volta a ser gate de verdade + a leva de correções do 1º app real.** Fecha
seis issues do consumidor (GB): o check enxerga o split contrato/bind e não passa mais
verde vazio, `requires` sem `authorize` vira violação, o `gen` projeta o `requires` fiel
e poda órfãos, o `create` tolera repo recém-iniciado e o template nasce com os providers.
**Sem breaking de API** — mas o check pode ficar VERMELHO onde antes passava vacuamente
(é o conserto; ver o 1º e o 2º bullets).

- **`opus check` enxerga `defineContract` + `bindAction`.** Era cego: só `defineAction`,
  e app refatorado pro split saía INTEIRO do radar ("Nenhuma action encontrada", exit 0 —
  gate vazio passando verde por 200+ arquivos). Contrato passa pelas mesmas regras
  (name/kind/ordem/`export const`); bind conta como action e cobra `export const`.
  ⚠️ Comportamento: **0 actions agora é exit ≠ 0** — gate sem nada pra checar não é
  aprovação; a mensagem diz o que ele procura (e acusa contrato sem bind no diretório).
  O hook `opus-check-on-stop` herda isso (bloqueia gate vazio).
- **Nova regra `requires-sem-authorize`.** `requires` é DECLARATIVO (o runtime 2.x não o
  executa — e dev que confia nele deixa a action ABERTA; aconteceu de verdade na
  graduação do reports). No `defineAction` acusa direto; no split, o join contrato↔bind é
  cross-file por identificador exportado — `authorize` no contrato OU no binding
  satisfaz; referência ambígua/não encontrada não acusa (sem falso-positivo). Runtime
  executar `requires` como authorize default segue em discussão; esta régua fecha o
  buraco sem mudar runtime.
- **`opus gen` poda órfãos.** `.md`/`.ts` que a rodada anterior gerou e esta não gerou
  saem de `docs/`, `client-stubs/` e `client-stubs/dicts/` — removido um domínio, a doc
  que o AGENTE lê parava anunciando endpoint 404. Só as extensões geradas nos dirs
  gerenciados; qualquer outro arquivo fica. O sumário lista o que podou.
- **`opus gen` projeta o `requires` FIEL no manifest.** `requires: ['sales','hr']`
  (semântica ANY) saía `"permission": "sales"` — a spec mentia por omissão. Agora lista
  sai lista (`permission: string | string[] | null`); a doc mostra
  `` `sales` | `hr` (qualquer uma) ``; os stubs já emitiam o JSON certo.
- **`opus create` tolera repo recém-iniciado.** Dir com só `.git` (e/ou README/LICENSE)
  não é mais "já existe e não está vazio" — `git init` + remote ANTES do scaffold é o
  ponto de partida normal de repo-cliente. Vale pro `--monorepo` e pro modo app; os
  templates não têm esses arquivos, então segue sem clobber por construção.
- **Template do app nasce com o trio de providers e favicon.** `QueryClientProvider` →
  `TbdlibProvider` → `TooltipProvider` no `main.tsx` (qualquer Tooltip da base explodia
  no 1º uso; os hooks de action exigem os outros dois) e `public/favicon.svg` + link no
  `index.html` (some o 404 do console).

## 2.40.0 — 2026-07-22

**Modo design: `fake`/`fakeMany` + split de reação.** Duas peças aditivas que fundam o preview
UI-only-com-dado — o `mockHandler`/`OPUS_MODE=design` (que já existia) agora tem com o que ser
alimentado — e deixam a REAÇÃO autorável como contrato. **Sem breaking** (só exports novos).

- **`fake(schema)` / `fakeMany(schema, n)` (`@softize/opus/testing`).** Dado sintético
  DETERMINÍSTICO (PRNG semeado, sem `Math.random` → preview reproduzível) que SATISFAZ o schema
  zod — matéria-prima do `mockHandler` no modo design e de fixtures de teste. Heurística por
  check do zod (email length-aware, uuid, url, datetime) e por nome de campo (id, data,
  telefone, cpf/cnpj, nome…), sabor pt-BR; respeita min/max/int/length/enum/literal (formato por
  NOME cede pro length quando há restrição).
- **Split `defineReactionContract` → `bindReaction` (`@softize/opus/core`).** Espelha
  `defineContract`/`bindAction` pras reações: a DECLARAÇÃO (`name`/`on`/`description`/`authorize`)
  é isomorfa e autorável no design; `handler` + entrega (`retry`/`timeout`/`dedup`/`concurrency`)
  vão no `bindReaction`, que produz uma `ReactionDef` normal (o runtime não sabe do split).
- **Docs.** `mockHandler`/`OPUS_MODE=design` e `fake` documentados (estavam implementados mas
  invisíveis). Nota: o `mockHandler` roda no server de design; tirá-lo do bundle web é transform
  deferido (o contrato é isomorfo — gate por `import.meta.env` estouraria no server).

## 2.39.0 — 2026-07-22

**Elicitação: o agente pergunta à pessoa no meio do turno, seguro por construção.** Fecha a
pegadinha vivida no chat do Maestro — o `AskUserQuestion` embutido do Claude Code, em modo
headless, volta VAZIO e a pergunta some pro usuário (o agente "infere sozinho"). O contrato +
o guard agora moram na base, então qualquer consumidor de IA do Opus nasce protegido. **Sem
breaking** (tipos/campos/exports novos, aditivos).

- **Contrato de pergunta (novos tipos, `@softize/opus`).** `AskQuestion` (enunciado + `header`
  + `options` + `multiSelect`), `AskOption` e `AskAnswer` (rótulos escolhidos + texto livre) —
  a forma única que todo mundo fala. Espelha o AskUserQuestion do Claude Code, mas servida por
  um round-trip REAL.
- **`AiRunOptions.onAsk` / `BoundAiRunOptions.onAsk` (novo hook).** O round-trip de "perguntar
  à pessoa": com ele, o driver INTERCEPTA a tool reservada `ask_user` (injetada automaticamente
  no catálogo) e chama o handler, devolvendo as respostas ao modelo. Passa direto por `ctx.ai`
  (o `bindAi` já espalha as opções).
- **Guard anti dead-end no driver `ai/anthropic` (`run` + `runStream`).** SEM `onAsk`, uma
  chamada a `ask_user` é RECUSADA com erro acionável ("pergunte em texto e siga") em vez de
  cair no `execute` do consumidor — que, num loop headless/one-shot (o caso dos copilots
  `services/ai`), resolveria contra o servidor MCP e morreria em silêncio. Segurança por
  construção: um ask-tool não vira dead-end silencioso.
- **Novos exports em `@softize/opus/ai`.** `ASK_USER_TOOL` (nome reservado), `ASK_INPUT_SCHEMA`
  (JSON Schema da tool), `ASK_TOOL_DESCRIPTION` e `formatAnswers()` — pra quem serve o tool por
  fora (ex.: o chat do Maestro, que roda via `claude` CLI e monta o `ask_user` como MCP).

## 2.38.0 — 2026-07-22

O **menu vira primitivo pivotável** (`<ShellNav>`), fechando o gap "cada app copia a sidebar
à mão" — Maestro, back-office e GB reescreviam o `<button>` do item de menu (3 produtos).
**Sem breaking** (componentes/props novos).

- **`<ShellNav>` (novo primitivo PIVOTÁVEL).** O menu — grupos → itens, com `heading` e
  âncora inferior (`footer`) — que serve os DOIS lares do chrome sem flag: na sidebar do
  `<AppShell>` recolhe pra ícone-só (com tooltip) quando a sidebar recolhe; dentro do
  conteúdo (num `<SectionShell>`) fica expandido. Sabe onde está pelo novo contexto de slot
  do AppShell (`useSidebarSlot`) — o app não passa o estado de rail em duas mãos (fecha a
  nota `n=2` que o `gb-nav-rail` deixou anotada). v1 PLANA; aninhamento/árvore + DnD COMPOSTO
  vêm quando o 1º consumidor de árvore migrar. Acompanha `<ShellNavHeading>` (título + slot
  de ação, o "+" de criar). Desenho em `docs/shellnav.md`.
- **`<AppShell>` publica o slot da sidebar (`useSidebarSlot`).** `useAppShell` não distinguia
  sidebar × conteúdo (o colapso vale a árvore toda); o novo contexto existe SÓ dentro dos
  slots da sidebar (header/nav/rodapé), `null` no conteúdo. É o que torna o `<ShellNav>`
  pivotável de verdade — e o que o `gb-nav-rail` previu virar prop do Opus quando n=2.
- **`<Composer>`: textarea plana também no escuro.** Faltava `dark:bg-transparent` — o
  `dark:bg-input/30` do primitivo `Textarea` vazava dentro da pílula, dando um fundo de input
  à área de escrita no tema escuro. Agora a textarea herda o `bg-card` do composer nos dois temas.

## 2.37.0 — 2026-07-22

Fecha o desenho da 2.36.0: os seletores da barra viram componente, e o `<Chat>` passa a
hospedá-los. **Sem breaking** (props novas, opcionais).

- **`<ComposerSelect>` (novo primitivo).** O seletor discreto que vive na barra de ações
  do composer: gatilho ghost (rótulo + chevron) que abre um menu com `Check` no ativo.
  Era o padrão que o **Maestro** escrevia à mão — DUAS vezes (app + task) — e que a GB ia
  copiar pro agente: a mesma receita (trigger + DropdownMenu + Check), agora uma vez só.
  Controlado (`value`/`onChange`), opções `{ value, label, hint?, triggerLabel? }` — `hint`
  é o detalhe à direita (a chave da task em mono) e `triggerLabel` encurta o gatilho quando
  o rótulo da lista é longo (a task mostra o título na lista, só a chave no gatilho). Pra um
  select de formulário (moldura/label), siga com `Select`/`Combobox` — este é calibrado pra
  barra: discreto, sem borda, some no fundo até o hover.
- **`<Chat composerActions>` — o chat também hospeda a barra.** A 2.36.0 pôs o slot
  `actions` no `<Composer>`, mas deixou o `<Chat>` "inalterado por fora": quem tinha um chat
  (não um composer nu) não alcançava a barra — o comentário mandava "usar o `<Composer>`
  direto", o que num chat significaria reimplementar o transcript. Agora o `<Chat>` repassa
  `composerActions` pro Composer interno. É o que faltava pro rail do Copilot da GB tirar o
  seletor de agente do slot `notice` (acima, deslocado) e pô-lo na barra, onde o Maestro já
  põe app + task. `notice` volta a ser só aviso; `composerActions`, só controle.

## 2.36.0 — 2026-07-21

Novo `<Composer>` — a caixa de escrever extraída do `<Chat>`, reusável sozinha.

- **`<Composer>` (novo primitivo).** O composer do `<Chat>` (textarea numa pílula elevada,
  Enter envia / Shift+Enter quebra linha, enviar dentro) virou componente próprio, pra usar
  ONDE HÁ ENTRADA DE TEXTO MAS NÃO UM CHAT — o caso do composer de criação de sessão do
  Maestro (sem histórico). Controlado (`value`/`onChange`/`onSubmit`); `submitDisabled` trava
  o enviar além de vazio/busy (ex.: falta escolher o app); `busy` vira spinner no enviar.
- **Slot `actions` — seletores discretos na barra.** Com `actions`, o composer vira duas
  linhas: textarea em cima, uma barra limpa embaixo com os controles à esquerda e o enviar à
  direita. É onde o Maestro prende app + task e a GB, o agente — o padrão que os apps
  resolviam cada um do seu jeito (GB no slot `notice` acima; Maestro à mão) agora tem lugar.
- **`<Chat>` inalterado por fora.** Ele passa a usar o `<Composer>` por dentro; sem `actions`,
  o composer é byte-a-byte o de antes. Única mudança visível: o enviar mostra spinner enquanto
  `busy` (era só desabilitado). Nenhuma migração.

## 2.35.1 — 2026-07-21

Correções da 2.35.0 (que saiu antes de passar pelo review). **Prefira esta à 2.35.0.**

- **`AppShell` recolhido não estreitava se o app passasse `sidebarClassName` com largura.** O
  `sidebarClassName` (ex.: `w-72`) vencia a largura do recolhido no `twMerge`, então recolher
  escondia o rótulo por CSS mas o aside ficava largo — meio-recolhido, sem erro. Agora a
  largura do recolhido é aplicada por último: `sidebarClassName` manda no expandido, o estado
  manda no recolhido (quem quer outra largura recolhida usa `sidebarCollapsedClassName`).
- **Doc e registro do rem, alinhados ao CSS.** O `tokens.md` ainda dizia "14px / ~12,5%"
  (contradizia o `theme.css`, que já publicava 15px), o comentário do `theme.css` contava uma
  história falsa (dizia "revert" — 15px é valor inédito, antes da 2.31.5 valia o 16px do
  browser) e a entrada da 2.31.5 tinha dois números derivados de uma baseline que nunca
  existiu. Tudo conferido no git e corrigido. Sem efeito em runtime — o rem segue 15px.

## 2.35.0 — 2026-07-21

Sidebar recolhível no `AppShell` e o **rem base passa a 15px**. *(Publicada com os defeitos
que a 2.35.1 conserta — use a 2.35.1.)*

- **Base do rem em 15px. Visual: GLOBAL — e o delta depende de onde você está.** Todo `rem`
  deriva daqui, então tipografia, espaçamento e tamanho de controle mudam em toda tela no
  bump. Confira sua origem:
  - **vindo de 2.31.5–2.34.2** (base 14px): tudo **cresce ~7,1%** (0.875rem: 12.25px → 13.125px);
  - **vindo de ≤ 2.31.4** (o pacote não fixava rem; valia o 16px do browser): tudo
    **encolhe ~6,25%** — direção oposta, não pule este bullet.

  Isto **não é um revert** da 2.31.5: 15px é valor inédito (antes dela não havia regra de
  rem alguma). O número veio da prática — a Empresa X escolheu 15px por conta própria em
  **dois** apps, e a exceção reincidente virou o default (régua da casa: promove na
  reincidência). Quem quiser a escala densa fixa `html { font-size: 14px }` no próprio entry.
  **Consumidor que já sobrescrevia pra 15px pode remover o override** — vira redundante, não
  conflitante (mesmo valor), então dá pra limpar sem pressa.
  <br>*Correção de registro:* a entrada da 2.31.5 saiu com dois erros (chamava 15px de "a
  escala anterior" e dizia "encolhe ~6,7%"), ambos por supor uma baseline de 15px que nunca
  existiu. Corrigidos lá, com nota.
- **`AppShell` ganha `collapsible` (opt-in).** Sem a prop, nada muda. Com ela, o shell
  controla a **largura** e publica o estado; **o que some é decisão do conteúdo**, porque o
  `sidebarNav` é do consumidor. Em vez de exigir estado no app, o aside expõe
  `data-collapsed` e o grupo `sidebar`: o rótulo some por CSS —
  `className="group-data-[collapsed=true]/sidebar:hidden"`. O botão é o novo
  **`<AppShellTrigger />`**, posicionado por você (some sozinho quando o shell não é
  `collapsible`, então dispensa condicional); **`useAppShell()`** dá o estado em JS. Pra
  persistir a preferência, controle de fora com `collapsed` + `onCollapsedChange`;
  `sidebarCollapsedClassName` ajusta a largura recolhida (default `w-14`).

## 2.34.2 — 2026-07-21

O `opus setup` passa a conhecer o Tailwind v4. **Sem breaking** (o caminho v3 é o mesmo).

- **Não nasce mais `tailwind.config.js` fantasma.** O setup criava o config sempre que o
  app tinha `index.html` e nenhum config — sem nunca olhar a versão do Tailwind. Na v4 não
  existe config (tema e escaneamento moram no CSS: `@import`, `@theme`, `@source`), então o
  arquivo nascia órfão: a build ignora, mas ele parece oficial pra quem abrir. Agora o
  setup lê a geração do consumidor e só semeia o config até a v3.
- **E o config que já nasceu agora é avisado.** Quem tomou o arquivo fantasma nas versões
  anteriores ouve, numa build v4, que ele é inerte e pode ser apagado. O setup não apaga
  nada — arquivo do projeto é do projeto —, mas parar de criar sem apontar o que já criou
  deixaria o problema no disco em silêncio. Config plugado de propósito com `@config` não
  é avisado, e a busca por esse `@config` varre **todos** os CSS do app, não só o de
  entrada: ele pode morar num parcial, e errar aí mandaria apagar um arquivo em uso.
- **Some o aviso falso do tema.** A checagem do `@softize/opus/ui/theme.css` olhava só o
  entry `.tsx`. App v4 importa o tema pelo CSS, então quem já fazia certo ouvia "importe o
  tema" a cada install — aviso que ensina a duplicar o que já está feito. Na v4 a
  checagem passa a olhar o CSS de entrada, e o CSS é **encontrado varrendo `src/`**, não
  casando uma lista de nomes: `src/styles/app.css` vale tanto quanto `src/index.css`, e
  entre vários vence o que importa o tema (num app com reset + entry, é esse que responde).
- **Ganha um aviso que faltava:** app v4 sem `@source` apontando
  `node_modules/@softize/opus/src/ui` renderiza cru, porque o Tailwind v4 não escaneia
  `node_modules`. Era o outro lado do `content` glob da v3, e ninguém avisava.
- **Detecção pela versão INSTALADA**, com o range declarado só como palpite de reserva. A
  pergunta real é "que Tailwind essa build roda?", e o range não responde: `catalog:` (o
  protocolo de catálogo do pnpm), `workspace:*`, `latest` e `*` não têm dígito nenhum, e
  `>=3` pode ter resolvido 4.x. Ler o range primeiro erraria bem no formato de monorepo
  pnpm que originou este achado. A busca **sobe a árvore** (`node_modules` da raiz, para o
  monorepo com hoisting), e `peerDependencies`/`optionalDependencies` entram na conta do
  fallback. Só quando não há Tailwind instalado **e** o range não tem número nenhum
  (`catalog:`, `workspace:*`, `latest`, `*`) o setup mantém o caminho v3 — aí seria palpite.
- Achado ao dar `postinstall: opus setup` aos apps de um monorepo pnpm: lá o `INIT_CWD` é
  a RAIZ, que não tem a dep, então o setup nunca rodava pros apps — e o `opus.json` de um
  deles envelhecia desde 2.28.0. Com o setup rodando de verdade, o defeito apareceu.
- 41 testes novos em `tests/init` — a fundação de UI não tinha nenhum.

## 2.34.1 — 2026-07-21

Conserta a regressão que a 2.34.0 introduziu no `DocBrowser`. **Suba da 2.33.x direto
para cá.**

- **`navigate` volta a notificar por evento.** Ele avisava só uma lista de assinantes em
  escopo de módulo, então quem escuta `popstate` na unha — a forma de todas as cópias que
  a primitiva substituiu — deixou de ser avisado. Agora dispara
  `new PopStateEvent('popstate')` no `window`. Como o `window` é um só, isso também faz
  duas cópias do pacote no `node_modules` se enxergarem; a lista privada não fazia.
- **Quem estava quebrado:** app que renderiza `<DocBrowser basePath="…" />` **sem** a prop
  `path` e guarda o path por conta própria. Clicar numa página trocava a URL e o corpo,
  mas o realce do menu do host congelava — sem erro, sem aviso.
- **O no-op do `navigate` passa a normalizar o destino** (`new URL(...).href` dos dois
  lados) em vez de comparar strings cruas. Isso conserta dois furos de uma vez: o **hash**
  (estando em `/a#secao`, `navigate('/a')` virava no-op e a âncora nunca saía) e o
  **encoding** — `window.location` devolve `/relatórios` percent-encodado, então o no-op
  nunca disparava para rota acentuada ou query com espaço, e cada clique repetido
  empilhava uma entrada. Era o caso pt-BR inteiro.
- Some a última cópia à mão do mecanismo, que tinha sobrado dentro do próprio
  `registry.tsx` (o redirect de `/docs/ui` pra `/ui` — agora `navigate('/ui', { replace:
  true })`, porque redirect não merece entrada no histórico).

## 2.34.0 — 2026-07-21

> ⚠️ **Regressão — use a 2.34.1.** A frase "nada existente muda de comportamento" logo
> abaixo é FALSA: o `DocBrowser` standalone parou de notificar o host. O diagnóstico e o
> conserto estão na 2.34.1. A entrada fica porque a versão foi publicada.

Roteamento vira primitiva da casa. **Sem breaking** (módulo novo; nada existente muda de
comportamento).

- **`navigate` / `usePathname` / `useSegments` / `useSearchParams`** (`ui/react`):
  history-based, sem dependência — o pathname É o estado, então deep-link, reload e o
  botão voltar funcionam sem um segundo lugar guardando "onde estou".
- Nasceu por **reincidência**, não por gosto: o mesmo mecanismo estava reimplementado à
  mão em quatro lugares, TRÊS deles dentro da casa (o `DocBrowser` daqui, o site do Opus
  e o back-office da Empresa X). O `DocBrowser` e o site já consomem a primitiva — a
  doc é o primeiro consumidor, como no `SectionShell`.
- As cópias divergiam no que importa: só uma usava `useSyncExternalStore` (as outras leem
  `window.location` em `useState`, que sofre **tearing** no modo concurrent), e o no-op de
  destino repetido — sem ele, clicar duas vezes no mesmo item empilha entradas idênticas e
  o "voltar" não sai do lugar — não estava em todas.
- **Escopo pequeno de propósito**: não há tabela de rotas, `<Route>`, params tipados nem
  data loader. A doc traz a régua de quando isto NÃO serve (casamento de padrão, params,
  carregamento por rota → o caso pede uma biblioteca de rotas).
- Compatibilidade: `subscribe` escuta `popstate`, então quem ainda notifica por evento
  sintético (`dispatchEvent(new PopStateEvent('popstate'))`) segue funcionando. ⚠️ A mão
  inversa é que faltava — ver 2.34.1.
- SSR: snapshot de servidor constante (`/`), reconciliado no 1º render do cliente.

## 2.33.0 — 2026-07-20

Conserta a regressão da 2.32.0 na nav da doc — **não use a 2.32.0** se você serve doc
por pasta. **Sem breaking** (as props novas são aditivas).

### O que quebrou na 2.32.0

Ao passar o `DocBrowser` pro `<SectionShell>`, o mapeamento achatou o tier
`DocGroup.label`. A justificativa registrada era falsa: o tier parecia morto porque o
catálogo hardcoded (`DOC_SECTIONS`) nunca o preenche — mas `docSectionsFromFolder` o
preenche a partir de sub-pasta (ou do frontmatter `group:`), que é o caminho do
`opusDocs({ source })`. **Sintoma:** projeto que serve `docs/` com sub-pasta perdia os
sub-cabeçalhos, e as páginas de subgrupos distintos viravam uma lista indistinguível.
A entrada da 2.32.0 diz "saída visual idêntica" — vale só pro catálogo do próprio Opus.

- **`SectionNavGroup.subgroups`** (novo): o 3º nível — rótulo mais fraco e mais
  indentado, renderizado DEPOIS dos `items` soltos do grupo. `items` virou opcional
  (quem já passava segue igual). O `DocBrowser` mapeia seção → grupo → subgrupo e a
  nav de 3 níveis volta ao que era.
- **`navLabel`** (novo, default `Navegação da seção`): rotula a landmark `<nav>`. Sem
  isso, um app com nav na sidebar + nav de seção anuncia "navigation" duas vezes e o
  leitor de tela não distingue.
- **`scrollResetKey` — a doc estava invertida.** Dizia pra passar "algo mais fino"
  quando a tela pagina no lugar, e o exemplo (`scrollResetKey={id}` com `id` = o
  `activeId`) era um no-op: paginar não muda o `activeId`, então o default já resolvia.
  Os dois usos reais estão documentados agora — chave CONSTANTE pra nunca remontar,
  chave MAIS FINA pra remontar dentro da mesma tela.
- Rótulo vazio (`''`) passa a contar como ausente no grupo e no subgrupo (antes saía um
  cabeçalho com padding e sem texto); a normalização saiu dos consumidores.
- `id` de item agora está documentado como único em TODA a nav — repetido, marca dois
  itens com `aria-current` e o painel não remonta ao alternar.
- Testes: regressão do tier de grupo no nível do `DocBrowser` (inclusive pelo caminho
  real do `docSectionsFromFolder`), os dois sentidos do `scrollResetKey`, subgrupos,
  rótulo vazio, `icon`/`badge`/`navHeader`/`navFooter` e a landmark rotulada.

## 2.32.0 — 2026-07-20

`<SectionShell>` — o nível que faltava entre o `AppShell` e a `Page`. **Sem breaking**
(componente novo; nada existente muda de comportamento).

- **`<SectionShell>` (ui)**: uma SEÇÃO com navegação própria — nav `w-56` com filete +
  painel — pras telas irmãs de Configurações, Relatórios ou doc. Controlado
  (`activeId` + `onSelect`): o roteamento é do app, mesma regra do `AppShell`. Trocar de
  item REMONTA o painel (é o que zera o scroll); `scrollResetKey` fixa a chave quando a
  mesma tela pagina no lugar. Itens aceitam `icon`, `badge` e `disabled`; o ativo ganha
  `aria-current="page"`.
- Nasceu por **reincidência**, não por gosto: o mesmo casco `nav + painel` já estava
  copiado à mão no `DocBrowser`, no site do Opus e no back-office da Empresa X — três
  vezes as mesmas classes. O `DocBrowser` agora **consome** o componente (a doc é o
  primeiro consumidor; saída visual idêntica — ⚠️ **falso**: regrediu a nav de quem
  serve doc por pasta, ver 2.33.0).
- A doc do pattern traz a régua que faltava: **quando é sidebar do app, quando é seção,
  quando é tab**. Sintoma de seção que devia sair da sidebar: o item de topo não tem
  tela própria e só existe pra abrir um accordion.

## 2.31.5 — 2026-07-17

**Base do rem em 14px** — a escala inteira densifica. **Visual: GLOBAL.** Todo `rem`
encolhe ~12,5% (0.875rem = 12.25px, e assim por diante), então tipografia, espaçamento e
tamanho de controle mudam em toda tela de todo consumidor no bump. Antes desta versão o
pacote não fixava rem: valia o 16px do browser. Projeto que quiser a escala larga
sobrescreve `html { font-size: 15px }` no próprio entry (é o que o back-office do Grand
Brasil faz).

> **Corrigido em 2.35.0.** Esta entrada é do lote reconstruído do git e saiu com dois erros,
> ambos derivados de supor uma baseline de 15px que nunca existiu: dizia "encolhe ~6,7%"
> (é 14/15; contra o 16px real são ~12,5% — o comentário no código sempre disse 12,5%) e
> chamava 15px de "a escala anterior" (a anterior era 16px; 15px é escolha da Empresa X).

## 2.31.4 — 2026-07-17

Blur do backdrop volta pro degrau `xs`: a escala do Tailwind v4 renomeou os degraus (o
antigo `sm` virou 8px), e o backdrop tinha herdado o valor errado no rename.

## 2.31.3 — 2026-07-17

Backdrop de modal mais claro, com blur; header do `Dialog` com o mesmo padding do corpo.

## 2.31.2 — 2026-07-17

Canvas quase-branco (tinte de 25% do muted) e bordas mais leves (92.5%) — a divergência
nº 5 da casa em relação ao stock do shadcn.

## 2.31.1 — 2026-07-17

`Dialog` se aproxima do `Card`: mesma forma (radius + 6px) e sombra pela metade.

## 2.31.0 — 2026-07-17

**Forma por PAPEL**: `rounded-card` / `rounded-popover` / `rounded-dialog`, na mesma
lógica dos tokens de elevação da 2.30.0 — papel novo ganha token novo. Entra junto a doc
do sistema de elevação.

## 2.30.2 — 2026-07-17

`shadow-card` menor na elevação de repouso (1px/3px).

## 2.30.1 — 2026-07-17

Idem 2.30.2 — degrau intermediário do mesmo ajuste.

## 2.30.0 — 2026-07-17

Elevação POR PAPEL + aresta — e os defaults do Tailwind de volta. **Visual: superfície
elevada troca a borda pelo anel; `shadow-*` cru volta a ser o stock do TW.**

- Tokens em namespace PRÓPRIO (sem pegadinha): `shadow-card` / `shadow-popover` /
  `shadow-dialog` (família Notion — curta+longa, com dark de verdade via vars) e
  `ring-edge`/`border-edge` (`--color-edge`: o anel hairline que SUBSTITUI a borda em
  superfície elevada — não combine com `border`). Papel novo = token novo (ex.: um
  futuro `shadow-input` nasce com demanda).
- A sobrescrita da escala `--shadow-*` (antiga divergência nº 4) foi APOSENTADA:
  `shadow-md` volta a significar exatamente o que a doc do Tailwind diz.
- Migrados: popover, select, dropdown-menu, context-menu, menubar, navigation-menu
  (`ring-edge` + `shadow-popover`); dialog, alert-dialog, sheet (`shadow-dialog`);
  `Card` (rounded-2xl + `ring-edge` + `shadow-card` — o flat aposentado); composer e
  card de fala do `<Chat>` (`shadow-card`).

## 2.29.5 — 2026-07-17

Degraus baixos da sombra com ALCANCE menor (geometria: xs 1px/4px, sm 3px/8px, base
5px/12px) — opacidade aprovada fica. **Sem breaking**; flutuantes intactos.

## 2.29.4 — 2026-07-17

Degraus baixos da sombra MAIS CLAROS (correção de rumo: xs 2.5%/3.5%, abaixo até do
original 3%/4%). **Sem breaking**; flutuantes intactos.

## 2.29.3 — 2026-07-17

Degraus baixos da sombra: mais um ponto de presença (xs 5%/7%, sm 5.5%/8%). **Sem
breaking** (só visual; flutuantes seguem intactos).

## 2.29.2 — 2026-07-17

Degraus BAIXOS da sombra mais presentes. **Sem breaking** (só visual).

- `2xs`/`xs`/`sm`/`shadow` sobem de opacidade (xs 3.5%/5.5%, sm 4%/6%…) — cards e
  superfícies apoiadas ficavam invisíveis sobre o canvas. `md`/`lg`/`xl`/`2xl`
  (flutuantes) NÃO mudam — calibração aprovada; progressão da escala preservada.

## 2.29.1 — 2026-07-17

O flush completa a promessa do shell transparente. **Visual: consumidores flush que
querem miolo branco pintam `bg-background` no próprio contêiner.**

- `<AppShell flush>`: o main deixa de pintar `bg-background` — o shell NÃO pinta nada
  (canvas do body em tudo; o filete `border-l` fica). Superfície é decisão do conteúdo.

## 2.29.0 — 2026-07-17

Conversa que começa pelo assistente. **Sem breaking.**

- `<Chat kickoff>`: no modo autogerenciado, roda uma vez no mount quando o transcript
  nasce vazio — o agente abre a conversa (proatividade: relatório recém-criado, sessão
  nova). Mesmo contrato do `send` (string ou stream de ChatEvent), sem mensagem de
  usuário; com `initialMessages` (histórico) não dispara. 2 testes.

## 2.28.0 — 2026-07-17

O canvas é do BODY; o shell fica transparente. **Sem breaking** (mesmo tom).

- `theme.css`: o body ganha o cinza clarinho da casa (`color-mix` de muted 40% sobre
  background — o tom que o AppShell pintava); overscroll/áreas fora do shell param de
  aparecer brancas, e o canvas segue o tema (light/dark).
- `<AppShell>`: perde o `bg-muted/40` da raiz (transparente) — as superfícies pintam
  `bg-background` por cima, como já faziam.

## 2.27.2 — 2026-07-17

Escala de sombra COMPLETA da casa. **Sem breaking** (só visual).

- Todos os degraus (`2xs`→`2xl`) redefinidos no theme.css com a curva leve de duas
  camadas (estilo Notion) — antes só `md`/`lg`; agora QUALQUER `shadow-*` em qualquer
  consumidor sai sem o peso default do Tailwind, preservando o sentido da progressão.

## 2.27.1 — 2026-07-17

Reset de borda no tema — bordas pretas em consumidor novo. **Sem breaking.**

- `theme.css` ganha o base layer canônico `* { border-color: var(--border) }` (o
  globals.css do shadcn): sem ele, todo consumidor NOVO do opus/ui nascia com bordas
  `currentColor` (pretas) até copiar o reset à mão no entry CSS. App com a cópia local
  segue funcionando (redundante, inofensivo — pode remover).

## 2.27.0 — 2026-07-17

Chrome flush no `<AppShell>` (padrão Vetra BI). **Sem breaking** (default inalterado).

- `flush` (prop): sidebar sem o padding do shell e conteúdo full-bleed — encosta no
  browser, com filete à esquerda (`border-l`) sobre `bg-background`. O chrome clássico
  (miolo em card sobre o muted) segue sendo o default.
- `<AppShellBar>`: a faixa h-12 com `border-b` — uma na sidebar (marca) e outra no topo
  do conteúdo (breadcrumb/ações); as alturas casam e a linha do header atravessa a tela
  inteira. Doc com a receita completa + 3 testes.

## 2.26.2 — 2026-07-17

Sombra dos flutuantes: calibração fina pra baixo (md 3%/7.5%, lg 4%/9.5%). **Sem
breaking** (só visual).

## 2.26.1 — 2026-07-17

Tabela larga rola nela mesma. **Sem breaking.**

- `<Markdown>`: a `<table>` ganha invólucro `overflow-x-auto` — tabela mais larga que o
  bloco (ex.: ranking no `<Chat>`) rola horizontalmente NELA, sem arrastar o scroll do
  chat/doc inteiro.

## 2.26.0 — 2026-07-17

Tabela GFM no `<Markdown>` (e, por tabela, no `<Chat>`). **Sem breaking.**

- O renderer nativo (zero-dep) ganha tabela GFM: linha de células `| a | b |` +
  separadora `|---|` viram `<table>` semântica (thead/tbody), células com inline
  (bold/code/link) — resposta de agente com ranking/tabela renderiza no chat. Linha
  com `|` sem separadora segue parágrafo.

## 2.25.2 — 2026-07-17

Calibrações de UI (3ª rodada no olho do dono). **Sem breaking** (só visual).

- Sombras `md`/`lg` um pouco mais opacas (md 3.5%/8.5%, lg 4.5%/10.5%) — meio-termo
  entre a 2.24.3 (pesada) e a 2.25.1 (leve demais).
- `DropdownMenuContent`/`ContextMenuContent` com `min-w` 10rem (era 8rem).

## 2.25.1 — 2026-07-17

Sombras dos flutuantes ainda mais leves (2ª calibração no olho do dono). **Sem
breaking** (só visual).

- `md` 3%/7% · `lg` 4%/9% — mesma expansão, menos peso.

## 2.25.0 — 2026-07-17

Form contract-driven ganha o campo de ícone. **Sem breaking.**

- `widget: 'icon'` no vocabulário de `fields`: string com o nome kebab-case da paleta
  da casa — o `ActionForm`/`ActionFormDialog` renderiza o `<IconPicker>` (2.24.0) no
  campo. Fecha o ciclo: o seletor de ícone entra no form declarado no contrato, sem
  form à mão.

## 2.24.3 — 2026-07-17

Sombras dos flutuantes mais leves. **Sem breaking** (só visual).

- `md`/`lg` com opacidade reduzida (camada curta 4–5%, longa 10–12%) — a 2.24.1 tinha
  ficado mais pesada que a referência (Notion); a expansão fica, o peso sai.

## 2.24.2 — 2026-07-17

Destructive dos menus: hover muda **só a cor**. **Sem breaking** (só visual).

- `DropdownMenuItem`/`ContextMenuItem` `variant="destructive"`: no hover/focus o fundo
  segue o accent NORMAL dos itens — só texto/ícone pintam de destructive (refinamento
  do 2.24.1, que tingia o fundo de vermelho).

## 2.24.1 — 2026-07-17

Elevação estilo Notion + destructive discreto. **Sem breaking** (só visual).

- Sombras `md`/`lg` viram duas camadas difusas e expandidas (curta pra descolar +
  longa pra flutuar) nos FLUTUANTES (menus, popovers, selects, dialogs) — divergência
  declarada nº 4 no `theme.css`. `xs`/`sm` ficam (superfícies apoiadas).
- `DropdownMenuItem`/`ContextMenuItem` `variant="destructive"`: NEUTRO em repouso —
  vermelho (texto/ícone/fundo) só no hover/focus, como o "Move to Trash" do Notion.
  `dropdown-menu` e `context-menu` viram **ejected** no lock (delta declarado).

## 2.24.0 — 2026-07-17

Seletor de ícone da casa. **Sem breaking.**

- `<IconPicker>` (ui): gatilho com o ícone corrente + lista buscável (Popover+Command,
  a receita do Combobox). O value é o **nome** do ícone (kebab-case) — renderize com a
  mesma paleta (`iconPickerIcons[name] ?? fallback`). Paleta default curada (~40,
  lucide); vocabulário próprio via prop `icons`; selecionar o corrente desmarca.
  Caso de uso: personalização de item criado pelo usuário (relatório, projeto, pasta).

## 2.23.0 — 2026-07-17

`<Chat>` em modo CONTROLADO. **Sem breaking** (o modo autogerenciado segue igual).

- `messages` (transcript por prop, `ChatTranscriptItem[]` com item `error`), `onSend`
  (retornar `false` devolve o texto ao composer), `busy` e `activity` (`undefined`
  esconde o indicador, `null` = "Pensando…", string = rótulo) — pro app dono do estado
  (transporte próprio, ex.: SSE server-autoritativo com replay, como o ChatRail do
  Maestro). `notice` renderiza avisos do app acima do composer.

## 2.22.1 – 2.22.4 — 2026-07-17

Absorção do design do ChatRail no `<Chat>` (patches; o design do Maestro virou o
default do componente):

- **2.22.1** — `greeting` vira estado vazio centrado (✦, some quando a conversa
  começa); era falsa 1ª fala do assistente e entrava no transcript do `send`.
- **2.22.2** — composer pílula flutuante: enviar dentro do campo, focus ring.
- **2.22.3** — transcript em turnos: mensagem do usuário em card com Markdown,
  cabeçalho do turno sticky; balão preenchido aposentado.
- **2.22.4** — linha do composer centralizada verticalmente (py-2 fecha os 36px do
  botão).

## 2.22.0 — 2026-07-17

Protocolo de eventos de conversa (streaming). **Sem breaking.**

- `ChatEvent` no core: `text{delta}` · `tool{name,detail?}` · `artifact{kind,ref,title?}`
  · `done{ok,error?}` — contrato em `docs/chat-event-protocol.md`.
- `runStream` aditivo no driver anthropic (token a token com `client.messages.stream`;
  fallback `create` = delta único) e no `BoundAi` (mesmo execute-como-usuário/confirm).
- `<Chat>` aceita os dois modos no `send`: `Promise<string>` (request/response) OU
  `AsyncIterable<ChatEvent>` (streaming) — tool vira indicador vivo humanizado
  (`humanizeTool`), artifact via `renderArtifact`, `initialMessages` reidrata (troque a
  `key` ao trocar de conversa).

## 2.21.0 — 2026-07-16

Chrome de aplicação. **Sem breaking.**

- `<AppShell>` (ui): sidebar (header/nav/footer) + conteúdo + rail opcional
  redimensionável (Resizable) — o esqueleto que admin, Maestro e Vetra BI copiavam à
  mão, agora slot-based (`sidebarHeader`/`sidebarNav`/`sidebarFooter`/`rail`).

## 2.20.1 — 2026-07-14

Controles flat, agora HONESTO: a sombra sai de DENTRO dos componentes, não por token
mentiroso. **Sem breaking, mesmo visual da 2.20.0.**

- Reverte o `--shadow-xs: 0 0 #0000` da 2.20.0 — redefinir um token de escala pra "nada"
  mentia sobre o que a variável contém. O token volta ao valor real do shadcn.
- `shadow-xs` removido de dentro dos 16 controles (button, input, select, textarea,
  checkbox, switch, radio-group, toggle, toggle-group, input-group, input-otp,
  native-select, calendar, menubar, button-group, combobox) — a intenção "flat" mora
  onde o componente é definido.
- Os 14 que eram byte-fiéis ao shadcn viram **ejected** (assumidos pela casa). O lock
  guarda a proveniência (`upstreamHash` = merge-base) pro re-sync futuro por diff.

## 2.20.0 — 2026-07-14

Controles **flat**: a sombra sutil (`shadow-xs`) dos controles zerada por token do tema.
**Sem breaking.**

- `--shadow-xs: 0 0 #0000` no tema — button, input, select (trigger), input-group,
  textarea, native-select e o toggle-group perdem a sombra, pareando com os Cards flat
  da casa e com o ToggleGroup "junto" (que já era flat). Divergência declarada vs shadcn.
- Superfícies elevadas (dialog, popover, dropdown, card — `shadow-md`/`lg`) NÃO mudam.

## 2.19.0 — 2026-07-14

Toolbar do `ActionList` em **altura default (36px)**, alinhada com a ação primária da
página. **Sem breaking.**

- Os controles da toolbar (período, filtros, busca, segment de views, recarregar,
  exibição) saem de `sm` (32px) pra `default` (36px) — mais respiro e legibilidade; a
  ação primária da página (o `Page` header) e a toolbar passam a ter o mesmo peso.
- Simplifica: a busca deixa de precisar do nivelamento manual (`h-8`/`h-full`) que o
  `sm` exigia (o `Input` não tem variante `sm`) — em default tudo alinha nativamente.

## 2.18.0 — 2026-07-13

Novo subpath **`@softize/opus/mcp`** — o MCP server de runtime. **Sem breaking.**

- `createOpusMcpServer(runtime, { resolveContext })`: expõe as actions `ai:enabled` como
  **tools MCP** (ListTools) e as executa via `runtime.execute` **como o usuário resolvido**
  (CallTool) — a porta pra uma IA de FORA alcançar o app pelo protocolo. Mesmo bridge do
  agente co-locado; o MCP é só o transporte (você monta stdio/HTTP).
- `runtime.aiTools()` agora é **público** — o bridge actions `ai:enabled` → tool specs,
  reusado pela cola do `ctx.ai` E pelo MCP server.

## 2.17.0 — 2026-07-13

Novo componente **`Chat`** (`@softize/opus/ui/react`) — **sem breaking**.

- `Chat`: a UI reutilizável do agente — lista de mensagens + composer, self-managed
  (estado, loading, auto-scroll; Enter envia, Shift+Enter quebra linha). A inteligência
  vem da prop `send`; no Opus, o backend liga numa rota que chama `runtime.aiFor(base).run`
  (o agente da 2.16 sobre as actions `ai:enabled`, como o usuário). Tipos: `ChatProps`,
  `ChatMessage`.

## 2.16.0 — 2026-07-13

Loop **AGÊNTICO** no recurso `ai` — as actions viram tools do modelo. **Sem breaking.**

- `AiAdapter` ganha `run?(input, { tools, execute, maxSteps })` (opcional, aditivo): o loop
  de tool-use multi-turn, PURO (o driver não conhece o registry — tools e execute vêm por
  DI). Implementado no driver `anthropic`.
- Uma action com `ai: { enabled: true }` no contrato vira uma **tool**. `ctx.ai.run(prompt)`
  — ou `runtime.aiFor(base).run(historico)` fora do handler — roda o agente sobre essas
  actions, executando-as **COMO O USUÁRIO** (limitado pelo `ctx.can`). O que é `destructive`
  ou pede `requiresConfirmation` não roda sem `run(prompt, { confirm })`.
- `ctx.ai` passa a ser `BoundAi` (complete/extract iguais; `run` com tools+execute
  pré-injetados). Novos tipos: `AiTool`, `AiMessage`, `AiRunOptions`, `AiRunResult`,
  `BoundAi`, `BoundAiRunOptions`.

## 2.15.0 — 2026-07-13

Novo componente de UI **`Copyable`** (nativo) — **sem breaking**.

- **`Copyable`** (`@softize/opus/ui/react`): clicar-pra-copiar com feedback — copia `value`
  pro clipboard e o ícone vira um check por ~1.5s (`feedbackMs` ajusta). Sem filhos é um
  botão-ícone (toolbar/célula); com filhos, o rótulo visível + o ícone. Pra IDs, tokens,
  slugs, URLs. No-op silencioso sem clipboard (contexto inseguro/SSR).
- Promovido pela régua da reincidência (o funil "Issues do Opus"): apontado como enhancement
  num projeto-cliente (issue #2/#4 em `softize-dev/opus`), aceito na triagem — sai da espera
  do `PROMOTED.md` pras Promovidas. Projeto que compunha um copiar-com-feedback à mão troca
  pelo import.

## 2.14.1 — 2026-07-12

Primeiro release do repo próprio do Opus (`github.com/softize-dev/opus`). Só correções
de doc/vocabulário desde 2.14.0 — **sem mudança de API**.

- Vocabulário "a base Opus" → "**o Opus**" no bloco gerenciado do CLAUDE.md e nas docs
  (o `opus setup` re-sincroniza o bloco; nada quebra); "camada-base" → "camada materializada".
- Correção de doc: o kind `search` (morto desde o rename pra `list`) sobrevivia em
  `actions.md`, `action-list.md` e na skill `create-action` (prosa + exemplos + `scaffold.mjs`)
  → tudo `list`.
- Doc self-contained: o exemplo de `bindAction` deixou de importar de pacote interno;
  `create-action` aponta a página Actions em vez de reproduzir a ordem canônica dos campos.

## 2.14.0 — 2026-07-11

Primeira rodada do funil de feedback da base: os 4 apontamentos do projeto-exercício
(`fieldnotes`, via `.opus/base-feedback.jsonl` → Radar) triados e resolvidos.

- Handler (e `authorize`) agora recebem o tipo PARSEADO do schema de input: campo com
  `.default()` deixa de chegar `| undefined` — remova os `?? fallback` redundantes.
- `ViewAction.projection` virou opcional: view simples sem expand não paga mais o
  boilerplate `projection: []`.
- `opus create`/`setup` semeiam as skills DO PACOTE (`registry/skills`) em
  `.claude/skills/` — esqueleto standalone não nasce mais sem o que o tarball carrega
  (untracked via `info/exclude`; o Maestro segue sendo o reconciliador). O bloco do
  CLAUDE.md agora diz o que só chega quando o Maestro rege.
- Átomos do catálogo em input de action: `t.slug().zod()` (e afins) documentado na
  página de actions e travado por teste como superfície pública — não re-escreva
  regex do que a base valida.

## 2.13.1 — 2026-07-11

- Fix visual no DocBrowser: as regras custom de `code`/`pre` do prose (theme.css)
  vazavam pra dentro de `not-prose` — o `<pre>` do CodeBlock ganhava uma segunda
  borda colada na do contêiner (a "borda duplicada", gritante no dark). Guard
  `[class~='not-prose']` espelhando o escopo do plugin typography.

## 2.13.0 — 2026-07-11

- `@softize/opus/testing` (EXPERIMENTAL): harness de teste do protocolo —
  `runAction` roda uma action em UNIDADE com o mesmo pipeline do runtime (valida
  input, cobra `public`/`authorize` com semântica idêntica incl. DSL e `loaded`,
  valida output, mesmos `ActionError`); `testContext` (ctx com emit/log capturados,
  `can` configurável) e `memStorage` (StorageAdapter em memória). O template do
  `opus create` já testa com ele.
- `ctx.ai` (EXPERIMENTAL): IA generativa no protocolo — `AiAdapter`
  (`complete`/`extract`) com driver `@softize/opus/ai/anthropic` (peer
  `@anthropic-ai/sdk` opcional, client injetável, default haiku). O diferencial do
  `extract`: o mesmo `Schema` das actions vira o contrato da resposta do modelo —
  saída estruturada forçada e VALIDADA pelo schema antes de devolver.
- Docs novas na surface SDK: "Testes de action" e "IA generativa"; a skill `test`
  da metodologia passa a transcluir a página do harness.

### Breaking

- `ActionContext`/`ReactionContext` ganharam `ai` — só afeta quem CONSTRÓI o
  contexto à mão: adicione `ai: null`, ou troque pra `testContext()` do novo
  `@softize/opus/testing` (que já vem completo e observável).

## 2.12.0 — 2026-07-11

- `opus create` ficou workspace-aware: dentro de um monorepo gera só os arquivos do app
  (nada de `.npmrc`/workspace yaml aninhado) e avisa o que a raiz precisa ter.
- `opus create <dir> --monorepo`: cria a RAIZ canônica de um workspace (apps/* +
  packages/*, allowBuilds da base, escopo do registry); o primeiro app vem de
  `opus create apps/<nome>` na raiz.

## 2.11.0 — 2026-07-11

- `ctx.storage` (EXPERIMENTAL): storage de arquivos no protocolo — `StorageAdapter`
  (put/get/delete/url) com drivers `@softize/opus/storage/fs` (disco local) e
  `@softize/opus/storage/s3` (S3-compatível; peers `@aws-sdk/*` opcionais).
- `opus db migrate` aplica o SCHEMA IDEMPOTENTE (config `schema`, script SQL evolutivo)
  e roda o drift-check entidade ↔ banco na sequência (exit ≠ 0 se divergir).

### Breaking

- **`opus db migrate` não roda mais migrations Kysely** (`migrations/*.ts` com
  `up`/`down`) e **`migrate down` foi removido**. Migração: converta o schema num
  script SQL idempotente (`CREATE IF NOT EXISTS` + guards `DO $$ IF EXISTS`), salve
  em `db/schema.sql` (ou aponte via `schema:` no opus.config.ts) e delete os
  `migrations/*.ts`. Rollback passa a ser: editar o script e re-rodar.
- `ActionContext`/`ReactionContext` ganharam `storage` — só afeta quem CONSTRÓI o
  contexto à mão (testes): adicione `storage: null`.

## 2.10.0 — 2026-07-10

- `opus create <dir>`: scaffold do app canônico (vite + react + tema v4 CSS-first,
  domínio-exemplo com gates verdes, dia zero completo, dev server na porta do preview).
- Template com `.prettierrc.json` (espelho executável da seção Formatação da skill
  code-style) + `format`/`format:check` no CI de fábrica.
- Correção de empacotamento: `@tailwindcss/typography` virou dependency (a theme.css
  o exige — consumidor standalone quebrava no build).

## 2.9.0 — 2026-07-10

- Hooks da base no registry (`registry/hooks`): `opus-check-on-stop` (gate do
  `opus check` no fim do turno, escopado aos apps alterados) e `link-memory-on-start`
  (liga a memória versionada do repo à sessão). Materializados pelo Maestro.
- `opus setup`: CLAUDE.md com BLOCO GERENCIADO (`<!-- opus:base -->…`) — o setup
  re-sinca o bloco quando a base evolui; fora dele o arquivo é seu. Dia zero:
  semente de `.claude/memory/` + CI de fábrica + aviso de script `test` ausente.

## 2.8.0 e anteriores

Sem changelog (a disciplina começou na 2.9.0). Referência: o histórico do monorepo.
