@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.
- package/CHANGELOG.md +82 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +293 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +1 -9
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +34 -15
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +29 -17
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +8 -35
- package/src/ui/components/patterns/surface-anatomy.ts +219 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/command.tsx +12 -4
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +138 -35
- package/src/ui/components/primitives/drawer.tsx +122 -23
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/docs/content/card.md +221 -30
- package/src/ui/docs/content/content.md +6 -0
- package/src/ui/docs/content/dialog.md +115 -7
- package/src/ui/docs/content/drawer.md +136 -9
- package/src/ui/docs/content/metric-card.md +57 -18
- package/src/ui/docs/content/page.md +8 -0
- package/src/ui/docs/content/pane.md +250 -0
- package/src/ui/docs/content/sidebar.md +22 -43
- package/src/ui/docs/content/skeleton.md +7 -3
- package/src/ui/docs/content/split.md +16 -21
- package/src/ui/docs/registry.tsx +8 -2
- package/src/ui/meta.ts +8 -2
- 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
|
@@ -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
|
-
? "
|
|
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
|
|
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
|
-
{/*
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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"
|