@softize/opus 21.0.1 → 22.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/cli.mjs +9 -1
  3. package/bin/lib/check.mjs +37 -29
  4. package/docs/adr/0004-page-content-state-is-composed.md +3 -2
  5. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
  6. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
  8. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
  9. package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
  10. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
  11. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
  12. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
  13. package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
  14. package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
  15. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
  16. package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
  17. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
  18. package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
  19. package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
  20. package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
  21. package/docs/radius-scale.md +5 -2
  22. package/package.json +5 -1
  23. package/registry/skills/build-opus-ui/SKILL.md +9 -6
  24. package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
  25. package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
  26. package/registry/skills/create-opus-action/references/contract-and-binding.md +5 -3
  27. package/registry/skills/create-opus-action/scripts/scaffold.mjs +0 -4
  28. package/registry/skills/test-opus-action/SKILL.md +2 -2
  29. package/registry/skills/test-opus-action/references/harness.md +2 -2
  30. package/registry/templates/app/src/App.tsx +1 -1
  31. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
  32. package/registry/templates/app/src/domains/tasks/actions/list.ts +3 -5
  33. package/registry/templates/app/vite.config.ts +3 -2
  34. package/src/core/domain.ts +2 -2
  35. package/src/core/types.ts +6 -20
  36. package/src/design/index.ts +179 -0
  37. package/src/testing/fake.ts +2 -2
  38. package/src/testing/index.ts +7 -3
  39. package/src/ui/components/patterns/content-header.tsx +51 -13
  40. package/src/ui/components/patterns/list.tsx +51 -33
  41. package/src/ui/components/patterns/page.tsx +139 -251
  42. package/src/ui/components/patterns/presentation.tsx +2 -6
  43. package/src/ui/components/primitives/button-group.tsx +80 -8
  44. package/src/ui/components/primitives/button.tsx +85 -60
  45. package/src/ui/components/primitives/detail.tsx +63 -57
  46. package/src/ui/components/primitives/dialog.tsx +8 -11
  47. package/src/ui/components/primitives/drawer.tsx +16 -12
  48. package/src/ui/components/primitives/tabs.tsx +123 -54
  49. package/src/ui/docs/content/actions.md +49 -43
  50. package/src/ui/docs/content/button.md +55 -29
  51. package/src/ui/docs/content/content.md +61 -36
  52. package/src/ui/docs/content/detail.md +28 -19
  53. package/src/ui/docs/content/drawer.md +53 -47
  54. package/src/ui/docs/content/input.md +51 -35
  55. package/src/ui/docs/content/page.md +84 -95
  56. package/src/ui/docs/content/pane.md +83 -63
  57. package/src/ui/docs/content/presentation.md +20 -20
  58. package/src/ui/docs/content/tabs.md +107 -36
  59. package/src/ui/docs/content/testing.md +9 -25
  60. package/src/ui/meta.ts +5 -5
  61. package/src/ui/react.tsx +0 -1
  62. package/src/vite/design.ts +133 -23
