@softize/opus 18.5.0 → 19.0.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 (32) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +293 -0
  3. package/package.json +1 -1
  4. package/src/ui/components/patterns/content-header.tsx +1 -9
  5. package/src/ui/components/patterns/heading-level.ts +16 -0
  6. package/src/ui/components/patterns/list.tsx +34 -15
  7. package/src/ui/components/patterns/pane.tsx +294 -0
  8. package/src/ui/components/patterns/presentation.tsx +29 -17
  9. package/src/ui/components/patterns/sidebar.tsx +13 -72
  10. package/src/ui/components/patterns/split.tsx +8 -35
  11. package/src/ui/components/patterns/surface-anatomy.ts +219 -0
  12. package/src/ui/components/patterns/surface-header.tsx +51 -6
  13. package/src/ui/components/patterns/surface-media.tsx +69 -0
  14. package/src/ui/components/primitives/card.tsx +91 -29
  15. package/src/ui/components/primitives/command.tsx +12 -4
  16. package/src/ui/components/primitives/detail.tsx +10 -13
  17. package/src/ui/components/primitives/dialog.tsx +138 -35
  18. package/src/ui/components/primitives/drawer.tsx +122 -23
  19. package/src/ui/components/primitives/metric-card.tsx +98 -39
  20. package/src/ui/docs/content/card.md +221 -30
  21. package/src/ui/docs/content/content.md +6 -0
  22. package/src/ui/docs/content/dialog.md +115 -7
  23. package/src/ui/docs/content/drawer.md +136 -9
  24. package/src/ui/docs/content/metric-card.md +57 -18
  25. package/src/ui/docs/content/page.md +8 -0
  26. package/src/ui/docs/content/pane.md +250 -0
  27. package/src/ui/docs/content/sidebar.md +22 -43
  28. package/src/ui/docs/content/skeleton.md +7 -3
  29. package/src/ui/docs/content/split.md +16 -21
  30. package/src/ui/docs/registry.tsx +8 -2
  31. package/src/ui/meta.ts +8 -2
  32. package/src/ui/react.tsx +14 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,88 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 19.0.0 — 2026-09-24
