@softize/opus 24.0.0 → 24.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ 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
+ ## 24.1.0 — 2026-10-02
11
+
12
+ O `Markdown` deixa de carregar imagem com endereço absoluto (`https:`, `http:` ou `//host`). No
13
+ lugar dela fica o texto alternativo, num `span` com `data-slot="markdown-external-image"`, sem link.
14
+ O conteúdo vem de modelo, e um `<img>` é buscado pelo navegador sem clique: um modelo induzido por
15
+ injeção de prompt poderia levar dados da conversa no endereço da imagem (ADR 0050). Imagem com
16
+ endereço relativo e as imagens `data:` que o markdown-it já aceitava continuam renderizando. A
17
+ regra vale para a instância compartilhada, então chat, painéis e documentação mudam juntos. Quem
18
+ exibia imagem absoluta da própria origem passa a usar o endereço relativo.
19
+
20
+ ## 24.0.1 — 2026-10-01
21
+
22
+ Em `DialogHeader` e `DrawerHeader` com navegação e título, os dois passam a formar um grupo com
23
+ intervalo de `0.5rem`. Antes, o voltar ou o recolher ficava a `1rem` do título, a mesma distância
24
+ que separa o título das ações. As ações e o botão de fechar continuam fora do grupo, separados pelo
25
+ intervalo da família, e a mídia mantém a distância que já tinha até o título (ADR 0049). A API não
26
+ muda. Nessa combinação, a navegação passa a ficar dentro de `dialog-header-context` ou
27
+ `drawer-header-context`; seletores que exigiam um filho direto de `dialog-header-row` ou
28
+ `drawer-header-row` precisam usar o descendente, e seletores pelo `data-slot` da navegação
29
+ continuam válidos.
30
+
31
+ A documentação ganha a página **UI > Design > Composições**, que mostra como montar `Page`,
32
+ `Dialog`, `Drawer` e `Surface` com cabeçalho, corpo e rodapé, onde cada ação fica e quando o título
33
+ vai no cabeçalho ou na intro. A mesma tarefa aparece em cada superfície; `Dialog` e `Drawer`
34
+ aparecem nos layouts `sectioned` e `continuous`, e um exemplo de `Pane` mostra a `Surface` completa
35
+ dentro de um `Split`. As páginas de `Page`, `Dialog`, `Drawer` e `Pane` apontam para a nova
36
+ referência no ponto em que a escolha aparece.
37
+
10
38
  ## 24.0.0 — 2026-10-01
11
39
 
12
40
  `ToggleGroup` passa a compartilhar a gramática estrutural de `ButtonGroup`: `mode` aceita