@@ -0,0 +1,71 @@
1
+ # ADR 0029 — PageHeader é a única introdução estrutural da página
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Substitui:** ADR 0011, quanto à barra persistente e à separação entre `PageHeader` e
6
+ `PageIntro`.
7
+ - **Relacionada:** ADR 0016, quanto ao cabeçalho próprio de coleções.
8
+
9
+ ## Contexto
10
+
11
+ `PageShell` separou a navegação e as ações globais numa barra persistente, enquanto `PageIntro`
12
+ manteve o título no conteúdo. A separação estabilizou a posição das ações, mas criou duas regiões
13
+ para identificar a mesma página. Nas rotas simples, a barra passou a repetir o item já selecionado
14
+ na navegação da aplicação. Nas rotas profundas, o breadcrumb ficou visualmente distante do título
15
+ que ele contextualiza.
16
+
17
+ Breadcrumb é uma exceção para hierarquias que a navegação persistente não revela. Ele não precisa
18
+ reservar uma faixa em todas as páginas, e uma página de primeiro nível não ganha contexto ao repetir
19
+ seu próprio nome acima do título.
20
+
21
+ ## Decisão
22
+
23
+ `PageHeader` é a única introdução estrutural de `Page`:
24
+
25
+ - o header participa da rolagem normal do conteúdo;
26
+ - `PageNavigation` ou `PageBack`, quando presente, ocupa uma linha acima da linha principal;
27
+ - `PageTitle` e `PageActions` compõem a linha principal;
28
+ - páginas sem hierarquia adicional não renderizam nem reservam espaço para navegação;
29
+ - `PageIntro` deixa de fazer parte da anatomia pública;
30
+ - `PageShell` pode coordenar navegação e ações conhecidas pela aplicação, mas não desenha uma barra;
31
+ esses valores são compostos pelo `PageHeader` da página ativa;
32
+ - `PageState` integral continua substituindo o header e fornece o único heading do estado;
33
+ - uma coleção pode continuar fornecendo o heading principal por `Content level={1}` sem criar um
34
+ `PageHeader` redundante.
35
+
36
+ ## Consequências
37
+
38
+ - breadcrumb, título e ações voltam a pertencer à mesma região visual;
39
+ - páginas simples começam diretamente pelo título;
40
+ - a altura inicial do conteúdo pode variar quando existe breadcrumb, porque a variação agora
41
+ comunica hierarquia real;
42
+ - consumidores combinam navegação, título e ações em um único `PageHeader` ao migrar da composição
43
+ explícita;
44
+ - `PageShell` deixa de garantir navegação durante estados integrais, pois o estado substitui a
45
+ introdução completa da página;
46
+ - a remoção de `PageIntro` é uma mudança incompatível e requer migração coordenada dos consumidores.
47
+
48
+ ## Alternativas descartadas
49
+
50
+ ### Manter a barra persistente somente quando houver breadcrumb
51
+
52
+ Evitaria a repetição em páginas simples, mas preservaria duas anatomias para o mesmo cabeçalho e
53
+ manteria a navegação distante do título nas páginas profundas.
54
+
55
+ ### Manter `PageIntro` como região opcional abaixo de `PageHeader`
56
+
57
+ Preservaria compatibilidade, mas continuaria exigindo que cada página decidisse qual das duas
58
+ regiões contém o título e as ações.
59
+
60
+ ### Copiar a composição no consumidor
61
+
62
+ Validaria uma tela isolada mais rapidamente, porém criaria um segundo contrato visual fora do
63
+ catálogo e não provaria que páginas declarativas e `Presentation` mantêm a mesma anatomia.
64
+
65
+ ## Verificação
66
+
67
+ - testes de `Page` cobrem breadcrumb acima da linha de título e ações;
68
+ - shorthand e composição explícita produzem a mesma anatomia;
69
+ - `PageShell` não renderiza barra e entrega navegação e ações ao `PageHeader` descendente;
70
+ - `PageState` integral mantém um único heading e remove o header;
71
+ - testes de `Presentation` cobrem a mesma composição dentro e fora de `PageShell`.
@@ -0,0 +1,89 @@
1
+ # ADR 0030 — Content com moldura pode usar anatomia seccionada
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Substitui parcialmente:** ADR 0023, nas afirmações de que `Content` não expõe `layout` e é
6
+ sempre contínuo.
7
+ - **Complementa:** ADR 0025.
8
+
9
+ ## Contexto
10
+
11
+ `Content` já representa uma região nomeada como `section` ou `fieldset`, com header, body e moldura
12
+ opcional. Quando o body é uma estrutura que possui o próprio recuo — como `DetailGroup` com
13
+ divisores, uma lista ou uma tabela — o acabamento contínuo acrescenta padding ao redor dessa
14
+ estrutura e impede que suas linhas alcancem a moldura.
15
+
16
+ O protótipo do detalhe de Veículo no Evento de Pós-vendas confirmou uma composição útil: header com
17
+ mídia, título, recuo de `0.75rem` e divisor; body sem recuo; conteúdo interno responsável por suas
18
+ células. Produzir esse desenho exigiu classes no consumidor para remover o padding da raiz, recortar
19
+ os cantos e reconstruir o header.
20
+
21
+ As forças em tensão são:
22
+
23
+ - reutilizar o vocabulário `continuous | sectioned` de Card, Dialog, Drawer e Pane;
24
+ - preservar `Content` como região semântica interna, sem transformá-lo em Card;
25
+ - manter `frameVariant="legend"` como acabamento da borda, sem misturá-lo à anatomia;
26
+ - impedir combinações em que o body encosta nas bordas sem existir uma moldura para recortá-lo.
27
+
28
+ ## Decisão
29
+
30
+ `Content` passa a aceitar `layout="continuous" | "sectioned"`, com `continuous` como default.
31
+
32
+ O layout `sectioned` exige `framed` e forma um pacote indivisível:
33
+
34
+ - A raiz desenha a moldura, recorta os cantos e não aplica padding geral.
35
+ - `ContentHeader` aplica `p-3` e o divisor inferior.
36
+ - `ContentBody` não aplica padding; seu filho direto responde pelo recuo interno.
37
+ - Shorthand e composição explícita produzem a mesma anatomia.
38
+ - `ContentMedia`, título, descrição, contador e ações preservam seus contratos atuais.
39
+
40
+ `frameVariant` continua escolhendo somente o acabamento da moldura contínua. A combinação
41
+ `layout="sectioned"` com `frameVariant="legend"` é inválida: o primeiro coloca o header dentro da
42
+ moldura, enquanto o segundo o integra ao traço externo.
43
+
44
+ O layout não altera a semântica. `as="section"` continua emitindo section com heading e
45
+ `as="fieldset"` continua emitindo fieldset com legend nativa.
46
+
47
+ ### Emenda de 2026-09-28 — a moldura usa o raio das superfícies estruturais
48
+
49
+ Quando `framed` está ativo, `Content` desenha uma moldura estrutural completa e usa `rounded-xl`,
50
+ o mesmo degrau de `Card`. A semântica das duas famílias continua diferente, mas essa diferença não
51
+ produz uma geometria externa menor. Em composições aninhadas, evitar a moldura dupla em vez de
52
+ reduzir globalmente o raio de `Content`.
53
+
54
+ ## Consequências
55
+
56
+ - DetailGroup, listas e tabelas podem ocupar o body de ponta a ponta sem classes estruturais no
57
+ consumidor.
58
+ - Conteúdo livre que precisa de recuo continua usando o layout `continuous`, ou declara sua própria
59
+ região interna quando o body de ponta a ponta for deliberado.
60
+ - `Content` passa a compartilhar o vocabulário de anatomia das demais superfícies, mas continua sem
61
+ footer e sem assumir o papel de uma superfície autônoma.
62
+ - A API rejeita `sectioned` sem `framed` e rejeita a combinação com `legend`.
63
+
64
+ ## Alternativas consideradas
65
+
66
+ ### Adicionar `frameVariant="sectioned"`
67
+
68
+ Foi descartado porque misturaria anatomia de regiões com o acabamento da linha externa. Card,
69
+ Dialog, Drawer e Pane já usam `layout` para essa decisão.
70
+
71
+ ### Criar propriedades de borda e padding
72
+
73
+ Propriedades como `headerDivider` e `bodyInset` permitiriam combinações arbitrárias e devolveriam a
74
+ anatomia ao consumidor. O resultado validado é um pacote, não dois controles independentes.
75
+
76
+ ### Criar DetailCard
77
+
78
+ Foi descartado porque duplicaria a semântica e os slots de `Content` para um único tipo de filho.
79
+ A anatomia também é válida para listas e tabelas.
80
+
81
+ ## Verificação
82
+
83
+ - Testes de UI cobrem shorthand, composição explícita, section e fieldset.
84
+ - Testes confirmam header com divisor e `p-3`, body sem padding e raiz com recorte.
85
+ - Testes confirmam `rounded-xl` nas molduras `default`, `legend` e `sectioned`.
86
+ - Testes reprovam `sectioned` sem moldura e a combinação com `frameVariant="legend"`.
87
+ - A documentação demonstra o caso com `DetailGroup` e diferencia anatomia de acabamento.
88
+ - O consumidor de Pós-vendas usa somente props públicas do Opus, sem classes que reconstruam a
89
+ anatomia.
@@ -0,0 +1,61 @@
1
+ # ADR 0031 — Divisórias de DetailGroup definem o tratamento tabular dos rótulos
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+
6
+ ## Contexto
7
+
8
+ `DetailGroup` separa três decisões: orientação dos pares, divisórias internas e moldura externa.
9
+ Apesar disso, o fundo sutil dos rótulos horizontais era ativado por `variant="framed"`. Assim, a
10
+ mesma grade com divisórias mudava de aparência ao ser hospedada por um `Content` seccionado, onde a
11
+ moldura pertence ao `Content`, e um grupo independente, onde a moldura pertence ao próprio
12
+ `DetailGroup`.
13
+
14
+ O fundo do rótulo é um acabamento da grade tabular: ele ajuda a distinguir chave e valor quando os
15
+ campos formam células conectadas. Não comunica que o grupo possui uma borda externa.
16
+
17
+ ## Decisão
18
+
19
+ Na orientação horizontal, `dividers` ativa a composição tabular completa de cada campo:
20
+
21
+ - rótulo e valor recebem padding próprio;
22
+ - a coluna de rótulos usa fundo sutil;
23
+ - a largura padrão dos rótulos passa a `8.5rem`;
24
+ - as divisórias continuam delimitando as células.
25
+
26
+ `variant="framed"` controla somente a moldura externa do grupo. Sem `dividers`, um grupo horizontal
27
+ emoldurado mantém rótulo e valor sobre o mesmo fundo. Na orientação vertical, `dividers` continua
28
+ adicionando somente as separações entre campos, sem pintar o rótulo.
29
+
30
+ ## Consequências
31
+
32
+ - Grades horizontais com divisórias têm a mesma aparência dentro de `Content layout="sectioned"`
33
+ e como bloco principal independente.
34
+ - Moldura externa, orientação e acabamento tabular permanecem decisões combináveis.
35
+ - Não é necessária uma nova prop apenas para reproduzir um enfeite que já acompanha a grade.
36
+ - Consumidores que usavam `framed + horizontal` sem divisórias deixam de receber fundo no rótulo.
37
+
38
+ ## Alternativas consideradas
39
+
40
+ ### Manter o fundo associado a `framed`
41
+
42
+ Preservaria o comportamento atual, mas continuaria fazendo a moldura externa alterar o interior da
43
+ grade e manteria inconsistentes os grupos hospedados por outra superfície.
44
+
45
+ ### Criar `labelVariant="muted"`
46
+
47
+ Tornaria o acabamento explícito, mas adicionaria uma decisão a todos os consumidores sem haver um
48
+ caso de produto que precise variar o fundo independentemente das divisórias.
49
+
50
+ ### Aplicar o fundo a toda orientação horizontal
51
+
52
+ Uniformizaria os rótulos, mas daria aparência tabular também a grupos simples, sem células ou
53
+ divisórias que sustentem essa leitura.
54
+
55
+ ## Verificação
56
+
57
+ - Testes de UI confirmam fundo e padding separados em `horizontal + dividers`, com e sem moldura.
58
+ - Testes de UI confirmam ausência de fundo em `horizontal + framed` sem divisórias.
59
+ - Testes de UI confirmam que divisórias verticais não pintam o rótulo.
60
+ - A documentação de `DetailGroup` descreve `framed`, `dividers` e `orientation` como eixos
61
+ independentes.
@@ -0,0 +1,59 @@
1
+ # ADR 0032 — A linha-base das abas respeita o recuo do contêiner
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Relacionada:** ADR 0023, quanto ao alinhamento horizontal dentro de superfícies.
6
+
7
+ ## Contexto
8
+
9
+ A variante horizontal `line` de `TabsList` estendia a linha-base para fora da própria lista usando
10
+ o recuo de `DrawerBody` e `DialogBody`. Os rótulos permaneciam alinhados ao conteúdo, mas a linha
11
+ alcançava as laterais da superfície e rompia o contorno visual formado pelo corpo.
12
+
13
+ A tela de detalhes do Evento tornou a divergência evidente: cards, detalhes e conteúdo respeitam o
14
+ mesmo recuo lateral, enquanto apenas a navegação por abas terminava nas bordas do drawer.
15
+
16
+ ## Decisão
17
+
18
+ A linha-base horizontal ocupa somente a largura de `TabsList`. Ela respeita o recuo aplicado pelo
19
+ contêiner e acompanha o alinhamento do corpo em qualquer superfície.
20
+
21
+ `DrawerBody` e `DialogBody` deixam de publicar uma variável de gutter exclusiva para Tabs. O recuo
22
+ continua pertencendo à anatomia da superfície, e Tabs apenas ocupa a largura que recebe no fluxo.
23
+
24
+ ### Emenda de 2026-09-28 — a aba combina uma área de botão e o indicador
25
+
26
+ Na variante horizontal `line`, o gatilho mantém duas camadas: uma área interna com os estados
27
+ interativos e a mesma altura de um Button do tamanho correspondente, e o indicador ancorado na
28
+ linha-base da lista. A área interna permanece transparente e recebe fundo apenas no hover; a aba
29
+ ativa se distingue pelo indicador primário. Um respiro de `0.375rem` separa as duas camadas no uso
30
+ comum; dentro de `Tabs framed`, ele aumenta para `0.5rem` e acompanha o padding superior da moldura.
31
+
32
+ Essa apresentação pertence ao Opus: consumidores escolhem `variant="line"` e não repetem classes
33
+ de fundo, raio, padding ou interação para obter o estado selecionado.
34
+
35
+ ## Consequências
36
+
37
+ - A linha e os rótulos das abas compartilham os limites horizontais do corpo.
38
+ - A seleção permanece indicada pela linha inferior, sem manter um segundo fundo persistente.
39
+ - Drawer e Dialog não mantêm uma integração implícita com Tabs.
40
+ - Um consumidor que realmente precise de uma linha até as bordas deve compor essa apresentação de
41
+ forma explícita, pois ela não é mais o comportamento padrão da variante `line`.
42
+
43
+ ## Alternativas descartadas
44
+
45
+ ### Manter a linha até as bordas
46
+
47
+ Preservaria o comportamento anterior, mas manteria uma exceção visual exclusiva das abas e uma
48
+ dependência oculta entre as primitivas de corpo e Tabs.
49
+
50
+ ### Adicionar uma propriedade de full bleed
51
+
52
+ Permitiria escolher os dois comportamentos, mas aumentaria a API para preservar uma apresentação
53
+ sem consumidor conhecido. A composição explícita é suficiente caso essa necessidade apareça.
54
+
55
+ ## Verificação
56
+
57
+ O teste da variante `line` verifica que a linha usa `inset-inline: 0`, inclusive quando Tabs está
58
+ dentro de `DrawerBody`. O mesmo teste verifica a separação estrutural entre a superfície interna e
59
+ o indicador da aba ativa. A documentação viva demonstra a variante com os limites do contêiner.
@@ -0,0 +1,72 @@
1
+ # ADR 0033 — Drawer usa uma moldura estrutural recuada
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Substitui:** a decisão da ADR 0005 que prendia a moldura externa do Drawer à borda da viewport.
6
+
7
+ ## Contexto
8
+
9
+ O `Drawer` ocupava toda a altura ou largura da viewport e desenhava somente a borda voltada para o
10
+ conteúdo. No consumidor, o shell principal já delimita as superfícies de trabalho com recuo de
11
+ `0.5rem`, borda completa, fundo base e raio `xl`. Ao abrir um Drawer sobre esse shell, o painel
12
+ voltava à geometria antiga de uma lâmina presa à janela: perdia os cantos, encostava em três bordas
13
+ e não parecia pertencer ao mesmo sistema de molduras.
14
+
15
+ O painel continua precisando comunicar de qual lado entra, preservar sua animação e aproveitar a
16
+ altura disponível. Ao mesmo tempo, a moldura não pode ser reconstruída por cada consumidor, porque
17
+ posição, recorte e animação pertencem à primitive.
18
+
19
+ ## Decisão
20
+
21
+ `DrawerContent` passa a ser uma superfície recuada da viewport em todos os lados:
22
+
23
+ - usa `0.25rem` de distância das bordas aplicáveis;
24
+ - usa borda completa, fundo base, `rounded-xl` e `overflow-hidden`;
25
+ - calcula a dimensão que antes ocupava `100%` descontando o recuo dos dois lados;
26
+ - mantém `side` responsável pela direção, dimensão principal e animação de entrada e saída;
27
+ - mantém largura e altura extensíveis por `className`, como antes.
28
+
29
+ O princípio durável é que uma superfície modal lateral possui moldura própria e não se confunde
30
+ com a viewport. O valor do recuo e o degrau de raio continuam materializações substituíveis da
31
+ escala visual vigente.
32
+
33
+ ## Consequências
34
+
35
+ - Drawer e shell passam a compartilhar a mesma leitura de moldura estrutural.
36
+ - Header, body e footer full-bleed são recortados pelos cantos do painel.
37
+ - A área útil do Drawer perde `0.5rem` no eixo que antes preenchia a viewport.
38
+ - Consumidores deixam de precisar adicionar borda, raio ou margem para obter essa composição.
39
+ - A mudança visual alcança `top`, `right`, `bottom` e `left`, evitando quatro geometrias distintas
40
+ para a mesma primitive.
41
+
42
+ ## Alternativas descartadas
43
+
44
+ ### Manter o Drawer preso à viewport
45
+
46
+ Preservaria a geometria histórica, mas manteria a divergência mais visível justamente quando o
47
+ painel aparece sobre um shell emoldurado.
48
+
49
+ ### Ajustar somente o Drawer do consumidor
50
+
51
+ Validaria a tela atual com menor alcance, mas espalharia posição e recorte por `className` e
52
+ deixaria outros Drawers fora do mesmo contrato visual.
53
+
54
+ ### Arredondar apenas os cantos expostos
55
+
56
+ Manteria o painel preso à borda de entrada, mas produziria uma moldura incompleta e continuaria sem
57
+ o respiro que distingue a superfície da viewport.
58
+
59
+ ### Alterar somente o lado direito
60
+
61
+ Resolveria o caso mais frequente, mas faria `side` trocar também o contrato visual da superfície,
62
+ sem uma razão de produto para essa diferença.
63
+
64
+ ## Verificação
65
+
66
+ - Testes de `DrawerContent` verificam borda completa, raio, recorte e recuo para os quatro valores
67
+ de `side`.
68
+ - Os testes existentes continuam cobrindo anatomia interna, fechamento e animação.
69
+ - A documentação do Drawer descreve a moldura recuada e continua demonstrando a extensão de
70
+ largura por `className`.
71
+ - Um teste do consumidor confirma que o Drawer de Evento usa o default do Opus, sem reconstruir a
72
+ moldura localmente.
@@ -0,0 +1,60 @@
1
+ # ADR 0034 — Tabs pode ter moldura sem criar uma superfície
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Relacionadas:** ADR 0023, quanto à anatomia das superfícies; ADR 0032, quanto à linha-base das abas.
6
+
7
+ ## Contexto
8
+
9
+ Uma composição em que a navegação por abas e seus painéis formam um único bloco precisa de borda,
10
+ raio e recorte compartilhados. Até aqui, consumidores obtinham essa forma envolvendo `Tabs` em um
11
+ `Card`.
12
+
13
+ O `Card`, porém, não é apenas uma moldura: ele aplica `bg-card text-card-foreground` e cria uma
14
+ superfície semântica própria. Em um `Drawer`, essa superfície pode se distinguir do
15
+ `bg-background`, especialmente no tema escuro, embora a intenção seja somente delimitar as abas
16
+ dentro da superfície já existente.
17
+
18
+ ## Decisão
19
+
20
+ `Tabs` aceita a propriedade booleana `framed`, desativada por padrão.
21
+
22
+ Quando ativa, `framed` aplica ao root a moldura estrutural canônica para esse bloco: borda, raio
23
+ `rounded-xl` e recorte do conteúdo. Ela não aplica token de fundo nem de foreground. O conjunto
24
+ herda a superfície do contêiner em que foi composto.
25
+
26
+ Essa propriedade não altera a apresentação de `TabsList`, a seleção, o tamanho nem a orientação. O
27
+ consumidor continua escolhendo esses comportamentos nas APIs existentes, inclusive
28
+ `TabsList variant="line"`.
29
+
30
+ ## Consequências
31
+
32
+ - Abas podem formar um bloco único sem serem promovidas a `Card`.
33
+ - Em Drawer, Dialog ou Pane, a moldura acompanha o fundo da superfície externa nos temas claro e
34
+ escuro.
35
+ - `Card` continua correto quando as abas realmente precisarem de uma superfície semântica própria.
36
+ - `framed` permanece independente de `TabsList variant`, que descreve apenas a apresentação da
37
+ lista.
38
+
39
+ ## Alternativas descartadas
40
+
41
+ ### Manter Card como wrapper obrigatório
42
+
43
+ Preservaria a API atual, mas continuaria acoplando uma necessidade estrutural ao token de superfície
44
+ do Card.
45
+
46
+ ### Aplicar classes no consumidor
47
+
48
+ Resolveria uma tela, mas repetiria borda, raio e clipping sem registrar o contrato visual da
49
+ composição.
50
+
51
+ ### Fazer toda Tabs ser emoldurada
52
+
53
+ Removeria a escolha, mas quebraria usos em que Tabs participa do fluxo de uma página ou toolbar sem
54
+ formar um bloco autônomo.
55
+
56
+ ## Verificação
57
+
58
+ Testes da primitiva verificam que o padrão permanece sem moldura e que `framed` aplica borda, raio e
59
+ recorte sem qualquer classe de fundo. O teste da view de Evento verifica que a composição usa o
60
+ root emoldurado diretamente, sem um `Card` intermediário.
@@ -0,0 +1,63 @@
1
+ # ADR 0035 — ButtonGroup separa moldura, relação e distribuição
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Relacionadas:** ADR 0015, quanto à escala dos controles; ADR 0017, quanto à distribuição igual explícita.
6
+
7
+ ## Contexto
8
+
9
+ Um conjunto de ações do Evento foi composto com `Card`, `ButtonGroup mode="spaced"`, separadores
10
+ manuais e `flex-1` repetido em cada botão. O resultado visual é um único grupo emoldurado, com
11
+ ações de mesma largura e divisores entre elas, mas o consumidor precisa reconstruir essas três
12
+ decisões em componentes e classes diferentes.
13
+
14
+ `ButtonGroup` já distingue a relação entre controles com `mode` e a ocupação da largura com
15
+ `distribution`. Tratar os divisores como uma propriedade booleana criaria combinações sem sentido,
16
+ como controles conectados e divididos ao mesmo tempo. Usar `Card` apenas como borda também cria uma
17
+ superfície semântica e um fundo que não pertencem ao grupo de ações.
18
+
19
+ ## Decisão
20
+
21
+ `ButtonGroup` passa a aceitar:
22
+
23
+ - `framed`, booleano independente que aplica borda, raio estrutural e padding sem definir fundo;
24
+ - `mode="divided"`, além de `connected` e `spaced`, para preservar a forma individual dos
25
+ controles e inserir automaticamente um divisor com respiro entre filhos adjacentes;
26
+ - `distribution="equal"`, já existente, para dividir o espaço disponível somente entre os
27
+ controles, sem incluir os divisores no cálculo.
28
+
29
+ O modo `divided` deriva a orientação dos separadores da orientação do grupo. Em grupos horizontais,
30
+ os divisores são verticais; em grupos verticais, são horizontais. `ButtonGroupSeparator` continua
31
+ público para composições conectadas ou divisões seletivas.
32
+
33
+ ## Consequências
34
+
35
+ - Consumidores expressam o conjunto em uma única raiz: `<ButtonGroup framed mode="divided"
36
+ distribution="equal">`.
37
+ - A moldura herda a superfície externa e não substitui `Card` quando uma superfície semântica for
38
+ realmente necessária.
39
+ - Os botões não repetem classes de largura, e separadores uniformes não são montados manualmente.
40
+ - `mode` continua sendo o único eixo que descreve a relação visual entre controles.
41
+
42
+ ## Alternativas descartadas
43
+
44
+ ### Manter Card e separadores explícitos
45
+
46
+ Preservaria a API, mas manteria no consumidor uma composição repetitiva que representa uma única
47
+ decisão visual.
48
+
49
+ ### Adicionar `dividers` como booleano
50
+
51
+ Permitiria ativar os traços, mas faria dois mecanismos independentes disputarem a relação entre os
52
+ controles e aceitaria combinações contraditórias.
53
+
54
+ ### Criar uma variante visual única
55
+
56
+ Uma variante como `variant="framed-divided-equal"` reduziria a chamada, mas acoplaria três eixos
57
+ que têm usos independentes e impediria novas combinações legítimas.
58
+
59
+ ## Verificação
60
+
61
+ Testes da primitiva verificam moldura sem fundo, divisores automáticos nas duas orientações e
62
+ distribuição igual restrita aos controles. O teste da view de Evento verifica que o grupo usa a API
63
+ canônica sem `Card`, `ButtonGroupSeparator` ou classes de distribuição nos filhos.
@@ -0,0 +1,58 @@
1
+ # ADR 0036 — Tabs declara a distribuição dos gatilhos
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-28.
5
+ - **Relacionadas:** ADR 0032, quanto à composição da variante `line`; ADR 0034, quanto à moldura de Tabs; ADR 0035, quanto ao eixo de distribuição em grupos de controles.
6
+
7
+ ## Contexto
8
+
9
+ Uma lista de abas pode abraçar a largura dos rótulos ou repartir a largura disponível igualmente
10
+ entre os gatilhos. A view de Evento usa quatro abas no cabeçalho de um bloco emoldurado; larguras
11
+ derivadas do texto deixam esse cabeçalho visualmente desequilibrado, embora todas as seções tenham o
12
+ mesmo nível.
13
+
14
+ Hoje, a variante horizontal `line` força cada gatilho a usar a largura do próprio conteúdo. Um
15
+ consumidor que precise de larguras iguais teria de sobrescrever o `flex` interno ou reconstruir a
16
+ lista com grid, duplicando uma decisão que já existe em `ButtonGroup` como
17
+ `distribution="content" | "equal"`.
18
+
19
+ ## Decisão
20
+
21
+ `Tabs` aceita `distribution="content" | "equal"`, com `content` como padrão. A propriedade fica no
22
+ root porque descreve o conjunto de abas e, como `size` e `orientation`, flui por contexto até a
23
+ lista e os gatilhos.
24
+
25
+ Na orientação horizontal, `equal` faz a lista ocupar a largura disponível e atribui a cada gatilho
26
+ a mesma fração flexível, com largura mínima zero. A área interativa interna ocupa toda essa fração,
27
+ incluindo o fundo de hover. `content` preserva o comportamento atual. A distribuição não altera a
28
+ largura nem o layout dos painéis de conteúdo.
29
+
30
+ ## Consequências
31
+
32
+ - Consumidores escolhem a mesma linguagem de distribuição usada por `ButtonGroup`.
33
+ - `TabsList variant="line"` continua responsável pela linha e pelos estados visuais; não recebe
34
+ classes de layout no consumidor.
35
+ - A view de Evento declara `distribution="equal"` no `Tabs` sem alterar cada `TabsTrigger`.
36
+ - Usos existentes preservam a distribuição pelo conteúdo.
37
+
38
+ ## Alternativas descartadas
39
+
40
+ ### Declarar a propriedade em TabsList
41
+
42
+ É a parte que organiza os gatilhos, mas faria a distribuição escapar do mesmo contrato de conjunto
43
+ em que `Tabs` já declara tamanho e orientação.
44
+
45
+ ### Usar grid e número de colunas no consumidor
46
+
47
+ Exigiria conhecer a quantidade de abas e repetiria classes de layout em cada tela.
48
+
49
+ ### Tornar larguras iguais o padrão
50
+
51
+ Atenderia a view de Evento, mas esticaria navegações curtas e barras que deliberadamente abraçam os
52
+ rótulos.
53
+
54
+ ## Verificação
55
+
56
+ Testes da primitiva verificam o default `content`, a largura total da lista e a fração igual dos
57
+ gatilhos quando `equal` está ativo. O teste da view de Evento verifica a propriedade no root e a
58
+ distribuição dos quatro gatilhos sem classes locais equivalentes.
@@ -0,0 +1,117 @@
1
+ # ADR 0037 — Dados do modo design ficam fora do contrato
2
+
3
+ - **Status:** aceita; a primeira etapa saiu na 22.1.0, e a remoção fica para a 23.0.0.
4
+ - **Data:** 2026-09-28.
5
+ - **Relacionadas:** ADR 0002 (seeds estruturados).
6
+
7
+ ## Contexto
8
+
9
+ O modo design serve para desenhar e revisar telas sem backend: `opusDesign()` monta o runtime
10
+ dentro do dev server, e cada action responde pelo `mockHandler` declarado no contrato. O preview de
11
+ sessão do Maestro depende desse modo e não tem outro propósito além de design.
12
+
13
+ Em setembro de 2026, o único consumidor, a Grand Brasil, tinha 233 `mockHandler` para 256
14
+ contratos, cerca de 3.200 linhas entre handlers, fixtures e estado simulado. A medição mostrou três
15
+ problemas:
16
+
17
+ - **O mock viaja com o contrato.** O contrato é a fatia compartilhada com a interface, então o
18
+ build de produção da aplicação leva os handlers simulados e suas fixtures. Um build de produção
19
+ da Grand Brasil continha textos das fixtures e os próprios handlers.
20
+ - **Metade dos mocks não acrescenta nada ao schema.** Cerca de 110 handlers devolvem um literal
21
+ fixo (`{ id: 'mock-x' }`) ou uma lista vazia, o que o `fake` já gera a partir do `output`.
22
+ - **A outra metade replica o domínio.** Cerca de 95 handlers simulam um CRUD em memória e
23
+ reimplementam regras do handler real, como o significado de cada filtro operacional de uma
24
+ listagem. Em setembro, 19 commits de funcionalidade tiveram de alterar mocks junto com o código
25
+ real, e nada garante que as duas versões da regra continuem iguais.
26
+
27
+ Forças em tensão:
28
+
29
+ - a aplicação real deve carregar o mínimo possível de algo que só existe para design;
30
+ - o preview precisa responder a qualquer action, inclusive às criadas durante a sessão;
31
+ - às vezes o design precisa de coerência entre respostas, como um item em cada situação ou algo
32
+ criado que aparece na lista em seguida;
33
+ - no preview, variar entre sessões é aceitável, porque dado de design não é evidência. Os seeds da
34
+ ADR 0002 são evidência e continuam determinísticos;
35
+ - o Opus é protocolo e SDK: onde os cenários ficam e se são versionados é escolha do projeto.
36
+
37
+ ## Decisão
38
+
39
+ 1. **O contrato deixa de declarar dados de design.** `mockHandler` sai de `ActionBase`, do
40
+ contrato, do `ActionDef` e do `runAction`. O runtime de produção não lê mais `OPUS_MODE` nem
41
+ tem ramo de design. Toda a lógica de design fica em `@softize/opus/vite`.
42
+ 2. **Toda action responde por padrão no modo design.** O plugin registra cada contrato com um
43
+ executor de design que, na ausência de cenário, devolve dado gerado pelo `fake` a partir do
44
+ `output`:
45
+ - `list` devolve três páginas geradas por `fakeMany`, com `items`, `cursor` e `total`, do
46
+ tamanho pedido em `limit` (20 quando a entrada não pede, até 100), e respeita a paginação
47
+ declarada. Busca e filtros não filtram o dado gerado, porque sobre dado
48
+ sorteado o resultado não mostraria nada útil; quando o filtro importar para o design, o
49
+ caminho é um cenário;
50
+ - a semente é derivada do nome da action e da entrada canônica, então a mesma requisição recebe
51
+ a mesma resposta;
52
+ - quando a entrada e a saída têm `id`, a saída ecoa o `id` da entrada, desde que o schema da
53
+ saída aceite esse valor, para que abrir um item listado mostre o mesmo identificador;
54
+ - uma action ligada a um handler real (`bindAction`) mantém esse handler, como hoje.
55
+ 3. **Cenários são registrados fora do contrato, pelo nome da action.** O plugin ganha a opção
56
+ `scenarios`, com um ou mais globs. Os módulos casados exportam cenários criados por
57
+ `defineDesignScenario(contrato, resposta)`, e a resposta pode ser um valor ou uma função tipada
58
+ pela entrada e pela saída do contrato. O cenário vence a resposta padrão. Um cenário para uma
59
+ action inexistente é erro visível no preview, não um silêncio.
60
+ 4. **O Opus não decide onde os cenários ficam.** A opção não tem default dentro do código da
61
+ aplicação. O consumidor escolhe, por exemplo, uma pasta ignorada pelo git que o agente da sessão
62
+ preenche sob demanda. Cenários começam descartáveis: só vale discutir versionar um cenário no
63
+ repositório do consumidor quando o mesmo for recriado em sessões diferentes.
64
+ 5. **Stubs de rotas externas continuam como estão.** A opção `stubs` segue atendendo superfícies
65
+ fora do protocolo de actions, como autenticação e motores externos.
66
+
67
+ ## Consequências
68
+
69
+ - A aplicação deixa de carregar mocks. O contrato volta a descrever só a capacidade, e o build de
70
+ produção não inclui dados de design.
71
+ - Uma action nova aparece no preview sem nenhuma linha de mock.
72
+ - O dado padrão tem o formato certo, mas não a regra de negócio: valores de enum são sorteados, e
73
+ campos de uma listagem e de um detalhe só coincidem no `id`. Quando isso atrapalhar o design, o
74
+ caminho é um cenário.
75
+ - Sem cenário, não há estado entre chamadas: criar um item não o faz aparecer na lista.
76
+ - É uma versão major. Quem declara `mockHandler` precisa remover a declaração ou transformá-la em
77
+ cenário, e testes que usam `OPUS_MODE=design` com `runAction` precisam de outra forma de
78
+ exercitar o contrato.
79
+ - O template de `opus create`, a documentação "Prototipar com dados simulados", as skills
80
+ `create-opus-action` e `test-opus-action` e o gate de ordem de campos (`check.mjs`) mudam junto.
81
+ - No Maestro, que fica fora deste pacote, o preview continua igual. O agente da sessão passa a
82
+ escrever cenários quando precisar, orientado por uma skill própria.
83
+
84
+ ## Alternativas descartadas
85
+
86
+ - **Manter o estado atual.** Preserva cenários prontos entre sessões, mas mantém o mock no bundle,
87
+ a regra de negócio duplicada e o custo de cada funcionalidade nova.
88
+ - **Remover `mockHandler` apenas no build de produção.** Resolve o bundle e nada mais. O contrato
89
+ continua misturando capacidade e simulação, e a duplicação de regra permanece.
90
+ - **Preview com backend real e dados dos seeds.** Mostra o comportamento verdadeiro e elimina os
91
+ mocks, mas exige banco, API e isolamento por sessão num preview que só serve a design, além de
92
+ rever as decisões do Maestro de manter o preview isolado e sem Postgres.
93
+ - **CRUD em memória genérico a partir de metadado novo no contrato.** Exigiria declarar entidade e
94
+ operação em cada action e, mesmo assim, não cobriria regras de negócio, que continuariam
95
+ escritas à mão.
96
+ - **Somente a resposta padrão, sem cenários.** É o mínimo absoluto, mas deixaria o design sem saída
97
+ quando a coerência importa. O custo do mecanismo de cenários é pequeno perto disso.
98
+
99
+ ## Transição
100
+
101
+ A saída acontece em duas versões. A 22.1.0 adiciona a resposta padrão e os cenários, com
102
+ `mockHandler` ainda aceito e precedendo a resposta gerada, e o `check` passa a contar o campo como
103
+ obsoleto. O consumidor remove seus mocks domínio a domínio. A 23.0.0 remove o campo e o ramo de
104
+ design do runtime.
105
+
106
+ ## Verificação
107
+
108
+ - testes do plugin: resposta padrão por `kind` (`view`, `list`, `form`, `simple`), páginas do
109
+ tamanho pedido em `limit` com a paginação declarada, mesma resposta para a mesma entrada, eco do
110
+ `id`, precedência do cenário e erro para cenário sem action ou duplicado;
111
+ - teste de tipos: `defineDesignScenario` infere entrada e saída do contrato e rejeita resposta
112
+ incompatível;
113
+ - busca estática: nenhum `OPUS_MODE` nem `mockHandler` em `src/core` e `src/testing` depois da
114
+ major;
115
+ - no consumidor: nenhum `mockHandler` nos contratos, e o build de produção sem textos das fixtures
116
+ antigas;
117
+ - typecheck, suíte do pacote, `opus check` e os gates de materialização verdes.