11
+
12
+ Card, Dialog, Drawer e Pane passam a derivar a anatomia da mesma infraestrutura que Page e Content
13
+ já usavam. Cada superfície declara `layout="continuous" | "sectioned"` no seu componente raiz, e o
14
+ layout responde por um pacote: recuo das regiões, ritmo vertical, escala do título, posição das
15
+ ações, apresentação da mídia e divisores. O divisor passa a ser derivado da vizinhança das regiões —
16
+ um header seguido de corpo desenha a linha; um header sozinho, não.
17
+
18
+ Os headers passam a posicionar sozinhos o que antes o consumidor montava: mídia, título, descrição
19
+ (onde a família a tem), ações no extremo oposto e o botão de fechar depois delas. `Dialog`, `Drawer`
20
+ e `Pane` ganham slots de ações e de navegação; `Card` e `Pane` ganham slot de mídia; `Card` ganha
21
+ título com nível semântico.
22
+
23
+ A família `Pane` deixa de viver em dois módulos: raiz e slots agora moram juntos, com título,
24
+ mídia, ações, descrição e página própria na documentação.
25
+
26
+ `MetricCard` ganha ativação: com `onActivate`, `active` e `activationLabel`, a medida vira um
27
+ controle que aplica o recorte que ela resume — o card passa a emitir `aria-pressed` e a anunciar o
28
+ próprio nome. As três propriedades andam juntas pelo tipo; declarar `active` sem `onActivate` não
29
+ compila. A marcação de ativo é a borda na cor primária: antes era um anel desenhado para fora da
30
+ borda, que qualquer ancestral com `overflow-hidden` cortava. O anel de foco também passa a ser
31
+ interno.
32
+
33
+ Na mesma versão, a lista em `layout="fill"` troca a rolagem montada com um contêiner posicionado
34
+ por uma grade de duas linhas: os resultados ocupam a folga, o rodapé se mede pelo conteúdo, e o
35
+ contêiner da tabela abre mão da rolagem horizontal para não roubar do cabeçalho a referência do
36
+ `sticky`. O rodapé encosta na última linha e mantém a mesma altura com e sem paginador. O detalhe
37
+ aproxima o ícone do rótulo, na escala densa das superfícies produtivas.
38
+
39
+ ### Breaking
40
+
41
+ **Headers validam seus slots.** `CardHeader`, `DialogHeader`, `DrawerHeader` e `PaneHeader` aceitam
42
+ apenas os slots da família e no máximo um de cada. Nó solto dentro do header passa a lançar erro com
43
+ a lista do que é aceito. Migração: envolva ações em `CardAction`, `DialogActions`, `DrawerActions`
44
+ ou `PaneActions`; navegação em `DialogNavigation` ou `DrawerNavigation`; ícone em `CardMedia`,
45
+ `DrawerMedia` ou `PaneMedia`. Descrição dentro do header de modal não tem slot e vai para o começo
46
+ do corpo, como as ADRs 0014 e 0018 já determinavam.
47
+
48
+ **`CardTitle` virou heading.** Era um `div` sem escala tipográfica; agora emite `h3` por padrão,
49
+ aceita `level` para a hierarquia correta e declara a escala pelo layout. Migração: remova o heading
50
+ que você embutia dentro dele e o `text-*` que aplicava por fora; passe `level` quando a página pedir
51
+ outra hierarquia.
52
+
53
+ **Divisores saem do consumidor.** `<CardHeader className="border-b">` e
54
+ `<CardFooter className="border-t">` deixam de ser necessários: use `layout="sectioned"` no `Card`.
55
+ As classes continuam funcionando, mas somam uma segunda linha sobre a derivada.
56
+
57
+ **`PaneHeader` e `PaneFooter` ganharam recuo próprio** e o `Pane` passa a usar `inset="none"`
58
+ sozinho quando contém slots estruturais. Migração: remova o `inset="none"` e o padding que a tela
59
+ aplicava no header; remova também o `border-b-0`/`border-t-0` de quem desligava o divisor para
60
+ redesenhá-lo à mão.
61
+
62
+ **O rodapé do `Drawer` recebeu a faixa sutil do `Dialog`.** A ausência era divergência não declarada
63
+ entre duas superfícies com a mesma anatomia; agora as duas usam o mesmo fundo. O título do Drawer
64
+ também perdeu o `text-foreground` que só ele tinha.
65
+
66
+ **Títulos de modal passam a truncar.** `DialogTitle` e `DrawerTitle` mantêm o nome da superfície em
67
+ uma linha, com reticências quando não cabe, em vez de empurrar o corpo para baixo. O
68
+ `mode="alert"` é a exceção: ali o título é centralizado e pode ocupar duas linhas, porque a
69
+ resposta obrigatória não tem corpo para empurrar. As telas que já
70
+ escreviam `truncate` podem remover a classe. Quando o nome precisar aparecer inteiro, ele pertence
71
+ ao começo do corpo, não ao cabeçalho (ADR 0018).
72
+
73
+ **Mídia com visual próprio.** Os slots de mídia aceitam `asChild`: uma marca que já se desenha —
74
+ um orbe, um logotipo — ocupa o slot sem ganhar a moldura por cima, e continua herdando a escala.
75
+
76
+ **O rodapé do `Drawer` mantém o `mt-auto`** que o `Dialog` não tem. A diferença passa a ser
77
+ declarada: o conteúdo lateral raramente preenche a altura da janela, e sem isso o rodapé subiria
78
+ atrás do corpo.
79
+
80
+ **Divisor não se desliga por classe.** `border-b-0` no consumidor não apaga a linha derivada — ela
81
+ vem de uma variante condicional, que vence uma classe simples. Uma superfície sem divisores é uma
82
+ superfície `continuous`: troque o layout no root em vez de apagar a borda região a região.
83
+
84
+ ### Migração rápida
85
+
86
+ 1. Rode a aplicação e siga os erros dos headers: cada um diz o slot que falta.
87
+ 2. Remova `p-*`, `border-b`, `border-t`, `flex-row`, `justify-between` e `text-*` que existiam só
88
+ para corrigir o default da família.
89
+ 3. Troque `<Card>` + classes de divisor por `<Card layout="sectioned">`.
90
+ 4. Onde a tela remontava header com `div`, use os slots — eles agora posicionam sozinhos.
91
+
10
92
  ## 18.5.0 — 2026-09-22