@@ -0,0 +1,95 @@
1
+ # ADR 0049 — Navegação inline forma um grupo com o título
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-01.
5
+ - **Complementa:** ADR 0023, quanto à linha do cabeçalho de Dialog e Drawer; ADR 0048, quanto ao
6
+ formato compacto, com título no cabeçalho.
7
+
8
+ ## Contexto
9
+
10
+ `StructuralHeader` monta a linha do cabeçalho de Dialog e Drawer com `leadingPlacement="inline"`:
11
+ a navegação abre a linha, a mídia e o título vêm em seguida, as ações ficam no extremo oposto e o
12
+ botão de fechar entra por último. Até a 24.0.0, todos esses nós eram irmãos da mesma linha flex e
13
+ recebiam o intervalo da família, que em Dialog e Drawer é `gap-4` (`1rem`).
14
+
15
+ Com um único intervalo, o voltar ou o recolher ficava tão longe do título quanto o título ficava das
16
+ ações. A proximidade não distinguia duas relações diferentes:
17
+
18
+ - a navegação diz de onde a pessoa veio ou como sair da etapa, e só faz sentido lida junto com o
19
+ nome da tela;
20
+ - as ações operam sobre a superfície inteira e formam um conjunto próprio, com ritmo interno denso
21
+ (`gap-2`, de `surfaceActionsClasses`).
22
+
23
+ A ADR 0048 fixou a escolha entre a intro e o formato compacto, em que o título permanece no
24
+ cabeçalho. A página **Composições** passou a mostrar as duas formas lado a lado, e a distância
25
+ uniforme ficou evidente no Drawer compacto, em que o recolher do painel precede o título.
26
+
27
+ ## Decisão
28
+
29
+ Quando um `StructuralHeader` com `leadingPlacement="inline"` recebe navegação e heading, os dois
30
+ formam um grupo contextual:
31
+
32
+ 1. Um wrapper `data-slot="<slot>-header-context"` (`flex min-w-0 flex-1 items-center gap-2`)
33
+ envolve a navegação e o heading. O intervalo entre eles passa a ser `gap-2` (`0.5rem`).
34
+ 2. As ações e o botão de fechar ficam fora do grupo, como irmãos dele na linha
35
+ `<slot>-header-row`, e continuam separados pelo intervalo da família (`gap-4` em Dialog e
36
+ Drawer). O ritmo interno das ações não muda.
37
+ 3. O grupo ocupa a folga (`flex-1`) e aceita compressão (`min-w-0`): as ações seguem no extremo
38
+ oposto e o título continua truncando quando não cabe.
39
+ 4. Quando há mídia na linha, mídia e heading ficam num wrapper `<slot>-header-identity` que
40
+ reaplica as classes da linha da família. A aproximação vale só entre a navegação e o que vem
41
+ depois dela; a distância da mídia até o título é a mesma que existiria sem navegação.
42
+ 5. A navegação mantém `data-slot="<slot>-navigation"` dentro do grupo, para que seletores de teste
43
+ e de CSS continuem encontrando a região nas duas apresentações.
44
+
45
+ Fora dessa combinação nada muda: navegação sem título, título sem navegação,
46
+ `leadingPlacement="above"`, usado pela `Page`, e o `Dialog mode="alert"`, cujo cabeçalho não usa
47
+ `StructuralHeader`. `SurfaceHeader` não tem slot de navegação, e as intros (`DialogIntro` e
48
+ `DrawerIntro`) também não aceitam navegação.
49
+
50
+ A regra mora uma vez em `structural-header.tsx`. Dialog e Drawer a herdam porque ambos declaram
51
+ `leadingPlacement="inline"` e passam `surfaceHeaderRowClasses(family)` como `rowClassName`; nenhuma
52
+ família reescreve o intervalo.
53
+
54
+ ## Consequências
55
+
56
+ - Em Dialog e Drawer com navegação e título, o voltar ou o recolher passa de `1rem` para `0.5rem`
57
+ do título, ou da mídia que o antecede. Ações e fechamento mantêm a mesma posição e o mesmo ritmo.
58
+ - A API pública não muda, por isso a correção sai como patch (24.0.1).
59
+ - O DOM ganha um nível nessa combinação, e dois quando há mídia: a navegação deixa de ser filha
60
+ direta de `<slot>-header-row` e passa a ser filha de `<slot>-header-context`. Seletores que
61
+ dependiam do filho direto, como `[data-slot=drawer-header-row] > [data-slot=drawer-navigation]`,
62
+ precisam usar o descendente; seletores pelo `data-slot` da navegação continuam válidos.
63
+ - Uma família nova que adote a linha inline com navegação herda o agrupamento sem configuração. Se
64
+ ela precisar de outra relação entre navegação e título, a mudança é desta ADR, não de um
65
+ `className` local.
66
+
67
+ ## Alternativas consideradas
68
+
69
+ - **Manter o intervalo único.** Não exige mudança, mas iguala duas relações diferentes.
70
+ Descartada.
71
+ - **Reduzir o intervalo da linha inteira para `gap-2`.** Aproximaria navegação e título, mas também
72
+ aproximaria as ações do título, que é a separação que a família precisa preservar. Descartada.
73
+ - **Ajustar a margem no slot de navegação de cada família**, por exemplo com `-mr-2` em
74
+ `DrawerNavigation`. Resolveria o caso visual, mas espalharia uma decisão da anatomia
75
+ compartilhada por cada família e dependeria de compensar um intervalo que continua existindo.
76
+ Descartada.
77
+ - **Levar a navegação para dentro do wrapper do heading.** Misturaria uma região de controle com o
78
+ bloco que nomeia a superfície e mudaria o elemento que absorve a compressão do título. Descartada.
79
+ - **Aplicar `gap-2` também entre mídia e título quando houver navegação.** Seria mais simples, mas
80
+ faria a distância da mídia até o título depender da presença da navegação. Descartada.
81
+ - **Expor o intervalo como propriedade pública.** Abriria uma variação por consumidor para uma
82
+ relação que deve ser igual em todas as superfícies. Descartada.
83
+
84
+ ## Verificação
85
+
86
+ - `tests/ui/drawer.test.tsx` fixa a regra no Drawer: com navegação, título e ações, a linha mantém
87
+ `gap-4`, o grupo `drawer-header-context` usa `gap-2` e contém o título, `drawer-actions` mantém
88
+ `gap-2` e não há wrapper de identidade sem mídia. Com mídia, `drawer-header-identity` fica dentro
89
+ do grupo, mantém `gap-4` e reúne mídia e título, sem a navegação.
90
+ - `tests/ui/dialog.test.tsx` cobre a mesma regra no Dialog com navegação, mídia, título e ações,
91
+ inclusive as ações como filhas diretas da linha, fora do grupo.
92
+ - Os testes existentes cobrem as combinações que não mudam: navegação e ações sem título continuam
93
+ na mesma linha, e ações isoladas continuam no extremo final.
94
+ - Na página **UI > Design > Composições**, o Drawer compacto exibe o recolher e o título no
95
+ cabeçalho e serve de verificação visual da regra.
@@ -0,0 +1,64 @@
1
+ # ADR 0050 — Markdown não carrega imagem externa
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-02.
5
+ - **Complementa:** a decisão registrada em `markdown.tsx` de renderizar com `html: false`, porque o
6
+ conteúdo vem de pessoas e de modelos.
7
+
8
+ ## Contexto
9
+
10
+ O `Markdown` é o renderer único do pacote: o chat, os painéis e a documentação usam a mesma
11
+ instância do markdown-it. Ele já escapa HTML do fonte e o markdown-it já recusa links
12
+ `javascript:` e `data:text/html`. Faltava uma terceira via: `![](https://host/x.png?d=…)` virava um
13
+ `<img>` que o navegador busca sozinho, sem nenhum clique.
14
+
15
+ Isso importa porque boa parte desse conteúdo é escrita por um modelo. Um modelo induzido por
16
+ injeção de prompt (um documento, uma resposta de ferramenta, uma mensagem de terceiro) pode montar
17
+ o endereço de uma imagem com dados da conversa e deixar que o navegador de quem lê os entregue ao
18
+ host. Num relatório salvo, isso acontece também com quem abre o relatório depois.
19
+
20
+ Nenhum consumidor conhecido depende de imagem externa no Markdown: a documentação do pacote não usa
21
+ imagem em Markdown, e a plataforma Grand Brasil não tem imagem externa em documentos, Base de
22
+ conhecimento nem relatórios.
23
+
24
+ ## Decisão
25
+
26
+ 1. O `Markdown` não renderiza `<img>` para endereço absoluto, com esquema (`https:`, `http:`) ou
27
+ relativo ao protocolo (`//host`). No lugar da imagem fica o texto alternativo dela, num
28
+ `span` com `data-slot="markdown-external-image"`, sem link: seguir o endereço exigiria um
29
+ clique, mas um link montado pelo modelo não deve ser oferecido como se fosse da imagem.
30
+ 2. Endereço relativo (`/caminho`, `./arquivo.png`) continua renderizando a imagem, porque é servido
31
+ pela própria aplicação. As imagens em `data:` que o markdown-it já aceita (`png`, `gif`, `jpeg`,
32
+ `webp`) também continuam: não fazem requisição.
33
+ 3. A regra vale para a instância compartilhada `markdownIt`, então chat, painéis e documentação
34
+ seguem o mesmo comportamento. Não há opção para liberar imagem externa; um consumidor que
35
+ precise disso traz o caso concreto para uma decisão nova.
36
+
37
+ ## Consequências
38
+
39
+ - Uma resposta ou um relatório com imagem externa mostra o texto alternativo no lugar da imagem.
40
+ - A defesa não depende de cada aplicação configurar uma Content Security Policy com `img-src`,
41
+ embora uma CSP continue sendo uma segunda camada recomendada.
42
+ - Imagem absoluta da própria origem também deixa de carregar. Não há uso conhecido; se aparecer,
43
+ o caminho é usar o endereço relativo.
44
+ - A imagem relativa continua fazendo uma requisição à própria origem, com os cookies de quem lê. Se
45
+ a aplicação tiver um redirecionamento aberto ou um proxy de imagem, o mesmo vetor volta por ali.
46
+ É mais um motivo para manter a CSP com `img-src` como segunda camada.
47
+
48
+ ## Alternativas consideradas
49
+
50
+ - **Bloquear toda imagem.** Fecharia também o resíduo da imagem relativa, mas tiraria imagens que a
51
+ própria aplicação serve (anexos, ilustrações de documentação) sem um vetor conhecido que o
52
+ justifique hoje.
53
+ - **Transformar a imagem externa em link.** Mantém o endereço acessível, mas oferece com aparência
54
+ legítima um link montado pelo modelo; o texto alternativo basta para a leitura.
55
+ - **Lista de origens permitidas ou proxy de imagem.** Exigiriam configuração por aplicação e um
56
+ serviço novo para um uso que não existe. Ficam para quando um consumidor trouxer o caso.
57
+ - **Só CSP na aplicação.** Depende de cada consumidor configurar e não vale fora do navegador da
58
+ aplicação (por exemplo, na documentação publicada). Fica como segunda camada, não como a defesa.
59
+
60
+ ## Verificação
61
+
62
+ - Teste do `Markdown`: imagem `https:`, `//host`, com maiúsculas, com esquema em entidade, por
63
+ referência, dentro de link e em célula de tabela não geram `<img>` e mostram o texto alternativo;
64
+ imagem relativa e `data:image/png` continuam gerando `<img>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "24.0.0",
3
+ "version": "24.1.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",
@@ -230,20 +230,52 @@ export function StructuralHeader({
230
230
  );
231
231
  // A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
232
232
  // ou de CSS que funcione no header padrão precisa funcionar na barra.
233
- const row = (
234
- <>
235
- {leadingPlacement === "inline" && leading.length > 0 && (
236
- // `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
237
- // wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
233
+ // Navegação e título formam um contexto com intervalo de 0,5rem (ADR 0049). O intervalo da
234
+ // linha continua separando esse contexto das ações, e a mídia mantém, até o título, o mesmo
235
+ // intervalo que teria sem navegação.
236
+ const rowMedias = mediaPlacement === "row" ? medias : [];
237
+ const inlineContext =
238
+ leadingPlacement === "inline" && leading.length > 0 && heading ? (
239
+ <Wrapper
240
+ data-slot={`${slot}-header-context`}
241
+ className="flex min-w-0 flex-1 items-center gap-2"
242
+ >
238
243
  <Wrapper
239
244
  data-slot={`${slot}-navigation`}
240
- className={cn("flex min-w-0 items-center", !heading && "flex-1")}
245
+ className="flex min-w-0 items-center"
241
246
  >
242
247
  {leading}
243
248
  </Wrapper>
244
- )}
245
- {mediaPlacement === "row" && medias}
246
- {heading}
249
+ {rowMedias.length > 0 ? (
250
+ <Wrapper
251
+ data-slot={`${slot}-header-identity`}
252
+ className={cn("flex min-w-0 items-center gap-3", rowClassName)}
253
+ >
254
+ {rowMedias}
255
+ {heading}
256
+ </Wrapper>
257
+ ) : (
258
+ heading
259
+ )}
260
+ </Wrapper>
261
+ ) : null;
262
+ const row = (
263
+ <>
264
+ {inlineContext}
265
+ {inlineContext === null &&
266
+ leadingPlacement === "inline" &&
267
+ leading.length > 0 && (
268
+ // `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
269
+ // wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
270
+ <Wrapper
271
+ data-slot={`${slot}-navigation`}
272
+ className={cn("flex min-w-0 items-center", !heading && "flex-1")}
273
+ >
274
+ {leading}
275
+ </Wrapper>
276
+ )}
277
+ {inlineContext === null && mediaPlacement === "row" && medias}
278
+ {inlineContext === null && heading}
247
279
  {!heading && leading.length === 0 && (
248
280
  <Wrapper
249
281
  data-slot={`${slot}-header-spacer`}
@@ -17,6 +17,23 @@ import MarkdownIt from 'markdown-it'
17
17
  * opções próprias divergiriam no primeiro edge case, e "mesmo motor" viraria promessa. */
18
18
  export const markdownIt = new MarkdownIt({ html: false, linkify: true })
19
19
 
20
+ /**
21
+ * Imagem com endereço absoluto não carrega (ADR 0050). O conteúdo vem de modelo, e um `<img>` é
22
+ * buscado pelo navegador sem clique: um modelo induzido por injeção de prompt poderia levar dados
23
+ * da conversa no endereço. Fica o texto alternativo; imagem relativa e `data:` seguem normais.
24
+ */
25
+ const EXTERNAL_IMAGE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
26
+ const renderImage = markdownIt.renderer.rules.image
27
+ markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
28
+ const token = tokens[idx]!
29
+ const src = token.attrGet('src') ?? ''
30
+ if (EXTERNAL_IMAGE.test(src) && !src.toLowerCase().startsWith('data:')) {
31
+ const alt = self.renderInlineAsText(token.children ?? [], options, env)
32
+ return `<span data-slot="markdown-external-image">${markdownIt.utils.escapeHtml(alt)}</span>`
33
+ }
34
+ return renderImage!(tokens, idx, options, env, self)
35
+ }
36
+
20
37
  export interface MarkdownProps {
21
38
  content: string
22
39
  className?: string
@@ -0,0 +1,479 @@
1
+ ---
2
+ title: Composições
3
+ ---
4
+
5
+ # Composições
6
+
7
+ Use esta página para decidir como montar uma tela antes de escolher as propriedades de cada
8
+ componente. `Page`, `Dialog`, `Drawer` e `Surface` se organizam da mesma forma: um cabeçalho, um
9
+ corpo e um rodapé com a decisão da etapa. Cada família admite as ações e a navegação indicadas na
10
+ tabela abaixo. Em `Page`, `Dialog` e `Drawer`, o corpo também pode começar por uma intro que leva o
11
+ título. `Pane` fica fora dessa organização: ele só reserva, dentro de um `Split`, o espaço em que
12
+ uma `Surface` aparece.
13
+
14
+ Todos os exemplos mostram a mesma tarefa, editar uma ordem de serviço, em superfícies diferentes.
15
+ As propriedades de cada componente ficam nas páginas da família: [`Page`](/ui/page),
16
+ [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer), [`Surface`](/ui/surface) e [`Pane`](/ui/pane).
17
+
18
+ ## As regiões de cada superfície
19
+
20
+ Cada família dá um nome próprio a cada região. A tabela mostra a correspondência entre elas.
21
+
22
+ | Região | Page | Dialog | Drawer | Surface |
23
+ | -------------------- | ------------------------------------------- | ----------------------------- | ----------------------------- | ----------------------------- |
24
+ | Navegação | `PageNavigation` ou `PageBack` | `DialogNavigation` | `DrawerNavigation` | Não existe |
25
+ | Título no cabeçalho | `PageTitle` | `DialogTitle` | `DrawerTitle` | `SurfaceTitle` |
26
+ | Ações do cabeçalho | `PageActions` | `DialogActions` | `DrawerActions` | `SurfaceActions` |
27
+ | Intro | `Surface level={1}` no início do `PageBody` | `DialogIntro` | `DrawerIntro` | Não existe |
28
+ | Ações da intro | Ações da `Surface level={1}` | `DialogIntroActions` | `DrawerIntroActions` | Não existe |
29
+ | Corpo | `PageBody` | `DialogBody` | `DrawerBody` | `SurfaceBody` |
30
+ | Rodapé | `PageFooter` | `DialogFooter` | `DrawerFooter` | `SurfaceFooter` |
31
+ | Divisores | Antes do `PageFooter` | `sectioned` (padrão) | `sectioned` (padrão) | `sectioned` |
32
+ | Sem divisores | Cabeçalho e corpo | `continuous` | `continuous` | `continuous` (padrão) |
33
+
34
+ `layout` escolhe entre as duas anatomias em `DialogContent`, `DrawerContent` e `Surface`. Em
35
+ `sectioned`, uma linha separa as regiões vizinhas; em `continuous`, o espaço faz essa separação. Na
36
+ `Surface`, `sectioned` exige `framed` ou `elevation="raised"`, para que a linha tenha um limite. A
37
+ `Page` não oferece anatomia `sectioned`: cabeçalho e corpo são contínuos, enquanto o `PageFooter`
38
+ é uma faixa própria, separada por sua borda superior. O `layout` da página decide apenas a ocupação
39
+ de altura.
40
+
41
+ ## Onde cada ação fica
42
+
43
+ A posição de uma ação informa a que ela se refere. Escolha o lugar pelo alcance da ação, não pelo
44
+ espaço livre na tela.
45
+
46
+ | Lugar | Use para |
47
+ | ---------- | ---------------------------------------------------------------------------------------------------------- |
48
+ | Navegação | Sair ou voltar na estrutura: a trilha da página, a etapa anterior de um fluxo modal ou recolher o painel. |
49
+ | Cabeçalho | Ações que valem para a superfície inteira, como abrir uma conversa sobre o registro. |
50
+ | Intro | Ações ligadas ao título e ao conteúdo, como ver o histórico do registro. |
51
+ | Rodapé | A decisão da etapa: **Cancelar** em `ghost` e a ação principal sólida, alinhadas à direita. |
52
+
53
+ Em `Dialog` e `Drawer`, a navegação abre a linha do cabeçalho e fica junto do título, a `0.5rem`
54
+ dele. As ações ficam no extremo oposto, antes do botão de fechar, e mantêm a distância maior da
55
+ família até o título. Na `Page`, a navegação fica acima do título; quando o título está na intro,
56
+ ela divide a linha com as ações da página.
57
+
58
+ Em `Dialog` e `Drawer`, cabeçalho e rodapé permanecem visíveis enquanto o corpo rola; a intro rola
59
+ junto com o conteúdo.
60
+
61
+ ## Título no cabeçalho ou na intro
62
+
63
+ O título pode ficar em dois lugares, e essa escolha define a composição:
64
+
65
+ - **Com intro:** o cabeçalho guarda só a navegação, as ações da superfície e o fechamento. O título
66
+ abre o corpo junto das ações do conteúdo. Use quando esses controles da superfície precisam
67
+ continuar estáveis e separados do registro aberto.
68
+ - **Compacta:** título, ações do conteúdo e ações da superfície dividem a linha do cabeçalho, e a
69
+ intro é omitida. Use quando uma linha basta para nomear a tarefa e agir sobre ela.
70
+
71
+ Na composição compacta, coloque as ações do conteúdo antes das ações da superfície, no mesmo slot de
72
+ ações. Em `Dialog` e `Drawer`, o título no cabeçalho trunca quando não cabe na linha; na intro, ele
73
+ usa uma escala maior e quebra em mais de uma linha.
74
+
75
+ ## Dialog
76
+
77
+ Abra cada combinação para comparar. As linhas trocam o `layout` de `DialogContent`; as colunas
78
+ trocam o lugar do título. O campo e o rodapé são os mesmos nos quatro casos.
79
+
80
+ ```tsx preview
81
+ function OrderField() {
82
+ const id = useId();
83
+ return (
84
+ <div className="space-y-2">
85
+ <Label htmlFor={id}>Responsável</Label>
86
+ <Input id={id} defaultValue="Marina Alves" />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function DialogWithIntro({ layout, label }) {
92
+ return (
93
+ <Dialog>
94
+ <DialogTrigger asChild>
95
+ <Button variant="outline">{label}</Button>
96
+ </DialogTrigger>
97
+ <DialogContent layout={layout}>
98
+ <DialogHeader>
99
+ <DialogActions>
100
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
101
+ <MessageCircle />
102
+ </Button>
103
+ </DialogActions>
104
+ </DialogHeader>
105
+ <DialogBody>
106
+ <DialogIntro>
107
+ <DialogTitle>Editar ordem de serviço</DialogTitle>
108
+ <DialogIntroActions>
109
+ <Button variant="outline" size="sm">
110
+ Ver histórico
111
+ </Button>
112
+ </DialogIntroActions>
113
+ </DialogIntro>
114
+ <OrderField />
115
+ </DialogBody>
116
+ <DialogFooter>
117
+ <ButtonGroup mode="spaced">
118
+ <DialogClose asChild>
119
+ <Button variant="ghost">Cancelar</Button>
120
+ </DialogClose>
121
+ <Button>Salvar</Button>
122
+ </ButtonGroup>
123
+ </DialogFooter>
124
+ </DialogContent>
125
+ </Dialog>
126
+ );
127
+ }
128
+
129
+ function CompactDialog({ layout, label }) {
130
+ return (
131
+ <Dialog>
132
+ <DialogTrigger asChild>
133
+ <Button variant="outline">{label}</Button>
134
+ </DialogTrigger>
135
+ <DialogContent layout={layout}>
136
+ <DialogHeader>
137
+ <DialogMedia>
138
+ <Wrench />
139
+ </DialogMedia>
140
+ <DialogTitle>Editar ordem de serviço</DialogTitle>
141
+ <DialogActions>
142
+ <Button variant="outline" size="sm">
143
+ Ver histórico
144
+ </Button>
145
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
146
+ <MessageCircle />
147
+ </Button>
148
+ </DialogActions>
149
+ </DialogHeader>
150
+ <DialogBody>
151
+ <OrderField />
152
+ </DialogBody>
153
+ <DialogFooter>
154
+ <ButtonGroup mode="spaced">
155
+ <DialogClose asChild>
156
+ <Button variant="ghost">Cancelar</Button>
157
+ </DialogClose>
158
+ <Button>Salvar</Button>
159
+ </ButtonGroup>
160
+ </DialogFooter>
161
+ </DialogContent>
162
+ </Dialog>
163
+ );
164
+ }
165
+
166
+ render(
167
+ <div className="grid grid-cols-2 gap-3">
168
+ <DialogWithIntro layout="sectioned" label="Seccionado com intro" />
169
+ <CompactDialog layout="sectioned" label="Seccionado compacto" />
170
+ <DialogWithIntro layout="continuous" label="Contínuo com intro" />
171
+ <CompactDialog layout="continuous" label="Contínuo compacto" />
172
+ </div>,
173
+ );
174
+ ```
175
+
176
+ Prefira `continuous` para uma tarefa curta, em que a linha entre as regiões seria ruído. O `Dialog`
177
+ não recebe navegação neste exemplo: use `DialogNavigation` quando o modal trocar de etapa e a pessoa
178
+ precisar voltar à anterior.
179
+
180
+ ## Drawer
181
+
182
+ O `Drawer` segue a mesma grade. A navegação aparece aqui como o controle de recolher o painel: um
183
+ `DrawerClose` dentro de `DrawerNavigation`, com `showCloseButton={false}` para que o painel não tenha
184
+ dois controles de fechamento.
185
+
186
+ ```tsx preview
187
+ function OrderField() {
188
+ const id = useId();
189
+ return (
190
+ <div className="space-y-2">
191
+ <Label htmlFor={id}>Responsável</Label>
192
+ <Input id={id} defaultValue="Marina Alves" />
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function CollapseButton() {
198
+ return (
199
+ <DrawerClose asChild>
200
+ <Button variant="ghost" size="icon" aria-label="Recolher painel">
201
+ <ChevronsRight />
202
+ </Button>
203
+ </DrawerClose>
204
+ );
205
+ }
206
+
207
+ function DrawerWithIntro({ layout, label }) {
208
+ return (
209
+ <Drawer>
210
+ <DrawerTrigger asChild>
211
+ <Button variant="outline">{label}</Button>
212
+ </DrawerTrigger>
213
+ <DrawerContent layout={layout} showCloseButton={false}>
214
+ <DrawerHeader>
215
+ <DrawerNavigation>
216
+ <CollapseButton />
217
+ </DrawerNavigation>
218
+ <DrawerActions>
219
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
220
+ <MessageCircle />
221
+ </Button>
222
+ </DrawerActions>
223
+ </DrawerHeader>
224
+ <DrawerBody>
225
+ <DrawerIntro>
226
+ <DrawerTitle>Editar ordem de serviço</DrawerTitle>
227
+ <DrawerIntroActions>
228
+ <Button variant="outline" size="sm">
229
+ Ver histórico
230
+ </Button>
231
+ </DrawerIntroActions>
232
+ </DrawerIntro>
233
+ <OrderField />
234
+ </DrawerBody>
235
+ <DrawerFooter>
236
+ <ButtonGroup mode="spaced">
237
+ <DrawerClose asChild>
238
+ <Button variant="ghost">Cancelar</Button>
239
+ </DrawerClose>
240
+ <Button>Salvar</Button>
241
+ </ButtonGroup>
242
+ </DrawerFooter>
243
+ </DrawerContent>
244
+ </Drawer>
245
+ );
246
+ }
247
+
248
+ function CompactDrawer({ layout, label }) {
249
+ return (
250
+ <Drawer>
251
+ <DrawerTrigger asChild>
252
+ <Button variant="outline">{label}</Button>
253
+ </DrawerTrigger>
254
+ <DrawerContent layout={layout} showCloseButton={false}>
255
+ <DrawerHeader>
256
+ <DrawerNavigation>
257
+ <CollapseButton />
258
+ </DrawerNavigation>
259
+ <DrawerTitle>Editar ordem de serviço</DrawerTitle>
260
+ <DrawerActions>
261
+ <Button variant="outline" size="sm">
262
+ Ver histórico
263
+ </Button>
264
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
265
+ <MessageCircle />
266
+ </Button>
267
+ </DrawerActions>
268
+ </DrawerHeader>
269
+ <DrawerBody>
270
+ <OrderField />
271
+ </DrawerBody>
272
+ <DrawerFooter>
273
+ <ButtonGroup mode="spaced">
274
+ <DrawerClose asChild>
275
+ <Button variant="ghost">Cancelar</Button>
276
+ </DrawerClose>
277
+ <Button>Salvar</Button>
278
+ </ButtonGroup>
279
+ </DrawerFooter>
280
+ </DrawerContent>
281
+ </Drawer>
282
+ );
283
+ }
284
+
285
+ render(
286
+ <div className="grid grid-cols-2 gap-3">
287
+ <DrawerWithIntro layout="sectioned" label="Seccionado com intro" />
288
+ <CompactDrawer layout="sectioned" label="Seccionado compacto" />
289
+ <DrawerWithIntro layout="continuous" label="Contínuo com intro" />
290
+ <CompactDrawer layout="continuous" label="Contínuo compacto" />
291
+ </div>,
292
+ );
293
+ ```
294
+
295
+ Mantenha `DrawerNavigation`, `DrawerTitle` e `DrawerActions` como filhos diretos de
296
+ `DrawerHeader`: o cabeçalho reconhece os slots pelo componente e interrompe a renderização diante de
297
+ qualquer outro nó, inclusive um componente local que devolva um slot. Por isso, o exemplo extrai
298
+ apenas o botão de recolher. Sem o botão de fechar, Esc e o clique no overlay continuam fechando o
299
+ painel.
300
+
301
+ ## Page
302
+
303
+ A `Page` não tem um slot de intro. Com intro, o `PageHeader` guarda a navegação e as ações da
304
+ página, e uma `Surface level={1}` abre o `PageBody` com o título e as ações do conteúdo. A página
305
+ precisa de exatamente um heading principal, por isso o `PageHeader` dessa composição não leva
306
+ `PageTitle`.
307
+
308
+ ```tsx preview col
309
+ render(
310
+ <div className="w-full overflow-hidden rounded-lg border border-border">
311
+ <Page>
312
+ <PageHeader>
313
+ <PageNavigation>
314
+ <Breadcrumb>
315
+ <BreadcrumbList>
316
+ <BreadcrumbItem>Oficina</BreadcrumbItem>
317
+ <BreadcrumbSeparator />
318
+ <BreadcrumbItem>Ordens de serviço</BreadcrumbItem>
319
+ <BreadcrumbSeparator />
320
+ <BreadcrumbPage>OS-1842</BreadcrumbPage>
321
+ </BreadcrumbList>
322
+ </Breadcrumb>
323
+ </PageNavigation>
324
+ <PageActions>
325
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
326
+ <MessageCircle />
327
+ </Button>
328
+ </PageActions>
329
+ </PageHeader>
330
+ <PageBody>
331
+ <Surface
332
+ as="section"
333
+ level={1}
334
+ title="Editar ordem de serviço"
335
+ actions={
336
+ <Button variant="outline" size="sm">
337
+ Ver histórico
338
+ </Button>
339
+ }
340
+ >
341
+ <div className="space-y-2">
342
+ <Label htmlFor="page-intro-owner">Responsável</Label>
343
+ <Input id="page-intro-owner" defaultValue="Marina Alves" />
344
+ </div>
345
+ </Surface>
346
+ </PageBody>
347
+ <PageFooter>
348
+ <ButtonGroup mode="spaced">
349
+ <Button variant="ghost">Cancelar</Button>
350
+ <Button>Salvar</Button>
351
+ </ButtonGroup>
352
+ </PageFooter>
353
+ </Page>
354
+ </div>,
355
+ );
356
+ ```
357
+
358
+ Na composição compacta, `PageTitle` entra no `PageHeader` e as ações do conteúdo passam para
359
+ `PageActions`, antes das ações da página. A forma curta, `<Page title actions>`, produz esse mesmo
360
+ cabeçalho; nela, a navegação vem do [`PageShell`](/ui/page).
361
+
362
+ ```tsx preview col
363
+ render(
364
+ <div className="w-full overflow-hidden rounded-lg border border-border">
365
+ <Page>
366
+ <PageHeader>
367
+ <PageNavigation>
368
+ <Breadcrumb>
369
+ <BreadcrumbList>
370
+ <BreadcrumbItem>Oficina</BreadcrumbItem>
371
+ <BreadcrumbSeparator />
372
+ <BreadcrumbItem>Ordens de serviço</BreadcrumbItem>
373
+ <BreadcrumbSeparator />
374
+ <BreadcrumbPage>OS-1842</BreadcrumbPage>
375
+ </BreadcrumbList>
376
+ </Breadcrumb>
377
+ </PageNavigation>
378
+ <PageTitle>Editar ordem de serviço</PageTitle>
379
+ <PageActions>
380
+ <Button variant="outline" size="sm">
381
+ Ver histórico
382
+ </Button>
383
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
384
+ <MessageCircle />
385
+ </Button>
386
+ </PageActions>
387
+ </PageHeader>
388
+ <PageBody>
389
+ <div className="space-y-2">
390
+ <Label htmlFor="page-compact-owner">Responsável</Label>
391
+ <Input id="page-compact-owner" defaultValue="Marina Alves" />
392
+ </div>
393
+ </PageBody>
394
+ <PageFooter>
395
+ <ButtonGroup mode="spaced">
396
+ <Button variant="ghost">Cancelar</Button>
397
+ <Button>Salvar</Button>
398
+ </ButtonGroup>
399
+ </PageFooter>
400
+ </Page>
401
+ </div>,
402
+ );
403
+ ```
404
+
405
+ Use `PageFooter` quando a página for uma etapa com decisão própria, como esta edição.
406
+
407
+ ## Pane com Surface
408
+
409
+ `Pane` declara uma coluna de um [`Split`](/ui/split) e não desenha nada: não tem cabeçalho, corpo,
410
+ rodapé, recuo, borda nem fundo. Quando a coluna precisa dessas regiões, componha uma `Surface`
411
+ dentro dela. A `Surface` não tem intro, então o título fica no `SurfaceHeader`, como na composição
412
+ compacta.
413
+
414
+ ```tsx preview col
415
+ render(
416
+ <div className="h-80 w-full overflow-hidden rounded-lg border border-border">
417
+ <Split>
418
+ <Pane initialSize="14rem">
419
+ <Sidebar className="w-full">
420
+ <SidebarNav
421
+ navLabel="Ordens de serviço"
422
+ activeId="os-1842"
423
+ groups={[
424
+ {
425
+ items: [
426
+ { id: "os-1842", label: "OS-1842" },
427
+ { id: "os-1839", label: "OS-1839" },
428
+ { id: "os-1835", label: "OS-1835" },
429
+ ],
430
+ },
431
+ ]}
432
+ />
433
+ </Sidebar>
434
+ </Pane>
435
+ <Pane grow>
436
+ <Surface
437
+ as="section"
438
+ framed
439
+ elevation="raised"
440
+ layout="sectioned"
441
+ className="flex h-full flex-col"
442
+ >
443
+ <SurfaceHeader>
444
+ <SurfaceMedia>
445
+ <Wrench />
446
+ </SurfaceMedia>
447
+ <SurfaceTitle>Editar ordem de serviço</SurfaceTitle>
448
+ <SurfaceActions>
449
+ <Button variant="outline" size="sm">
450
+ Ver histórico
451
+ </Button>
452
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
453
+ <MessageCircle />
454
+ </Button>
455
+ </SurfaceActions>
456
+ </SurfaceHeader>
457
+ <SurfaceBody className="min-h-0 flex-1 overflow-y-auto">
458
+ <div className="space-y-2">
459
+ <Label htmlFor="pane-owner">Responsável</Label>
460
+ <Input id="pane-owner" defaultValue="Marina Alves" />
461
+ </div>
462
+ </SurfaceBody>
463
+ <SurfaceFooter>
464
+ <ButtonGroup mode="spaced">
465
+ <Button variant="ghost">Cancelar</Button>
466
+ <Button>Salvar</Button>
467
+ </ButtonGroup>
468
+ </SurfaceFooter>
469
+ </Surface>
470
+ </Pane>
471
+ </Split>
472
+ </div>,
473
+ );
474
+ ```
475
+
476
+ Os dois panes recebem conteúdos diferentes sem mudar de componente: o primeiro hospeda uma
477
+ [`Sidebar`](/ui/sidebar), que já tem anatomia própria, e o segundo, uma `Surface` completa. A
478
+ rolagem também pertence ao conteúdo: a `Surface` ocupa a altura do pane com `flex h-full flex-col`,
479
+ e o `SurfaceBody` declara `min-h-0 flex-1 overflow-y-auto` para rolar entre o cabeçalho e o rodapé.
@@ -109,8 +109,10 @@ compartilham a anatomia, e um fluxo que troca de superfície não muda de aparê
109
109
 
110
110
  O cabeçalho reconhece quatro slots — `DialogNavigation`, `DialogMedia`, `DialogTitle` e
111
111
  `DialogActions` — e posiciona cada um: a navegação abre a linha, o título ocupa a folga, as ações
112
- vão para o extremo oposto e o botão de fechar entra depois delas. Um nó que não seja um desses
113
- slots interrompe a renderização com um erro; contexto e conteúdo pertencem ao `DialogBody`.
112
+ vão para o extremo oposto e o botão de fechar entra depois delas. A navegação fica a `0.5rem` do
113
+ título, ou da mídia que o antecede, para se ler junto dele; as ações mantêm a distância maior da
114
+ família. Um nó que não seja um desses slots interrompe a renderização com um erro; contexto e
115
+ conteúdo pertencem ao `DialogBody`.
114
116
 
115
117
  Use `DialogActions` para uma ação da tela que precisa ficar à vista sem competir com o rodapé, como
116
118
  abrir um assistente ou alternar um modo de visualização. Use `DialogNavigation` para o retorno de um
@@ -463,7 +465,7 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Arqu
463
465
 
464
466
  | Propriedade | Tipo | Padrão | Descrição |
465
467
  | ----------------- | ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
466
- | `showCloseButton` | `boolean` | `true` | No modo padrão, inclui a ação de fechamento no header; o modo de alerta exige uma resposta identificável. |
468
+ | `showCloseButton` | `boolean` | `true` | No modo padrão, inclui a ação de fechamento no cabeçalho; o modo de alerta exige uma resposta identificável. |
467
469
  | `closeDisabled` | `boolean` | `false` | Mantém o botão de fechar visível e desabilitado e impede que Esc e o clique externo fechem a superfície. Handlers como `onEscapeKeyDown` e `onPointerDownOutside` continuam sendo chamados antes do bloqueio. |
468
470
  | `closeSize` | `"xs" \| "sm"` | `"sm"` | Define o tamanho da ação circular de fechar. |
469
471
  | `layout` | `'continuous' \| 'sectioned'` | `'sectioned'` | Anatomia da superfície inteira; os slots leem esse valor e não o recebem. Ignorado em `mode="alert"`. |
@@ -601,7 +603,7 @@ imperativa.
601
603
 
602
604
  ## Intro separada do chrome
603
605
 
604
- Quando o header contém apenas navegação, ações globais e o fechamento, coloque o título do
606
+ Quando o cabeçalho contém apenas navegação, ações globais e o fechamento, coloque o título do
605
607
  conteúdo no primeiro bloco de `DialogBody`. `DialogIntro` exige um `DialogTitle` e aceita um
606
608
  `DialogIntroActions`; o body continua responsável pelo espaço antes, entre e depois dos blocos.
607
609
 
@@ -627,4 +629,5 @@ conteúdo no primeiro bloco de `DialogBody`. `DialogIntro` exige um `DialogTitle
627
629
  </Dialog>
628
630
  ```
629
631
 
630
- No formato compacto, mantenha `DialogTitle` e `DialogActions` no próprio `DialogHeader`.
632
+ No formato compacto, mantenha `DialogTitle` e `DialogActions` no próprio `DialogHeader`. Para
633
+ comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions).
@@ -102,9 +102,10 @@ rodapé produz uma linha só. A tela não escreve `border-b` nem `border-t`.
102
102
 
103
103
  O cabeçalho reconhece quatro slots — `DrawerNavigation`, `DrawerMedia`, `DrawerTitle` e
104
104
  `DrawerActions` — e posiciona cada um: a navegação abre a linha, a mídia vem antes do título, o
105
- título ocupa a folga, as ações vão para o extremo oposto e o botão de fechar entra depois delas. Um
106
- nó que não seja um desses slots interrompe a renderização com um erro; contexto e conteúdo
107
- pertencem ao `DrawerBody`.
105
+ título ocupa a folga, as ações vão para o extremo oposto e o botão de fechar entra depois delas. A
106
+ navegação fica a `0.5rem` do título, ou da mídia que o antecede, para se ler junto dele; as ações
107
+ mantêm a distância maior da família. Um nó que não seja um desses slots interrompe a renderização
108
+ com um erro; contexto e conteúdo pertencem ao `DrawerBody`.
108
109
 
109
110
  `DrawerMedia` reserva o espaço que identifica o tipo do recurso aberto, sem acrescentar moldura.
110
111
  Componha [`IconTile`](/ui/icon-tile) quando ela ajudar na hierarquia. `DrawerNavigation` carrega o
@@ -265,7 +266,7 @@ o conteúdo controlar o próprio inset e um wrapper filho para relações intern
265
266
 
266
267
  ## Intro separada do chrome
267
268
 
268
- Quando o header contém apenas o recolhimento, ações globais e o fechamento, use `DrawerIntro`
269
+ Quando o cabeçalho contém apenas o recolhimento, ações globais e o fechamento, use `DrawerIntro`
269
270
  como primeiro bloco de `DrawerBody`. Ele exige um `DrawerTitle` e aceita `DrawerIntroActions`.
270
271
  Assim o título acompanha o conteúdo rolável enquanto o chrome permanece estável.
271
272
 
@@ -295,4 +296,5 @@ Assim o título acompanha o conteúdo rolável enquanto o chrome permanece está
295
296
  </Drawer>
296
297
  ```
297
298
 
298
- No formato compacto, mantenha `DrawerTitle` e `DrawerActions` no próprio `DrawerHeader`.
299
+ No formato compacto, mantenha `DrawerTitle` e `DrawerActions` no próprio `DrawerHeader`. Para
300
+ comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions).
@@ -6,9 +6,9 @@ centralizado e ocupa a largura disponível até `80rem` (`max-w-7xl`). Use `clas
6
6
  a composição pedir outro teto ou largura total.
7
7
 
8
8
  A forma curta é o padrão para páginas comuns e cria `PageHeader` e `PageBody`. A forma explícita
9
- exige exatamente um heading principal. Use `PageTitle` no header, `Surface level={1}` no body, ou
10
- deixe um `PageState` ativo assumir esse papel. O header pode ser omitido quando o próprio conteúdo
11
- nomeia a página. Contadores e outros indicadores pertencem ao conteúdo que os explica.
9
+ exige exatamente um heading principal. Use `PageTitle` no cabeçalho, `Surface level={1}` no body,
10
+ ou deixe um `PageState` ativo assumir esse papel. O cabeçalho pode ser omitido quando o próprio
11
+ conteúdo nomeia a página. Contadores e outros indicadores pertencem ao conteúdo que os explica.
12
12
 
13
13
  A `Page` é contínua por definição e não há divisores entre suas regiões. O `layout` da `Page` não
14
14
  é a anatomia de superfície de
@@ -109,7 +109,7 @@ navegação e as ações do shell no `PageHeader` rolável da página ativa.
109
109
  Durante `PageState`, o cabeçalho inteiro desaparece e o título do estado assume o heading principal.
110
110
  Se uma ação não puder ser executada sem o conteúdo, a própria rota deve omiti-la naquele estado.
111
111
 
112
- Na composição explícita, combine navegação, título e ações no mesmo header:
112
+ Na composição explícita, combine navegação, título e ações no mesmo cabeçalho:
113
113
 
114
114
  ```tsx preview col
115
115
  <PageShell
@@ -137,7 +137,7 @@ Na composição explícita, combine navegação, título e ações no mesmo head
137
137
 
138
138
  ## Composição explícita
139
139
 
140
- Use os slots quando a página precisar compor o header diretamente. Não misture propriedades da
140
+ Use os slots quando a página precisar compor o cabeçalho diretamente. Não misture propriedades da
141
141
  forma curta com `PageHeader` ou `PageBody`.
142
142
 
143
143
  ```tsx preview col
@@ -161,6 +161,8 @@ render(
161
161
  Em uma página de listagem, [`Surface`](/ui/surface) pode fornecer o título e as ações da coleção,
162
162
  deixando `Page` apenas com `PageBody`. Se `PageShell` fornecer navegação ou ações nesse caso, `Page`
163
163
  materializa somente o `PageHeader` necessário para recebê-las, sem duplicar o heading de `Surface`.
164
+ [Composições](/ui/compositions) compara essa forma, com o título no corpo, à forma compacta, com o
165
+ título no cabeçalho.
164
166
 
165
167
  ## Ações de etapa no rodapé
166
168
 
@@ -392,10 +394,10 @@ ocupa a altura.
392
394
 
393
395
  ## Propriedades de PageActionsTarget
394
396
 
395
- | Propriedade | Tipo | Descrição |
396
- | ----------- | --------------------- | ------------------------------------------------------------ |
397
- | `target` | `HTMLElement \| null` | Destino externo das ações; `null` mantém as ações no header. |
398
- | `children` | `ReactNode` | Árvore de página que poderá declarar `PageActions`. |
397
+ | Propriedade | Tipo | Descrição |
398
+ | ----------- | --------------------- | --------------------------------------------------------------- |
399
+ | `target` | `HTMLElement \| null` | Destino externo das ações; `null` mantém as ações no cabeçalho. |
400
+ | `children` | `ReactNode` | Árvore de página que poderá declarar `PageActions`. |
399
401
 
400
402
  ## Propriedades de PageState
401
403
 
@@ -27,7 +27,8 @@ render(
27
27
 
28
28
  Quando a região precisa de cabeçalho, corpo, rodapé ou aparência elevada, componha uma
29
29
  [`Surface`](/ui/surface) dentro do pane. Quando o filho já é uma estrutura completa, como
30
- `Sidebar`, ele pode ocupar o pane diretamente.
30
+ `Sidebar`, ele pode ocupar o pane diretamente. [Composições](/ui/compositions) mostra um pane com
31
+ `Surface` completa, com cabeçalho, corpo rolável e rodapé.
31
32
 
32
33
  ## Redimensionamento
33
34
 
@@ -49,6 +49,7 @@ import tokensMd from "./content/tokens.md?raw";
49
49
  import semanticContextMd from "./content/semantic-context.md?raw";
50
50
  import communicationMd from "./content/communication.md?raw";
51
51
  import customizationMd from "./content/customization.md?raw";
52
+ import compositionsMd from "./content/compositions.md?raw";
52
53
 
53
54
  // — Componentes —
54
55
  import accordionMd from "./content/accordion.md?raw";
@@ -395,6 +396,11 @@ export const UI_SECTIONS: DocSection[] = [
395
396
  title: "Comunicação",
396
397
  render: doc(communicationMd),
397
398
  },
399
+ {
400
+ slug: "compositions",
401
+ title: "Composições",
402
+ render: doc(compositionsMd),
403
+ },
398
404
  {
399
405
  slug: "customization",
400
406
  title: "Customização",