@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 +28 -0
- package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
- package/docs/adr/0050-markdown-does-not-load-external-images.md +64 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/structural-header.tsx +41 -9
- package/src/ui/components/primitives/markdown.tsx +17 -0
- package/src/ui/docs/content/compositions.md +479 -0
- package/src/ui/docs/content/dialog.md +8 -5
- package/src/ui/docs/content/drawer.md +7 -5
- package/src/ui/docs/content/page.md +11 -9
- package/src/ui/docs/content/pane.md +2 -1
- package/src/ui/docs/registry.tsx +6 -0
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: `` 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
|
@@ -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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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=
|
|
245
|
+
className="flex min-w-0 items-center"
|
|
241
246
|
>
|
|
242
247
|
{leading}
|
|
243
248
|
</Wrapper>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
113
|
-
|
|
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
|
|
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
|
|
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.
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
10
|
-
deixe um `PageState` ativo assumir esse papel. O
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -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",
|