11
93
 
12
94
  `Content` passa a concentrar estrutura e apresentação de blocos com cabeçalho e corpo: aceita
@@ -0,0 +1,293 @@
1
+ # ADR 0023 — Superfícies compartilham uma anatomia e dois layouts
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-23.
5
+ - **Complementa:** ADRs 0005, 0014, 0018, 0019 e 0021.
6
+
7
+ ## Contexto
8
+
9
+ A ADR 0005 estabeleceu a gramática `Root > Header + Body + Footer` para as superfícies da casa. O
10
+ vocabulário foi adotado, mas a implementação não: `Page` e `Content` derivam o header de uma
11
+ infraestrutura interna comum, enquanto `Card`, `Dialog`, `Drawer` e `Pane` desenham o seu à mão.
12
+
13
+ O efeito acumulado é mensurável no código de hoje:
14
+
15
+ - **Três escalas de padding para o mesmo papel.** `p-3` no chrome de Page, `p-4` nos slots de Dialog
16
+ e Drawer, `px-5` no Card. `Card` é a única com ritmo vertical assimétrico (`pt-5 pb-3` no header,
17
+ `pt-3 pb-5` no footer). `Content`, `PageBody` e `PaneBody` não têm padding nenhum.
18
+ - **`PaneHeader` sem padding e `PaneFooter` com `p-2`** — assimetria dentro da mesma família.
19
+ - **`CardTitle` é um `div` sem escala tipográfica**, único título do conjunto que não é heading e
20
+ não declara tamanho; herda o que vier do contexto.
21
+ - **Divisor automático em quatro famílias e manual no Card.** A documentação pública ensina o
22
+ consumidor a escrever `border-b` no `CardHeader` e `border-t` no `CardFooter`.
23
+ - **`Dialog` e `Drawer` não têm slot de ações no header.** Quem precisa injeta nós soltos ao lado do
24
+ título, como `Presentation` faz hoje.
25
+ - **A família `Pane` está partida**: a raiz mora em `split.tsx`, os slots em `sidebar.tsx`, a
26
+ documentação está na página da Sidebar e nenhum teste cobre a anatomia.
27
+ - **`Drawer` duplica `Dialog`** com três divergências não declaradas: footer sem fundo, header sem a
28
+ supressão condicional do divisor e título com `text-foreground` a mais.
29
+ - **Nenhuma suíte transversal** compara as famílias; cada divergência só aparece quando alguém
30
+ compõe as duas superfícies lado a lado.
31
+
32
+ A evidência que motivou a decisão veio da tela do Evento de Pós-vendas: para aproximar dois Cards do
33
+ recorte já usado por Dialog e Content, a tela precisou escrever `flex-row items-center gap-4
34
+ border-b p-4` no header, `p-4` no body, `text-sm` no título, um `<h3>` dentro do `CardTitle` e o
35
+ ícone dentro do texto. São cinco decisões estruturais tomadas pelo consumidor.
36
+
37
+ As forças em tensão:
38
+
39
+ - **Uniformidade × identidade da família.** Um modal, um card e um painel não precisam ter as mesmas
40
+ medidas; precisam ter a mesma gramática e o mesmo comportamento estrutural.
41
+ - **Compatibilidade × correção.** Alterar o default de uma família muda todas as telas que já a
42
+ usam. Preservar o default errado perpetua o defeito que motivou a decisão.
43
+ - **Uma API genérica × seis APIs específicas.** Um `<Surface>` público reduziria código e apagaria a
44
+ semântica de cada família; seis implementações independentes preservam a semântica e produzem a
45
+ divergência atual.
46
+
47
+ ## Decisão
48
+
49
+ ### 1. Uma infraestrutura interna, seis fachadas públicas
50
+
51
+ As telas continuam usando `CardHeader`, `DialogHeader`, `DrawerHeader`, `PaneHeader`,
52
+ `ContentHeader` e `PageHeader`. Não existe superfície genérica pública. `SurfaceHeader` e o novo
53
+ módulo de anatomia permanecem internos: a aplicação não os importa.
54
+
55
+ A anatomia compartilhada passa a viver em um único módulo interno, que responde por:
56
+
57
+ - as classes de cada região (`header`, `body`, `footer`) por família e layout;
58
+ - a escala visual do título, separada do nível semântico;
59
+ - a posição e o tamanho das ações;
60
+ - a apresentação da mídia;
61
+ - os divisores entre regiões adjacentes;
62
+ - o alinhamento horizontal comum entre as regiões da mesma superfície.
63
+
64
+ Nenhuma família reimplementa essas regras. Quando uma família precisa divergir, a divergência é um
65
+ valor declarado na tabela de anatomia, não uma classe escrita no componente.
66
+
67
+ ### 2. O layout é do root e chega aos slots por contexto
68
+
69
+ `layout` é propriedade do componente raiz: `Card`, `DialogContent`, `DrawerContent` e `Pane`. Os
70
+ slots não recebem `layout`; eles leem o contexto da superfície que os hospeda. Um slot usado fora da
71
+ sua família continua sendo erro, como hoje.
72
+
73
+ `Content` e `Page` não expõem `layout` de anatomia. `Content` é contínuo por definição e `Page`
74
+ mantém `layout="flow" | "fill"`, que descreve ocupação de altura, não anatomia. São nomes
75
+ diferentes para coisas diferentes e permanecem assim.
76
+
77
+ ### 3. O que cada layout significa
78
+
79
+ `continuous` e `sectioned` não são a ausência e a presença de uma borda. Cada um é um pacote:
80
+
81
+ | Aspecto | `continuous` | `sectioned` |
82
+ | --- | --- | --- |
83
+ | Divisores | nenhum | entre regiões adjacentes presentes |
84
+ | Ritmo vertical | header aproxima-se do corpo; o espaço separa | regiões simétricas; o divisor separa |
85
+ | Escala do título | a da família | a da família, um degrau mais contida quando a família tem duas |
86
+ | Fundo do footer | igual ao da superfície | sutil, quando a família declara |
87
+ | Alinhamento horizontal | idêntico entre header, body e footer | idêntico entre header, body e footer |
88
+
89
+ O alinhamento horizontal é invariante dos dois layouts: o recuo lateral de header, body e footer de
90
+ uma mesma superfície é sempre o mesmo valor. É o que a suíte transversal verifica.
91
+
92
+ ### 4. Os divisores são derivados, não escritos
93
+
94
+ No layout `sectioned`, o divisor existe quando há duas regiões adjacentes. Um header seguido de nada
95
+ não desenha linha; um footer que é a única região também não. A regra é estrutural e vive no
96
+ componente, derivada da presença dos irmãos — não uma classe que o consumidor aplica.
97
+
98
+ A recomendação de escrever `border-b` e `border-t` no Card sai da documentação.
99
+
100
+ ### 5. Defaults por família
101
+
102
+ Os defaults preservam o que cada família já entrega hoje, exceto onde o comportamento atual é o
103
+ defeito que esta decisão corrige:
104
+
105
+ | Família | Default | Por quê |
106
+ | --- | --- | --- |
107
+ | `Card` | `continuous` | é o que a família entrega hoje: sem divisores, ritmo assimétrico |
108
+ | `Dialog` (default) | `sectioned` | já tem divisores e faixas; o default nomeia o que existe |
109
+ | `Dialog` (`mode="alert"`) | composição própria | a resposta obrigatória mantém sua anatomia centrada |
110
+ | `Drawer` | `sectioned` | idem Dialog |
111
+ | `Pane` | `sectioned` | header e footer já têm divisor |
112
+ | `Content` | contínuo, sem opção | região semântica dentro de outra superfície |
113
+ | `Page` | contínuo, sem opção | a separação do chrome pertence ao shell |
114
+
115
+ Nenhum default muda de valor efetivo com esta ADR. O que muda é que o valor passa a ter nome,
116
+ implementação única e teste.
117
+
118
+ ### 5.1. O que o inventário dos consumidores mostrou
119
+
120
+ A migração foi decidida sobre 374 usos reais das seis famílias na plataforma. O que eles revelam:
121
+
122
+ - **Nenhuma tela usa `CardDescription`, `ContentDescription`, `DialogDescription` ou equivalente.**
123
+ Toda descrição é um `<p className="text-sm text-muted-foreground">` escrito à mão. Quando esse
124
+ parágrafo está dentro de um `DialogHeader`, ele contraria a ADR 0018 e precisa ir para o corpo na
125
+ migração — não ganhar um slot novo no header.
126
+ - **`Card` é usado como caixa sem slots em várias telas**, com o padding escolhido por elas (`p-3`,
127
+ `p-5`, `px-3 py-2`). O root do Card continua sem padding próprio: quem não usa slots continua
128
+ decidindo. O padding pertence aos slots, como hoje.
129
+ - **Os dois `PaneHeader` do produto escrevem quase a mesma linha** (`flex h-[3.75rem] items-center
130
+ … p-3 … border-b-0`) em contextos diferentes. É a prova de que padding e divisor pertencem à
131
+ primitiva.
132
+ - **Um comentário no código do login documenta o defeito**: o `py-3` do `CardBody` "conta com um
133
+ CardFooter, que o login não tem". O ritmo do layout contínuo precisa reagir à ausência do rodapé.
134
+ - **Três testes de consumidor contradizem a unificação** e precisam de decisão explícita, não de
135
+ ajuste mecânico: um proíbe `p-4` no Card, outro exige `p-0` no `DrawerBody`, outro proíbe
136
+ `ContentHeader` em uma seção. Os dois primeiros fixam contornos; o terceiro fixa uma escolha de
137
+ composição que continua válida.
138
+
139
+ ### 6. Slots por família
140
+
141
+ A anatomia é comum; o conjunto de slots continua sendo decisão de cada família, porque decisões
142
+ anteriores já os delimitaram:
143
+
144
+ | Slot | Card | Content | Page | Dialog | Drawer | Pane |
145
+ | --- | --- | --- | --- | --- | --- | --- |
146
+ | Media | sim | não | não | sim (`DialogMedia`) | sim | sim |
147
+ | Title | sim | sim | sim | sim | sim | sim |
148
+ | Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
149
+ | Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
150
+ | Actions | sim | sim | sim | sim | sim | sim |
151
+ | Body | sim | sim | sim | sim | sim | sim |
152
+ | Footer | sim | não | sim | sim | sim | sim |
153
+
154
+ As ADRs 0009, 0014 e 0018 não são reabertas: Page, Dialog e Drawer continuam sem descrição no
155
+ header, e Page continua sem contador. `Dialog`, `Drawer` e `Pane` ganham o slot de ações que hoje
156
+ não têm — a ausência obrigava a injetar nós soltos ao lado do título, e o `Studio` a manter três
157
+ anatomias paralelas para a mesma entrada declarativa. O botão de fechar continua sendo o último
158
+ elemento da linha; as ações da tela ficam antes dele.
159
+
160
+ `Pane` ganha também título e mídia. Hoje ele não tem nenhum dos dois, e as duas telas que precisam
161
+ de um cabeçalho remontam a linha com `span`, incluindo o ícone dentro do texto.
162
+
163
+ `Content` permanece sem footer: é uma região nomeada dentro de outra superfície, e um rodapé
164
+ pertence à superfície que a hospeda.
165
+
166
+ ### 7. Título: hierarquia e tamanho são independentes
167
+
168
+ O nível semântico vem de `level` (`1`–`6`) ou de `asChild`, e nunca do tamanho visual. O tamanho vem
169
+ da família e do layout. `CardTitle` deixa de ser um `div` sem escala e passa a declarar as duas
170
+ coisas, com `h3` como default — um card é uma seção dentro de uma página que já tem `h1`.
171
+
172
+ ### 8. Mídia reutiliza a moldura existente
173
+
174
+ O slot de mídia usa `IconTile` ou a mesma infraestrutura visual dele. Nenhuma família cria outra
175
+ escala de molduras.
176
+
177
+ Quando a marca já tem visual próprio — um orbe, um logotipo —, `asChild` deixa o slot ceder o
178
+ elemento para ela: a escala continua vindo da mesma fonte, e a moldura sai de cena em vez de ser
179
+ desenhada por baixo.
180
+
181
+ `DialogMedia` é a exceção declarada: no modo de alerta a mídia não acompanha uma linha de título,
182
+ ela é o elemento de reconhecimento da resposta, e mantém a moldura maior e centrada. A exceção vale
183
+ só nesse modo, deriva da mesma infraestrutura das demais e é coberta por teste. Fora do alerta, o
184
+ `DialogMedia` é a mesma moldura das outras famílias.
185
+
186
+ ### 7.1. O Pane aceita conteúdo livre; as demais não
187
+
188
+ `Card`, `Dialog`, `Drawer`, `Page` e `Content` sempre nomeiam alguma coisa: seus headers aceitam só
189
+ os slots da família e reprovam nó solto, com uma mensagem que lista o que é aceito.
190
+
191
+ `Pane` é a superfície genérica — a coluna que hospeda navegação, marca, busca ou chrome que não é
192
+ título nem ação. Seu header aceita conteúdo livre ao lado dos slots, e esse conteúdo ocupa o mesmo
193
+ espaço do heading, de modo que as ações continuam indo para o extremo oposto sem classe no
194
+ consumidor. A permissividade é dele, não uma brecha geral: forçar a marca de uma barra lateral a
195
+ virar "título" seria uniformidade sem semântica.
196
+
197
+ O conteúdo livre é renderizado **dentro** da região de heading, depois do título e da descrição,
198
+ qualquer que seja a ordem no JSX. Ele herda dali a largura que o heading absorve, o que é o que
199
+ permite às ações continuarem no extremo oposto — mas também significa que uma busca colocada no
200
+ header aparece **abaixo** do título, não ao lado dele. Um header que precise de algo antes do
201
+ título usa o slot de mídia; um que precise de algo ao lado usa o de ações.
202
+
203
+ ### 8.1. Nível semântico em todas as fachadas
204
+
205
+ Todo título de superfície aceita `level`. Nos modais, onde a primitiva de acessibilidade emite o
206
+ elemento, o nível troca a tag do mesmo nó por composição, nunca aninha um heading dentro de outro, e
207
+ não altera a escala. Sem `level`, vale o `h2` que a primitiva já emitia.
208
+
209
+ ### 9. `Pane` deixa de ser uma família partida
210
+
211
+ Raiz e slots passam a viver no mesmo módulo, com validação de pertencimento, documentação própria e
212
+ os mesmos testes das demais famílias. `split.tsx` continua dono do redimensionamento; `sidebar.tsx`
213
+ continua dono da navegação. A fronteira deixa de ser acidental e passa a ser declarada.
214
+
215
+ ### 10. Compatibilidade
216
+
217
+ Aliases depreciados existentes (`CardContent`, `PaneContent`) seguem seu plano de remoção; esta
218
+ decisão não cria novos.
219
+
220
+ Classes de recuo, ritmo e escala que o consumidor já aplica continuam funcionando: `className` é
221
+ mesclado por último e vence. Uma tela que ainda escreve `p-4` no `CardHeader` não quebra, apenas
222
+ deixa de precisar.
223
+
224
+ **Os divisores são a exceção, e é uma exceção real.** Eles nascem de variantes condicionais
225
+ (`:not(:last-child)`), que têm especificidade maior que uma classe simples: `border-b-0` no
226
+ consumidor não desliga a linha derivada. Desligar o divisor de uma região isolada não é, portanto,
227
+ uma decisão do consumidor — é decisão da superfície, e se expressa trocando o layout dela para
228
+ `continuous`. A paleta de comandos é o caso dentro do próprio SDK: ela é contínua, e não um
229
+ seccionado com a primeira linha apagada.
230
+
231
+ Pela mesma razão, a regra que suprime o divisor entre um header e um rodapé adjacentes precisa
232
+ vencer a que o cria — não basta vir depois dela na folha, porque a ordem das variantes não é nossa.
233
+
234
+ ## Consequências
235
+
236
+ - Uma mudança de espaçamento, divisor ou escala passa a ter um lugar só; hoje tem seis.
237
+ - Card ganha um modo que a aplicação já construía à mão, e a documentação deixa de ensinar o
238
+ contorno.
239
+ - Dialog e Drawer ganham ações no header; `Presentation` deixa de injetar nós soltos.
240
+ - Pane deixa de ser exceção não testada.
241
+ - Consumidores que aplicavam classes de correção podem removê-las; quem não remover continua
242
+ funcionando, o que permite migração incremental.
243
+ - O custo é concentrado: quase toda a superfície visual do SDK muda de implementação ao mesmo tempo,
244
+ e a rede de proteção precisa existir antes da migração, não depois.
245
+ - Uma família que precise divergir passa a precisar declarar a divergência na tabela e cobri-la por
246
+ teste. Isso é deliberado: torna a diferença uma decisão, não um acidente.
247
+
248
+ ## Alternativas descartadas
249
+
250
+ ### Manter cada família com seu header
251
+
252
+ Preservaria compatibilidade absoluta e evitaria o custo de migração. Foi descartada porque é o
253
+ estado atual: produziu três escalas de padding, um título sem escala, divisores manuais em uma
254
+ família e a documentação pública ensinando o contorno.
255
+
256
+ ### Expor um `<Surface>` genérico
257
+
258
+ Reduziria o código a uma implementação e uma API. Foi descartada porque apagaria a semântica de cada
259
+ família — um card, um modal e um painel deixariam de se distinguir no código da tela — e porque o
260
+ ganho real está na infraestrutura interna, que este ADR unifica sem expor.
261
+
262
+ ### Unificar também as medidas entre famílias
263
+
264
+ Um único recuo para todas as superfícies eliminaria a tabela de anatomia. Foi descartada porque as
265
+ famílias têm densidades legítimas diferentes: o chrome de página é mais compacto que um card, e um
266
+ modal tem largura controlada. O invariante que importa é o alinhamento dentro de cada superfície,
267
+ não a igualdade entre superfícies diferentes.
268
+
269
+ ### Tratar `layout` como propriedade de cada slot
270
+
271
+ Permitiria um header seccionado com um corpo contínuo. Foi descartada porque o layout é a relação
272
+ entre as regiões: um divisor depende de quem está do outro lado. Propriedade por slot permitiria
273
+ compor superfícies incoerentes sem erro.
274
+
275
+ ### Derivar os divisores por CSS no consumidor
276
+
277
+ Manteria o componente menor. Foi descartada porque devolve ao consumidor a decisão que esta ADR
278
+ retira dele, e porque a regra depende de presença de irmãos, que o componente conhece e a tela
279
+ não.
280
+
281
+ ## Verificação
282
+
283
+ - Uma suíte transversal parametrizada percorre as famílias configuráveis e verifica, para cada
284
+ layout: alinhamento horizontal idêntico entre header, body e footer; divisor apenas entre regiões
285
+ adjacentes presentes; escala do título independente do nível semântico; header com e sem mídia,
286
+ descrição e ações; ações no extremo oposto sem classe no consumidor; body sem header; header sem
287
+ footer; footer com e sem ações.
288
+ - A mesma suíte verifica que `Page` e `Content` não expõem `sectioned` e que nenhuma família
289
+ reimplementa as classes de anatomia localmente.
290
+ - Testes de família continuam cobrindo o que é específico de cada uma.
291
+ - Exemplos vivos no catálogo apresentam a matriz de layouts e slots.
292
+ - `opus check` continua reprovando slots fora da família.
293
+ - Um teste de API pública reprova a exportação de `SurfaceHeader` ou do módulo de anatomia.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "18.5.0",
3
+ "version": "19.0.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,6 +10,7 @@ import {
10
10
  type ReactNode,
11
11
  } from "react";
12
12
  import { cn } from "../../lib/cn.ts";
13
+ import { headingTags } from "./heading-level.ts";
13
14
  import { usePageHeadingPresence } from "./page-heading-context.tsx";
14
15
  import {
15
16
  SurfaceHeader,
@@ -291,15 +292,6 @@ export function ContentHeader({
291
292
  );
292
293
  }
293
294
 
294
- const headingTags = {
295
- 1: "h1",
296
- 2: "h2",
297
- 3: "h3",
298
- 4: "h4",
299
- 5: "h5",
300
- 6: "h6",
301
- } as const;
302
-
303
295
  export function ContentTitle({
304
296
  className,
305
297
  id,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Nível de heading (interno): hierarquia do documento, separada do tamanho visual.
3
+ *
4
+ * A ADR 0023 fixa a regra: `h1`–`h6` expressam hierarquia; a escala do título vem da
5
+ * família e do layout. As duas decisões nunca se misturam num mesmo valor.
6
+ */
7
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
8
+
9
+ export const headingTags = {
10
+ 1: "h1",
11
+ 2: "h2",
12
+ 3: "h3",
13
+ 4: "h4",
14
+ 5: "h5",
15
+ 6: "h6",
16
+ } as const satisfies Record<HeadingLevel, `h${HeadingLevel}`>;
@@ -2144,8 +2144,11 @@ export function ActionList<TInput, TItem>({
2144
2144
  variant="plain"
2145
2145
  className={tableStateActive ? "h-full" : undefined}
2146
2146
  containerClassName={
2147
+ // Quem rola é a linha de resultados, acima: aqui o contêiner abre mão da
2148
+ // rolagem horizontal para não virar a referência do `sticky` do cabeçalho.
2149
+ // `h-full` só no estado sem linhas, para a moldura vazia ocupar a altura.
2147
2150
  tableFillsAvailableHeight
2148
- ? "absolute inset-0 overflow-auto"
2151
+ ? cn("overflow-x-visible", tableStateActive && "h-full")
2149
2152
  : undefined
2150
2153
  }
2151
2154
  >
@@ -2290,7 +2293,14 @@ export function ActionList<TInput, TItem>({
2290
2293
  ) : null;
2291
2294
 
2292
2295
  // — Rodapé (o estilo da casa): Página X de Y à esquerda · pager CENTRALIZADO ·
2293
- // total à direita (grid de 3 colunas com laterais 1fr = centro geométrico) —
2296
+ // total à direita (grid de 3 colunas com laterais 1fr = centro geométrico).
2297
+ // Encosta na última linha: a borda de cima é o fecho da tabela, não a de um
2298
+ // bloco solto, então o rodapé não entra no `gap` da pilha.
2299
+ // Altura: 2.5rem de piso (a mesma do `TableHead`) para a faixa não mudar de
2300
+ // tamanho quando o pager aparece — ele ocupa 1.75rem e cabe dentro do piso.
2301
+ // O padding fica em 0.25rem só para o dia em que o conteúdo passar disso:
2302
+ // com `py-2` o padding empurraria o pager para fora e a faixa voltaria a
2303
+ // crescer —
2294
2304
  const pages =
2295
2305
  total !== undefined
2296
2306
  ? Math.max(1, Math.ceil(total / (state.limit ?? pageSize)))
@@ -2300,7 +2310,7 @@ export function ActionList<TInput, TItem>({
2300
2310
  total !== undefined && !isError ? (
2301
2311
  <div
2302
2312
  data-slot="action-list-footer"
2303
- className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1.5 text-xs text-muted-foreground"
2313
+ className="grid min-h-10 grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1 text-xs text-muted-foreground"
2304
2314
  >
2305
2315
  <span>
2306
2316
  Página {state.page} de {pages}
@@ -2368,19 +2378,28 @@ export function ActionList<TInput, TItem>({
2368
2378
  {toolbar}
2369
2379
  {beforeContent}
2370
2380
  {selectionBar}
2371
- {/* Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
2372
- <div
2373
- aria-busy={busy}
2374
- className={cn(
2375
- layout === "fill" && "relative flex min-h-0 flex-1 flex-col",
2376
- layout === "fill" &&
2377
- (tableFillsAvailableHeight ? "overflow-hidden" : "overflow-auto"),
2378
- busy && items.length > 0 && "pointer-events-none opacity-60",
2379
- )}
2380
- >
2381
- {body}
2381
+ {/* Tabela e rodapé formam um bloco só: o `gap` da pilha separa a barra de
2382
+ ferramentas do conteúdo, nunca o conteúdo do seu próprio rodapé. Duas
2383
+ linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
2384
+ pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
2385
+ tem altura própria e ela vira a altura do conteúdo. */}
2386
+ <div className={cn("grid min-h-0 grid-rows-[1fr_auto]", layout === "fill" && "flex-1")}>
2387
+ {/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
2388
+ cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
2389
+ essa referência e o cabeçalho voltaria a subir junto com as linhas.
2390
+ Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
2391
+ <div
2392
+ aria-busy={busy}
2393
+ className={cn(
2394
+ "min-h-0",
2395
+ layout === "fill" && "overflow-auto",
2396
+ busy && items.length > 0 && "pointer-events-none opacity-60",
2397
+ )}
2398
+ >
2399
+ {body}
2400
+ </div>
2401
+ {footer}
2382
2402
  </div>
2383
- {footer}
2384
2403
  {confirming !== null && (
2385
2404
  <Dialog
2386
2405
  mode="alert"