@softize/opus 20.0.0 → 20.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 +46 -0
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +14 -1
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +94 -15
- package/src/ui/components/patterns/list.tsx +2 -14
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/drawer.tsx +1 -1
- package/src/ui/docs/content/action-list.md +7 -6
- package/src/ui/docs/content/content.md +88 -20
- package/src/ui/docs/content/icon-tile.md +11 -11
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ 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
|
+
## 20.1.0 — 2026-09-26
|
|
11
|
+
|
|
12
|
+
`Content` ganha um slot de mídia e passa a aceitar a moldura integrada ao header também em regiões
|
|
13
|
+
de leitura. Quem não usa as novas props mantém a mesma aparência: `frameVariant="default"` continua
|
|
14
|
+
sendo o padrão, com a borda contínua em `section` e em `fieldset`. Alguns pontos podem pedir ajuste
|
|
15
|
+
sem interromper a aplicação:
|
|
16
|
+
|
|
17
|
+
- a `legend` de todo `Content as="fieldset"` passa a envolver o título nos wrappers do header
|
|
18
|
+
(`legend > span[data-slot=content-heading] > …`): um seletor como
|
|
19
|
+
`legend > [data-slot=content-title]` deixa de casar; use `legend [data-slot=content-title]`;
|
|
20
|
+
- um `ContentHeader` de fieldset com slot inválido lança outra mensagem: “ContentHeader de fieldset
|
|
21
|
+
aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda.”
|
|
22
|
+
Atualize testes que conferem o texto;
|
|
23
|
+
- com `frameVariant="legend"` dentro de uma superfície com fundo próprio, como um `Card` com `bg-*`
|
|
24
|
+
ou um `Popover`, declare `--surface-background` com esse fundo na superfície (veja **Cor da
|
|
25
|
+
superfície**);
|
|
26
|
+
- em `advancedFilters.sectionIcons`, um glifo solto ganha a moldura padrão, e testes que buscavam
|
|
27
|
+
`[data-slot="content-title-icon"]` devem buscar `[data-slot="content-media"]`.
|
|
28
|
+
|
|
29
|
+
Em detalhe:
|
|
30
|
+
|
|
31
|
+
- **Mídia no header.** `ContentHeader` aceita `ContentMedia`, e a forma curta de `Content` ganha
|
|
32
|
+
`media`. O slot já é a moldura de `IconTile`, com a mesma escala e posição em `section` e em
|
|
33
|
+
`fieldset`: passe só o glifo e escolha `size`, `context` e `variant` no slot. Se você colocava um
|
|
34
|
+
`IconTile` dentro de `title`, mova-o para `media`; o slot o adota e herda o que ele declara, sem
|
|
35
|
+
desenhar moldura dentro de moldura. `media` com `null` ou `false` não desenha nada. Em `fieldset`,
|
|
36
|
+
a mídia é decorativa e não entra no nome do grupo.
|
|
37
|
+
- **Moldura integrada em `section`.** `frameVariant="legend"` deixa de exigir `as="fieldset"`. Em
|
|
38
|
+
uma `section`, o título continua sendo um heading no nível declarado, e contador, descrição e
|
|
39
|
+
ações continuam disponíveis; nenhuma `legend` é emitida. Use `as="fieldset"` só para grupos de
|
|
40
|
+
campos: a `legend` aceita a mídia e o título, e contador, descrição e ações continuam fora dela
|
|
41
|
+
(ADR 0025).
|
|
42
|
+
- **Cor da superfície.** Para interromper a borda, o header pinta o título e as ações com a
|
|
43
|
+
variável `--surface-background`. `Card`, `Dialog` e `Drawer` a declaram com o fundo que pintam,
|
|
44
|
+
então a linha não reaparece atrás do título quando há um `CardBody`, um `DrawerBody` ou outro
|
|
45
|
+
wrapper transparente no caminho. Fora delas, vale `--background`.
|
|
46
|
+
- **Filtros avançados.** Os ícones de `advancedFilters.sectionIcons` passam a ocupar o
|
|
47
|
+
`ContentMedia` de cada seção: um glifo solto ganha a moldura padrão, em vez do ícone esmaecido sem
|
|
48
|
+
moldura, e um `IconTile` é adotado pelo slot com o que declara. Um ícone `null` não desenha
|
|
49
|
+
moldura.
|
|
50
|
+
|
|
51
|
+
A página de `Content` mostra a mídia, os dois acabamentos da moldura e a declaração de
|
|
52
|
+
`--surface-background`. A ADR 0025 registra que o acabamento da moldura não altera a semântica; a
|
|
53
|
+
ADR 0023 ganha a emenda do slot de mídia de `Content`, e as ADRs 0020 e 0021, emendas que apontam
|
|
54
|
+
para a 0025.
|
|
55
|
+
|
|
10
56
|
## 20.0.0 — 2026-09-26
|
|
11
57
|
|
|
12
58
|
Esta versão corrige componentes que falhavam em acessibilidade ou em layout, leva o `fill` da
|
|
@@ -24,6 +24,14 @@ a mesma base visual porque essas variantes já prometem a moldura.
|
|
|
24
24
|
`IconTile` é somente apresentação. Quando o ícone aciona algo, deve-se usar `Button` com tamanho
|
|
25
25
|
de ícone e nome acessível.
|
|
26
26
|
|
|
27
|
+
### Emenda de 2026-09-26 — slots de mídia das superfícies
|
|
28
|
+
|
|
29
|
+
Os slots de mídia da anatomia de superfícies (ADR 0023), como `CardMedia` e `ContentMedia`, não são
|
|
30
|
+
slots genéricos: assim como `ItemMedia variant="icon"`, eles já prometem a moldura, e o consumidor
|
|
31
|
+
passa só o glifo. `ContentMedia` adota um `IconTile` recebido como filho em vez de desenhar uma
|
|
32
|
+
moldura dentro da outra (ADR 0025). Slots genéricos, como `FieldLegend.icon`, continuam como esta
|
|
33
|
+
ADR descreve.
|
|
34
|
+
|
|
27
35
|
## Consequências
|
|
28
36
|
|
|
29
37
|
- A presença da moldura deixa de ser uma regra implícita da legenda.
|
|
@@ -30,6 +30,13 @@ uma `section`. A escolha de `fieldset` não define o acabamento visual: quando `
|
|
|
30
30
|
presente, a moldura é contínua por padrão. Os filtros avançados preservam a legenda integrada à
|
|
31
31
|
borda declarando `frameVariant="legend"`.
|
|
32
32
|
|
|
33
|
+
### Emenda de 2026-09-26 — o acabamento `legend` também vale para `section`
|
|
34
|
+
|
|
35
|
+
A ADR 0025 estende `frameVariant="legend"` a `Content as="section"`, para que regiões de leitura
|
|
36
|
+
integrem o header à moldura sem declarar semântica de formulário, e permite `ContentMedia` na
|
|
37
|
+
legenda de um `fieldset`. A regra desta ADR se mantém: `as` escolhe o elemento e a semântica, e
|
|
38
|
+
`frameVariant` escolhe só o acabamento. Na legenda, contador, descrição e ações continuam inválidos.
|
|
39
|
+
|
|
33
40
|
## Consequências
|
|
34
41
|
|
|
35
42
|
- A mesma API nomeia e delimita regiões de leitura e grupos de formulário.
|
|
@@ -143,7 +143,7 @@ anteriores já os delimitaram:
|
|
|
143
143
|
|
|
144
144
|
| Slot | Card | Content | Page | Dialog | Drawer | Pane |
|
|
145
145
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
146
|
-
| Media | sim |
|
|
146
|
+
| Media | sim | sim (`ContentMedia`) | não | sim (`DialogMedia`) | sim | sim |
|
|
147
147
|
| Title | sim | sim | sim | sim | sim | sim |
|
|
148
148
|
| Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
|
|
149
149
|
| Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
|
|
@@ -163,6 +163,19 @@ de um cabeçalho remontam a linha com `span`, incluindo o ícone dentro do texto
|
|
|
163
163
|
`Content` permanece sem footer: é uma região nomeada dentro de outra superfície, e um rodapé
|
|
164
164
|
pertence à superfície que a hospeda.
|
|
165
165
|
|
|
166
|
+
#### Emenda de 2026-09-25 — Content também expõe mídia
|
|
167
|
+
|
|
168
|
+
A aplicação posterior da anatomia à tela do Evento mostrou que regiões de leitura podem precisar
|
|
169
|
+
da mesma identificação visual que um Card sem deixarem de ser `Content`. Sem um slot próprio, a
|
|
170
|
+
documentação e os consumidores voltavam a inserir o ícone dentro de `ContentTitle`, repetindo o
|
|
171
|
+
desvio que motivou esta ADR.
|
|
172
|
+
|
|
173
|
+
`ContentHeader` passa a aceitar `ContentMedia`, e o shorthand expõe `media`. A mídia usa a mesma
|
|
174
|
+
moldura de `IconTile` das demais famílias e integra o grupo visual do título. Esse contrato vale
|
|
175
|
+
tanto para `section` quanto para `fieldset`: `as` troca o elemento e a semântica, não a apresentação
|
|
176
|
+
de mídia e título. Na `legend`, contador, descrição e ações continuam indisponíveis porque não
|
|
177
|
+
nomeiam o grupo de campos.
|
|
178
|
+
|
|
166
179
|
### 7. Título: hierarquia e tamanho são independentes
|
|
167
180
|
|
|
168
181
|
O nível semântico vem de `level` (`1`–`6`) ou de `asChild`, e nunca do tamanho visual. O tamanho vem
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ADR 0025 — O acabamento da moldura de Content não altera sua semântica
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-25.
|
|
5
|
+
- **Complementa:** ADRs 0021 e 0023.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
`Content` separa o elemento semântico (`section` ou `fieldset`) da presença da moldura. Porém, o
|
|
10
|
+
acabamento `frameVariant="legend"`, que integra visualmente o título à borda, só está disponível
|
|
11
|
+
quando a raiz é um `fieldset`.
|
|
12
|
+
|
|
13
|
+
Essa restrição leva regiões somente de leitura a escolherem `fieldset` apenas para obter o
|
|
14
|
+
acabamento visual. É o caso de grupos de detalhes como Cliente e Veículo: eles formam seções
|
|
15
|
+
nomeadas com `DetailGroup`, não conjuntos de controles de formulário. A aparência desejada se
|
|
16
|
+
parece com uma legenda, mas não transforma o título em uma `legend` semântica.
|
|
17
|
+
|
|
18
|
+
As forças em tensão são:
|
|
19
|
+
|
|
20
|
+
- preservar `fieldset` e `legend` exclusivamente para grupos de campos relacionados;
|
|
21
|
+
- oferecer a mesma linguagem visual a regiões de leitura;
|
|
22
|
+
- manter título, mídia, descrição, contador e ações compatíveis com a anatomia normal de `Content`;
|
|
23
|
+
- não criar outra família pública apenas para desenhar uma borda interrompida.
|
|
24
|
+
|
|
25
|
+
## Decisão
|
|
26
|
+
|
|
27
|
+
`frameVariant="legend"` passa a ser aceito tanto por `Content as="section"` quanto por
|
|
28
|
+
`Content as="fieldset"`. A propriedade escolhe somente o acabamento da moldura; `as` continua
|
|
29
|
+
sendo a única propriedade que determina o elemento e sua semântica.
|
|
30
|
+
|
|
31
|
+
Em uma `section`, o título permanece um heading no nível declarado, o header continua aceitando
|
|
32
|
+
`ContentMedia`, `ContentMeta`, `ContentDescription` e `ContentActions`, e a borda é visualmente
|
|
33
|
+
interrompida ao redor das regiões do header que a ocupam. Nenhum elemento `legend` é emitido.
|
|
34
|
+
|
|
35
|
+
Em um `fieldset`, o título continua sendo a `legend` nativa e pode ser precedido pelo mesmo
|
|
36
|
+
`ContentMedia` usado em uma `section`. A mídia é decorativa e acompanha o nome do grupo; contador,
|
|
37
|
+
descrição e ações continuam fora da legenda. `as` altera a semântica, não a apresentação do par
|
|
38
|
+
mídia + título nem o comportamento nativo de formulários.
|
|
39
|
+
|
|
40
|
+
O nome `legend` descreve o acabamento visual existente e não cria um terceiro valor incompatível
|
|
41
|
+
para o mesmo desenho. A documentação deve apresentar primeiro a escolha semântica e depois o
|
|
42
|
+
acabamento, deixando explícito que uma região de leitura permanece `section`.
|
|
43
|
+
|
|
44
|
+
Para interromper a linha sem depender da transparência dos wrappers intermediários, Card, Dialog e
|
|
45
|
+
Drawer declaram `--surface-background` com o token que já usam (`--background` ou `--card`). As
|
|
46
|
+
regiões do header que cruzam a borda usam essa cor; quando não houver uma superfície declarada,
|
|
47
|
+
`Content` usa `--background` como fallback. O nome segue o papel da variável, como os demais
|
|
48
|
+
contratos CSS locais, sem prefixo de pacote. Uma superfície com fundo próprio que não seja uma
|
|
49
|
+
dessas três, como um `Card` com `bg-*`, um `Popover` ou um contêiner da aplicação, declara a mesma
|
|
50
|
+
variável com o fundo que pinta.
|
|
51
|
+
|
|
52
|
+
### Relação com a ADR 0020
|
|
53
|
+
|
|
54
|
+
A ADR 0020 mantém genéricos os slots de ícone, como `FieldLegend.icon`: o consumidor passa o glifo
|
|
55
|
+
quando quer só o ícone e compõe `IconTile` quando quer a moldura. `ContentMedia` não é um slot
|
|
56
|
+
genérico; é o slot de mídia da anatomia de superfícies (ADR 0023), que já promete a moldura, como
|
|
57
|
+
`CardMedia` e `ItemMedia variant="icon"`. Por isso a mídia de `Content` sempre desenha a moldura, e
|
|
58
|
+
`asChild` continua sendo a saída para uma marca com visual próprio.
|
|
59
|
+
|
|
60
|
+
Consumidores que já compunham `IconTile` antes deste slot existir não devem ganhar moldura dentro de
|
|
61
|
+
moldura. `ContentMedia` adota um `IconTile` recebido como filho: o slot assume o lugar dele e herda
|
|
62
|
+
`size`, `context`, `variant` e as demais props declaradas nele. `media` com `null` ou `false` é
|
|
63
|
+
tratado como ausência, e nenhuma moldura vazia é desenhada. Os ícones de
|
|
64
|
+
`advancedFilters.sectionIcons` da `ActionList` passam pelo mesmo slot, sem normalização própria.
|
|
65
|
+
|
|
66
|
+
## Consequências
|
|
67
|
+
|
|
68
|
+
- Regiões de leitura podem integrar o header à moldura sem declarar semântica de formulário.
|
|
69
|
+
- A composição `Content + DetailGroup` preserva `section`, heading, mídia, descrição, contador e
|
|
70
|
+
ações.
|
|
71
|
+
- Grupos de campos continuam usando `fieldset` e `legend` quando essa relação existe no conteúdo.
|
|
72
|
+
- Filtros e regiões de leitura usam o mesmo slot, escala e alinhamento para mídia e título.
|
|
73
|
+
- O default permanece `frameVariant="default"`; quem não usa as novas props não muda visualmente.
|
|
74
|
+
- `advancedFilters.sectionIcons` muda: um glifo solto, antes exibido sem moldura, passa a ocupar o
|
|
75
|
+
`ContentMedia` e ganha a moldura padrão. Um `IconTile` continua com a aparência que declara.
|
|
76
|
+
- A `legend` de todo `Content as="fieldset"` passa a envolver título e mídia nos wrappers do
|
|
77
|
+
`SurfaceHeader` (`legend > span[data-slot=content-heading] > …`). Seletores que dependiam do
|
|
78
|
+
título como filho direto da `legend` precisam ser ajustados.
|
|
79
|
+
- O acabamento precisa funcionar sobre as superfícies canônicas sem o consumidor reconstruir a
|
|
80
|
+
borda ou posicionar o header por classes próprias.
|
|
81
|
+
- Wrappers transparentes entre a superfície e `Content` não deixam a borda aparecer atrás do grupo
|
|
82
|
+
de heading ou das ações, porque a cor vem da variável declarada pela superfície e não do pai.
|
|
83
|
+
- Superfícies com fundo próprio fora de Card, Dialog e Drawer ficam responsáveis por declarar
|
|
84
|
+
`--surface-background`; sem isso, a faixa atrás do header usa `--background`.
|
|
85
|
+
|
|
86
|
+
## Alternativas consideradas
|
|
87
|
+
|
|
88
|
+
### Usar `fieldset` também para detalhes somente de leitura
|
|
89
|
+
|
|
90
|
+
Reaproveitaria a implementação atual, mas atribuiria semântica de grupo de formulário a uma lista
|
|
91
|
+
de definições. Foi descartada porque a escolha do elemento passaria a ser governada pela aparência.
|
|
92
|
+
|
|
93
|
+
### Criar outro componente de painel
|
|
94
|
+
|
|
95
|
+
Separaria esse desenho de `Content`, mas duplicaria a anatomia de região nomeada e voltaria a exigir
|
|
96
|
+
uma decisão entre estruturas equivalentes. Foi descartada porque `Content` já é responsável pelo
|
|
97
|
+
header, corpo e moldura da região.
|
|
98
|
+
|
|
99
|
+
### Criar um novo valor visual com o mesmo desenho
|
|
100
|
+
|
|
101
|
+
Um nome como `notched` evitaria a associação com a `legend`, mas criaria dois valores para o mesmo
|
|
102
|
+
acabamento e uma migração sem ganho observável. Foi descartado porque o contrato já afirma que
|
|
103
|
+
`frameVariant` não altera a semântica.
|
|
104
|
+
|
|
105
|
+
### Herdar o background do pai imediato
|
|
106
|
+
|
|
107
|
+
Manteria a implementação restrita a `Content`, mas `background-color: inherit` recebe
|
|
108
|
+
`transparent` quando `DrawerBody`, `CardBody` ou outro wrapper não pinta o próprio fundo. Foi
|
|
109
|
+
descartado porque o acabamento depende da superfície que efetivamente pinta, não do pai estrutural
|
|
110
|
+
mais próximo.
|
|
111
|
+
|
|
112
|
+
### Redesenhar a borda em segmentos
|
|
113
|
+
|
|
114
|
+
Evitaria cobrir a linha com uma cor, mas exigiria derivar segmentos antes, entre e depois de título,
|
|
115
|
+
descrição e ações com larguras dinâmicas. Foi descartado porque duplicaria no CSS a anatomia que o
|
|
116
|
+
header já organiza.
|
|
117
|
+
|
|
118
|
+
## Verificação
|
|
119
|
+
|
|
120
|
+
- Testes de UI confirmam que `Content as="section"` com `frameVariant="legend"` mantém `section`,
|
|
121
|
+
heading e todos os slots do header.
|
|
122
|
+
- Testes confirmam que o mesmo acabamento em `fieldset` continua emitindo `legend` e restringindo
|
|
123
|
+
seus slots.
|
|
124
|
+
- Testes confirmam que o default continua usando a moldura contínua nos dois elementos.
|
|
125
|
+
- Testes confirmam que Card, Dialog e Drawer declaram `--surface-background` e que o grupo de
|
|
126
|
+
heading e as ações do header usam essa variável, com `--background` como fallback. A herança da
|
|
127
|
+
variável através de wrappers transparentes é comportamento do CSS, não é provada pela happy-dom e
|
|
128
|
+
depende de conferência visual no catálogo, onde a página de `Content` a demonstra sobre um `Card`.
|
|
129
|
+
- Testes confirmam que um `IconTile` passado como mídia é adotado pelo slot, que `null` e `false`
|
|
130
|
+
não desenham mídia e que um glifo solto em `sectionIcons` ganha a moldura.
|
|
131
|
+
- Um teste confirma que `Page layout="fill"` leva a altura até a `ActionList` através de um
|
|
132
|
+
`Content` com `frameVariant="legend"` (ADR 0024).
|
|
133
|
+
- A documentação demonstra `Content + DetailGroup` no acabamento `legend` sem usar `fieldset` e
|
|
134
|
+
mantém `Content as="fieldset" + FieldGroup` para grupos de controles.
|
package/package.json
CHANGED
|
@@ -10,9 +10,14 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cn } from "../../lib/cn.ts";
|
|
13
|
+
import { IconTile, type IconTileProps } from "../primitives/icon-tile.tsx";
|
|
13
14
|
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
14
15
|
import { headingTags } from "./heading-level.ts";
|
|
15
16
|
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
17
|
+
import {
|
|
18
|
+
SurfaceMedia,
|
|
19
|
+
type SurfaceMediaProps,
|
|
20
|
+
} from "./surface-media.tsx";
|
|
16
21
|
import {
|
|
17
22
|
SurfaceHeader,
|
|
18
23
|
surfaceHeaderClasses,
|
|
@@ -59,7 +64,8 @@ interface ContentSectionBaseProps
|
|
|
59
64
|
Omit<ComponentProps<"section">, "children" | "title"> {
|
|
60
65
|
/** Elemento semântico da região. */
|
|
61
66
|
as?: "section";
|
|
62
|
-
|
|
67
|
+
/** Tratamento visual da moldura: contínua (default) ou integrada ao header. */
|
|
68
|
+
frameVariant?: ContentFrameVariant;
|
|
63
69
|
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
64
70
|
level?: ContentHeaderLevel;
|
|
65
71
|
}
|
|
@@ -78,6 +84,8 @@ interface ContentFieldsetBaseProps
|
|
|
78
84
|
|
|
79
85
|
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
80
86
|
title: ReactNode;
|
|
87
|
+
/** Ícone ou imagem que identifica a região, antes do título. */
|
|
88
|
+
media?: ReactNode;
|
|
81
89
|
/** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
|
|
82
90
|
count?: number;
|
|
83
91
|
description?: ReactNode;
|
|
@@ -87,6 +95,8 @@ interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
|
87
95
|
|
|
88
96
|
interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
89
97
|
title: ReactNode;
|
|
98
|
+
/** Ícone ou imagem decorativa que acompanha o título dentro da legenda. */
|
|
99
|
+
media?: ReactNode;
|
|
90
100
|
count?: never;
|
|
91
101
|
description?: never;
|
|
92
102
|
actions?: never;
|
|
@@ -95,6 +105,7 @@ interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
|
95
105
|
|
|
96
106
|
interface ContentComposedProps {
|
|
97
107
|
title?: never;
|
|
108
|
+
media?: never;
|
|
98
109
|
count?: never;
|
|
99
110
|
description?: never;
|
|
100
111
|
actions?: never;
|
|
@@ -111,6 +122,7 @@ export type ContentProps =
|
|
|
111
122
|
export function Content({
|
|
112
123
|
as = "section",
|
|
113
124
|
title,
|
|
125
|
+
media,
|
|
114
126
|
count,
|
|
115
127
|
description,
|
|
116
128
|
actions,
|
|
@@ -164,6 +176,7 @@ export function Content({
|
|
|
164
176
|
const anatomy = shorthand ? (
|
|
165
177
|
<>
|
|
166
178
|
<ContentHeader>
|
|
179
|
+
{hasContentMedia(media) && <ContentMedia>{media}</ContentMedia>}
|
|
167
180
|
<ContentTitle>{title}</ContentTitle>
|
|
168
181
|
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
169
182
|
{description !== undefined && (
|
|
@@ -214,7 +227,12 @@ export function Content({
|
|
|
214
227
|
aria-labelledby={ariaLabelledBy}
|
|
215
228
|
className={cn(
|
|
216
229
|
"space-y-3",
|
|
217
|
-
framed &&
|
|
230
|
+
framed &&
|
|
231
|
+
frameVariant === "default" &&
|
|
232
|
+
"rounded-lg border border-border p-4",
|
|
233
|
+
framed &&
|
|
234
|
+
frameVariant === "legend" &&
|
|
235
|
+
"rounded-lg border border-border px-4 pb-4",
|
|
218
236
|
fill.filling && fillRegionClassName,
|
|
219
237
|
className,
|
|
220
238
|
)}
|
|
@@ -244,20 +262,15 @@ export function ContentHeader({
|
|
|
244
262
|
"Content",
|
|
245
263
|
);
|
|
246
264
|
if (content.element === "fieldset") {
|
|
247
|
-
const nodes = Children.toArray(children);
|
|
248
|
-
if (
|
|
249
|
-
nodes.length !== 1 ||
|
|
250
|
-
!isValidElement(nodes[0]) ||
|
|
251
|
-
nodes[0].type !== ContentTitle
|
|
252
|
-
) {
|
|
253
|
-
throw new Error(
|
|
254
|
-
"ContentHeader de fieldset exige somente um ContentTitle; contador, descrição e ações ficam fora da legenda.",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
265
|
return (
|
|
258
266
|
<ContentHeaderContext.Provider value={content}>
|
|
259
|
-
<
|
|
260
|
-
|
|
267
|
+
<SurfaceHeader
|
|
268
|
+
as="legend"
|
|
269
|
+
name="ContentHeader"
|
|
270
|
+
slot="content"
|
|
271
|
+
slots={{ media: ContentMedia, title: ContentTitle }}
|
|
272
|
+
mediaPlacement="heading"
|
|
273
|
+
invalidMessage="ContentHeader de fieldset aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda."
|
|
261
274
|
className={cn(
|
|
262
275
|
content.framed &&
|
|
263
276
|
content.frameVariant === "default" &&
|
|
@@ -270,7 +283,7 @@ export function ContentHeader({
|
|
|
270
283
|
{...props}
|
|
271
284
|
>
|
|
272
285
|
{children}
|
|
273
|
-
</
|
|
286
|
+
</SurfaceHeader>
|
|
274
287
|
</ContentHeaderContext.Provider>
|
|
275
288
|
);
|
|
276
289
|
}
|
|
@@ -280,12 +293,25 @@ export function ContentHeader({
|
|
|
280
293
|
name="ContentHeader"
|
|
281
294
|
slot="content"
|
|
282
295
|
slots={{
|
|
296
|
+
media: ContentMedia,
|
|
283
297
|
title: ContentTitle,
|
|
284
298
|
count: ContentMeta,
|
|
285
299
|
description: ContentDescription,
|
|
286
300
|
actions: ContentActions,
|
|
287
301
|
}}
|
|
302
|
+
mediaPlacement="heading"
|
|
288
303
|
className={cn(
|
|
304
|
+
content.framed &&
|
|
305
|
+
content.frameVariant === "legend" &&
|
|
306
|
+
"flex items-center [&>[data-slot=content-actions]]:-mr-2 [&>[data-slot=content-actions]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-actions]]:px-2 [&>[data-slot=content-heading]]:-ml-2 [&>[data-slot=content-heading]]:w-fit [&>[data-slot=content-heading]]:max-w-full [&>[data-slot=content-heading]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-heading]]:px-2",
|
|
307
|
+
content.framed &&
|
|
308
|
+
content.frameVariant === "legend" &&
|
|
309
|
+
content.variant === "section" &&
|
|
310
|
+
"-mt-4 min-h-8",
|
|
311
|
+
content.framed &&
|
|
312
|
+
content.frameVariant === "legend" &&
|
|
313
|
+
content.variant === "page" &&
|
|
314
|
+
"-mt-5 min-h-10",
|
|
289
315
|
content.variant === "page" &&
|
|
290
316
|
"flex-row items-center justify-start gap-3",
|
|
291
317
|
className,
|
|
@@ -298,6 +324,59 @@ export function ContentHeader({
|
|
|
298
324
|
);
|
|
299
325
|
}
|
|
300
326
|
|
|
327
|
+
/** `null`, `false` e `undefined` não desenham mídia: uma moldura vazia não identifica nada. */
|
|
328
|
+
function hasContentMedia(media: ReactNode): boolean {
|
|
329
|
+
return media !== undefined && media !== null && media !== false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Um `IconTile` recebido como filho não vira uma segunda moldura: o slot assume o lugar dele e
|
|
334
|
+
* herda o que ele declara (`size`, `context`, `variant`, demais props), somando as classes.
|
|
335
|
+
*/
|
|
336
|
+
function adoptIconTile(props: SurfaceMediaProps): SurfaceMediaProps {
|
|
337
|
+
const { children, asChild, className } = props;
|
|
338
|
+
if (asChild) return props;
|
|
339
|
+
// `toArray`, não `only`: um array com um único item (de um `map`/`filter`) é válido aqui.
|
|
340
|
+
const nodes = Children.toArray(children);
|
|
341
|
+
if (nodes.length !== 1) return props;
|
|
342
|
+
const child = nodes[0];
|
|
343
|
+
if (!isValidElement<IconTileProps>(child) || child.type !== IconTile) {
|
|
344
|
+
return props;
|
|
345
|
+
}
|
|
346
|
+
// `null` em size/context/variant significa "use o padrão" no IconTile: fica o do slot.
|
|
347
|
+
const declared = Object.fromEntries(
|
|
348
|
+
Object.entries(child.props).filter(([, value]) => value != null),
|
|
349
|
+
) as SurfaceMediaProps;
|
|
350
|
+
return {
|
|
351
|
+
...props,
|
|
352
|
+
...declared,
|
|
353
|
+
className: cn(className, declared.className),
|
|
354
|
+
children: declared.children,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Moldura de ícone ou imagem que identifica a região nomeada. */
|
|
359
|
+
export function ContentMedia({
|
|
360
|
+
size = "xs",
|
|
361
|
+
...rest
|
|
362
|
+
}: SurfaceMediaProps): ReactElement {
|
|
363
|
+
const context = requiredContext(
|
|
364
|
+
useContext(ContentHeaderContext),
|
|
365
|
+
"ContentMedia",
|
|
366
|
+
"ContentHeader",
|
|
367
|
+
);
|
|
368
|
+
const props = adoptIconTile({ size, ...rest });
|
|
369
|
+
return (
|
|
370
|
+
<SurfaceMedia
|
|
371
|
+
slot="content-media"
|
|
372
|
+
{...props}
|
|
373
|
+
aria-hidden={
|
|
374
|
+
context.element === "fieldset" ? true : props["aria-hidden"]
|
|
375
|
+
}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
301
380
|
export function ContentTitle({
|
|
302
381
|
className,
|
|
303
382
|
id,
|
|
@@ -1218,20 +1218,8 @@ function AdvancedFiltersDialog({
|
|
|
1218
1218
|
frameVariant={
|
|
1219
1219
|
sectionVariant === "framed" ? "legend" : "default"
|
|
1220
1220
|
}
|
|
1221
|
-
title={
|
|
1222
|
-
|
|
1223
|
-
{sectionIcons?.[section] !== undefined && (
|
|
1224
|
-
<span
|
|
1225
|
-
data-slot="content-title-icon"
|
|
1226
|
-
aria-hidden="true"
|
|
1227
|
-
className="inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
1228
|
-
>
|
|
1229
|
-
{sectionIcons[section]}
|
|
1230
|
-
</span>
|
|
1231
|
-
)}
|
|
1232
|
-
{section}
|
|
1233
|
-
</span>
|
|
1234
|
-
}
|
|
1221
|
+
title={section}
|
|
1222
|
+
media={sectionIcons?.[section]}
|
|
1235
1223
|
>
|
|
1236
1224
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
1237
1225
|
{entries.map(field)}
|
|
@@ -45,10 +45,12 @@ interface SurfaceHeaderSlots {
|
|
|
45
45
|
title: ElementType;
|
|
46
46
|
count?: ElementType;
|
|
47
47
|
description?: ElementType;
|
|
48
|
-
actions
|
|
48
|
+
actions?: ElementType;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
52
|
+
/** Elemento estrutural; `legend` preserva a mesma anatomia com wrappers phrasing. */
|
|
53
|
+
as?: "div" | "legend";
|
|
52
54
|
/** Nome do componente público, para as mensagens de erro (`PageHeader`, `ContentHeader`). */
|
|
53
55
|
name: string;
|
|
54
56
|
/** Prefixo dos `data-slot` (`page` → `page-header`, `page-heading`). */
|
|
@@ -57,6 +59,8 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
57
59
|
slots: SurfaceHeaderSlots;
|
|
58
60
|
/** Posição de um slot introdutório opcional, antes da linha ou dentro dela. */
|
|
59
61
|
leadingPlacement?: "above" | "inline";
|
|
62
|
+
/** Mantém a mídia na linha geral ou a agrupa com o heading. */
|
|
63
|
+
mediaPlacement?: "row" | "heading";
|
|
60
64
|
/** Classes de um container interno quando a moldura precisa ocupar toda a largura. */
|
|
61
65
|
contentClassName?: string;
|
|
62
66
|
/** Permite uma superfície composta apenas pelas regiões leading/actions. */
|
|
@@ -71,6 +75,11 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
71
75
|
* NOMEIAM algo (Card, Dialog, Drawer, Page, Content) continuam estritas.
|
|
72
76
|
*/
|
|
73
77
|
allowFreeContent?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Mensagem própria para uma composição inválida, quando a regra do header não cabe na
|
|
80
|
+
* mensagem genérica (a legenda de um fieldset aceita menos slots que o header comum).
|
|
81
|
+
*/
|
|
82
|
+
invalidMessage?: string;
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
/** Expande fragments de primeiro nível: o shorthand monta os slots dentro de um `<>`. */
|
|
@@ -83,19 +92,23 @@ function flatten(children: ReactNode): ReactNode[] {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export function SurfaceHeader({
|
|
95
|
+
as: Root = "div",
|
|
86
96
|
name,
|
|
87
97
|
slot,
|
|
88
98
|
slots,
|
|
89
99
|
leadingPlacement = "above",
|
|
100
|
+
mediaPlacement = "row",
|
|
90
101
|
contentClassName,
|
|
91
102
|
titleRequired = true,
|
|
92
103
|
rowClassName,
|
|
93
104
|
trailing,
|
|
94
105
|
allowFreeContent = false,
|
|
106
|
+
invalidMessage,
|
|
95
107
|
className,
|
|
96
108
|
children,
|
|
97
109
|
...props
|
|
98
110
|
}: SurfaceHeaderProps): ReactElement {
|
|
111
|
+
const Wrapper = Root === "legend" ? "span" : "div";
|
|
99
112
|
const nodes = flatten(children);
|
|
100
113
|
const of = (type: ElementType) =>
|
|
101
114
|
nodes.filter((node) => isValidElement(node) && node.type === type);
|
|
@@ -111,7 +124,8 @@ export function SurfaceHeader({
|
|
|
111
124
|
const counts = slots.count === undefined ? [] : of(slots.count);
|
|
112
125
|
const descriptions =
|
|
113
126
|
slots.description === undefined ? [] : of(slots.description);
|
|
114
|
-
const actionSlots =
|
|
127
|
+
const actionSlots =
|
|
128
|
+
slots.actions === undefined ? [] : of(slots.actions);
|
|
115
129
|
const slotTypes = [
|
|
116
130
|
...leadingTypes,
|
|
117
131
|
slots.media,
|
|
@@ -167,6 +181,7 @@ export function SurfaceHeader({
|
|
|
167
181
|
isValidElement(node) ? [node.type as ElementType] : [],
|
|
168
182
|
),
|
|
169
183
|
);
|
|
184
|
+
if (invalidMessage !== undefined) throw new Error(invalidMessage);
|
|
170
185
|
throw new Error(
|
|
171
186
|
distinctLeading.size > 1
|
|
172
187
|
? `${name} aceita ${[...distinctLeading].map(label).join(" ou ")} na região introdutória, nunca os dois na mesma superfície.`
|
|
@@ -174,24 +189,42 @@ export function SurfaceHeader({
|
|
|
174
189
|
);
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
const titleLine = titles.length + counts.length > 0 && (
|
|
193
|
+
<Wrapper className="flex items-baseline gap-2">
|
|
194
|
+
{titles}
|
|
195
|
+
{counts}
|
|
196
|
+
</Wrapper>
|
|
197
|
+
);
|
|
177
198
|
const heading = titles.length +
|
|
178
199
|
counts.length +
|
|
179
200
|
descriptions.length +
|
|
180
|
-
free.length
|
|
201
|
+
free.length +
|
|
202
|
+
(mediaPlacement === "heading" ? medias.length : 0) >
|
|
181
203
|
0 && (
|
|
182
|
-
<
|
|
204
|
+
<Wrapper
|
|
183
205
|
data-slot={`${slot}-heading`}
|
|
184
206
|
className={cn("min-w-0", leadingPlacement === "inline" && "flex-1")}
|
|
185
207
|
>
|
|
186
|
-
{
|
|
187
|
-
<
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
{mediaPlacement === "heading" && medias.length > 0 ? (
|
|
209
|
+
<Wrapper
|
|
210
|
+
data-slot={`${slot}-heading-row`}
|
|
211
|
+
className="flex items-center gap-2"
|
|
212
|
+
>
|
|
213
|
+
{medias}
|
|
214
|
+
<Wrapper data-slot={`${slot}-heading-content`} className="min-w-0">
|
|
215
|
+
{titleLine}
|
|
216
|
+
{descriptions}
|
|
217
|
+
{free}
|
|
218
|
+
</Wrapper>
|
|
219
|
+
</Wrapper>
|
|
220
|
+
) : (
|
|
221
|
+
<>
|
|
222
|
+
{titleLine}
|
|
223
|
+
{descriptions}
|
|
224
|
+
{free}
|
|
225
|
+
</>
|
|
191
226
|
)}
|
|
192
|
-
|
|
193
|
-
{free}
|
|
194
|
-
</div>
|
|
227
|
+
</Wrapper>
|
|
195
228
|
);
|
|
196
229
|
// A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
|
|
197
230
|
// ou de CSS que funcione no header padrão precisa funcionar na barra.
|
|
@@ -200,14 +233,14 @@ export function SurfaceHeader({
|
|
|
200
233
|
{leadingPlacement === "inline" && leading.length > 0 && (
|
|
201
234
|
// `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
|
|
202
235
|
// wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
|
|
203
|
-
<
|
|
236
|
+
<Wrapper
|
|
204
237
|
data-slot={`${slot}-navigation`}
|
|
205
238
|
className={cn("flex min-w-0 items-center", !heading && "flex-1")}
|
|
206
239
|
>
|
|
207
240
|
{leading}
|
|
208
|
-
</
|
|
241
|
+
</Wrapper>
|
|
209
242
|
)}
|
|
210
|
-
{medias}
|
|
243
|
+
{mediaPlacement === "row" && medias}
|
|
211
244
|
{heading}
|
|
212
245
|
{actionSlots}
|
|
213
246
|
{trailing}
|
|
@@ -215,15 +248,15 @@ export function SurfaceHeader({
|
|
|
215
248
|
);
|
|
216
249
|
const layout =
|
|
217
250
|
leadingPlacement === "inline" ? (
|
|
218
|
-
<
|
|
251
|
+
<Wrapper className={cn("flex min-w-0 items-center gap-3", rowClassName)}>
|
|
219
252
|
{row}
|
|
220
|
-
</
|
|
253
|
+
</Wrapper>
|
|
221
254
|
) : leading.length > 0 ? (
|
|
222
255
|
<>
|
|
223
|
-
<
|
|
256
|
+
<Wrapper data-slot={`${slot}-navigation`} className="mb-4">
|
|
224
257
|
{leading}
|
|
225
|
-
</
|
|
226
|
-
<
|
|
258
|
+
</Wrapper>
|
|
259
|
+
<Wrapper
|
|
227
260
|
data-slot={`${slot}-header-row`}
|
|
228
261
|
className={cn(
|
|
229
262
|
actionSlots.length > 0 &&
|
|
@@ -232,7 +265,7 @@ export function SurfaceHeader({
|
|
|
232
265
|
>
|
|
233
266
|
{heading}
|
|
234
267
|
{actionSlots}
|
|
235
|
-
</
|
|
268
|
+
</Wrapper>
|
|
236
269
|
</>
|
|
237
270
|
) : (
|
|
238
271
|
<>
|
|
@@ -242,7 +275,7 @@ export function SurfaceHeader({
|
|
|
242
275
|
);
|
|
243
276
|
|
|
244
277
|
return (
|
|
245
|
-
<
|
|
278
|
+
<Root
|
|
246
279
|
data-slot={`${slot}-header`}
|
|
247
280
|
className={cn(
|
|
248
281
|
contentClassName === undefined &&
|
|
@@ -257,10 +290,10 @@ export function SurfaceHeader({
|
|
|
257
290
|
{contentClassName === undefined ? (
|
|
258
291
|
layout
|
|
259
292
|
) : (
|
|
260
|
-
<
|
|
293
|
+
<Wrapper data-slot={`${slot}-header-content`} className={contentClassName}>
|
|
261
294
|
{layout}
|
|
262
|
-
</
|
|
295
|
+
</Wrapper>
|
|
263
296
|
)}
|
|
264
|
-
</
|
|
297
|
+
</Root>
|
|
265
298
|
);
|
|
266
299
|
}
|
|
@@ -52,7 +52,7 @@ export function Card({
|
|
|
52
52
|
data-slot="card"
|
|
53
53
|
data-layout={layout}
|
|
54
54
|
className={cn(
|
|
55
|
-
"rounded-xl border border-border bg-card text-card-foreground",
|
|
55
|
+
"rounded-xl border border-border bg-card text-card-foreground [--surface-background:var(--card)]",
|
|
56
56
|
// Seccionado: o divisor encosta na borda, então o raio precisa recortar.
|
|
57
57
|
layout === "sectioned" && "overflow-hidden",
|
|
58
58
|
className,
|
|
@@ -230,7 +230,7 @@ export function DialogContent({
|
|
|
230
230
|
const { mode } = useDialogContext();
|
|
231
231
|
const showClose = showCloseButton ?? mode !== "alert";
|
|
232
232
|
const contentClasses =
|
|
233
|
-
"fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
|
|
233
|
+
"fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
|
|
234
234
|
|
|
235
235
|
if (mode === "alert") {
|
|
236
236
|
return (
|
|
@@ -117,7 +117,7 @@ function DrawerContent({
|
|
|
117
117
|
data-slot="drawer-content"
|
|
118
118
|
data-layout={layout}
|
|
119
119
|
className={cn(
|
|
120
|
-
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
120
|
+
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
121
121
|
side === "right" &&
|
|
122
122
|
"inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
123
123
|
side === "left" &&
|
|
@@ -96,10 +96,11 @@ apresentação para o consumidor, como cards de status acima da lista. No modal,
|
|
|
96
96
|
`section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
|
|
97
97
|
Use `advancedFilters.sectionIcons` quando os grupos representarem dimensões recorrentes; a chave é
|
|
98
98
|
o mesmo rótulo de `section`, e cada grupo usa `Content as="fieldset"` para manter uma legenda
|
|
99
|
-
nativa.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
nativa. O ícone ocupa a mídia do grupo, antes do título, e recebe a moldura padrão: passe só o
|
|
100
|
+
glifo. Um `IconTile` é adotado pela mídia com o que declara, e um ícone `null` não desenha nada.
|
|
101
|
+
Quando os grupos precisarem de limites mais explícitos, `advancedFilters.sectionVariant: 'framed'`
|
|
102
|
+
envolve cada seção e pede ao `Content` o acabamento `frameVariant="legend"`, integrando visualmente
|
|
103
|
+
a legenda à borda sem atribuir esse design à semântica de `fieldset`.
|
|
103
104
|
Um filtro `select` pode declarar `searchable: true` quando a lista fechada for longa; a opção geral
|
|
104
105
|
continua disponível e participa da busca. Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
|
|
105
106
|
migração e equivale a `placement: 'advanced'`.
|
|
@@ -138,8 +139,8 @@ filters: {
|
|
|
138
139
|
columns: 2,
|
|
139
140
|
sectionVariant: 'framed',
|
|
140
141
|
sectionIcons: {
|
|
141
|
-
Relacionamento: <
|
|
142
|
-
Datas: <
|
|
142
|
+
Relacionamento: <UserRound />,
|
|
143
|
+
Datas: <CalendarDays />,
|
|
143
144
|
},
|
|
144
145
|
}}
|
|
145
146
|
/>
|
|
@@ -31,21 +31,15 @@ render(
|
|
|
31
31
|
|
|
32
32
|
Use `framed` quando título e corpo precisarem formar uma única região delimitada. A moldura pertence
|
|
33
33
|
ao `Content`; componentes internos, como `DetailGroup`, continuam responsáveis somente pelo layout
|
|
34
|
-
e pela semântica dos próprios dados.
|
|
34
|
+
e pela semântica dos próprios dados. Por padrão, a borda permanece contínua e o header fica dentro
|
|
35
|
+
do recuo da região.
|
|
35
36
|
|
|
36
37
|
```tsx preview col
|
|
37
38
|
render(
|
|
38
39
|
<Content
|
|
39
40
|
framed
|
|
40
|
-
title=
|
|
41
|
-
|
|
42
|
-
<UserRound
|
|
43
|
-
className="size-4 text-muted-foreground"
|
|
44
|
-
aria-hidden="true"
|
|
45
|
-
/>
|
|
46
|
-
Cliente
|
|
47
|
-
</span>
|
|
48
|
-
}
|
|
41
|
+
title="Cliente"
|
|
42
|
+
media={<UserRound aria-hidden="true" />}
|
|
49
43
|
>
|
|
50
44
|
<DetailGroup columns={2}>
|
|
51
45
|
<DetailField label="Nome" value="Carla Souza" />
|
|
@@ -55,16 +49,66 @@ render(
|
|
|
55
49
|
);
|
|
56
50
|
```
|
|
57
51
|
|
|
52
|
+
## Header integrado à moldura
|
|
53
|
+
|
|
54
|
+
Quando uma região compacta precisar integrar visualmente o header à borda, use
|
|
55
|
+
`frameVariant="legend"`. O acabamento não muda a semântica: uma região de leitura continua sendo
|
|
56
|
+
uma `section`, o título continua sendo um heading e descrição, contador e ações permanecem
|
|
57
|
+
disponíveis.
|
|
58
|
+
|
|
59
|
+
```tsx preview col
|
|
60
|
+
render(
|
|
61
|
+
<Content
|
|
62
|
+
framed
|
|
63
|
+
frameVariant="legend"
|
|
64
|
+
title="Veículo"
|
|
65
|
+
media={<CarFront aria-hidden="true" />}
|
|
66
|
+
>
|
|
67
|
+
<DetailGroup columns={2}>
|
|
68
|
+
<DetailField label="Placa" value="ABC1D23" />
|
|
69
|
+
<DetailField label="Modelo" value="Corolla XEi" />
|
|
70
|
+
</DetailGroup>
|
|
71
|
+
</Content>,
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Para interromper a borda, o header pinta o título e as ações com o fundo da superfície que hospeda o
|
|
76
|
+
`Content`, lido da variável CSS `--surface-background`. `Card`, `Dialog` e `Drawer` já a declaram
|
|
77
|
+
com o fundo que pintam; fora deles, vale `--background`. Uma superfície com fundo próprio, como um
|
|
78
|
+
`Card` com `bg-*`, um `Popover` ou um contêiner da aplicação, precisa declarar a variável com o
|
|
79
|
+
mesmo fundo. Sem isso, a faixa atrás do título aparece com a cor da página.
|
|
80
|
+
|
|
81
|
+
```tsx preview col
|
|
82
|
+
render(
|
|
83
|
+
<Card className="bg-muted [--surface-background:var(--muted)]">
|
|
84
|
+
<CardBody>
|
|
85
|
+
<Content framed frameVariant="legend" title="Veículo">
|
|
86
|
+
<DetailGroup columns={2}>
|
|
87
|
+
<DetailField label="Placa" value="ABC1D23" />
|
|
88
|
+
<DetailField label="Modelo" value="Corolla XEi" />
|
|
89
|
+
</DetailGroup>
|
|
90
|
+
</Content>
|
|
91
|
+
</CardBody>
|
|
92
|
+
</Card>,
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
58
96
|
## Grupo de campos
|
|
59
97
|
|
|
60
98
|
Quando a região nomear um conjunto de campos relacionados, use `as="fieldset"`. O `Content`
|
|
61
99
|
renderiza o título como `legend`, preserva as props nativas do `fieldset` e mantém o mesmo contrato
|
|
62
100
|
de título e corpo. Essa escolha altera somente a semântica e o comportamento nativo; com `framed`,
|
|
63
|
-
a moldura permanece contínua.
|
|
101
|
+
a moldura permanece contínua. A mídia usa a mesma apresentação da `section`; contador, descrição e
|
|
102
|
+
ações não entram na legenda.
|
|
64
103
|
|
|
65
104
|
```tsx preview col
|
|
66
105
|
render(
|
|
67
|
-
<Content
|
|
106
|
+
<Content
|
|
107
|
+
as="fieldset"
|
|
108
|
+
framed
|
|
109
|
+
title="Proprietário"
|
|
110
|
+
media={<UserRound aria-hidden="true" />}
|
|
111
|
+
>
|
|
68
112
|
<FieldGroup>
|
|
69
113
|
<Field>
|
|
70
114
|
<FieldLabel htmlFor="owner-name">Nome</FieldLabel>
|
|
@@ -79,8 +123,9 @@ render(
|
|
|
79
123
|
);
|
|
80
124
|
```
|
|
81
125
|
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
O mesmo acabamento pode integrar a legenda nativa à borda. Declare `frameVariant="legend"` em
|
|
127
|
+
agrupamentos compactos, como as seções de filtros avançados; `as="fieldset"` continua sendo a
|
|
128
|
+
decisão semântica, e não uma forma de obter esse desenho.
|
|
84
129
|
|
|
85
130
|
```tsx preview col
|
|
86
131
|
render(
|
|
@@ -104,13 +149,16 @@ render(
|
|
|
104
149
|
|
|
105
150
|
Use os slots quando o header precisar de composição própria. A árvore aceita exatamente um
|
|
106
151
|
`ContentHeader` e um `ContentBody` como filhos diretos. O header exige um `ContentTitle` e aceita
|
|
107
|
-
uma descrição, um contador (`ContentMeta`) e uma região de ações. O
|
|
108
|
-
`Content`; `PageHeader` reconhece somente navegação, título e ações.
|
|
152
|
+
uma mídia (`ContentMedia`), uma descrição, um contador (`ContentMeta`) e uma região de ações. O
|
|
153
|
+
contador é próprio de `Content`; `PageHeader` reconhece somente navegação, título e ações.
|
|
109
154
|
|
|
110
155
|
```tsx preview col
|
|
111
156
|
render(
|
|
112
157
|
<Content level={2}>
|
|
113
158
|
<ContentHeader>
|
|
159
|
+
<ContentMedia>
|
|
160
|
+
<Monitor aria-hidden="true" />
|
|
161
|
+
</ContentMedia>
|
|
114
162
|
<ContentTitle>Dispositivos conectados</ContentTitle>
|
|
115
163
|
<ContentMeta>3</ContentMeta>
|
|
116
164
|
<ContentDescription>Sessões com acesso à sua conta.</ContentDescription>
|
|
@@ -166,6 +214,7 @@ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio
|
|
|
166
214
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
167
215
|
| -------------- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
168
216
|
| `title` | `ReactNode` | | Forma curta: título da região (vira o heading ligado à `section`). |
|
|
217
|
+
| `media` | `ReactNode` | | Forma curta: ícone ou imagem que identifica a região antes do título. |
|
|
169
218
|
| `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
|
|
170
219
|
| `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
|
|
171
220
|
| `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
|
|
@@ -173,11 +222,30 @@ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio
|
|
|
173
222
|
| `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
|
|
174
223
|
| `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
|
|
175
224
|
| `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
|
|
176
|
-
| `frameVariant` | `'default' \| 'legend'` | `'default'` |
|
|
225
|
+
| `frameVariant` | `'default' \| 'legend'` | `'default'` | Mantém a borda contínua ou integra visualmente o header à moldura. Não altera o elemento nem sua semântica. |
|
|
177
226
|
| `children` | `ReactNode` | | Corpo da região na forma curta, ou `ContentHeader` e `ContentBody` na composição explícita. |
|
|
178
227
|
|
|
179
228
|
As demais propriedades são as nativas de `section` ou, com `as="fieldset"`, de `fieldset`.
|
|
180
229
|
|
|
181
|
-
Na composição explícita
|
|
182
|
-
`
|
|
183
|
-
|
|
230
|
+
Na composição explícita de uma `section`, `ContentHeader` recebe `ContentMedia`, `ContentTitle`,
|
|
231
|
+
`ContentMeta`, `ContentDescription` e `ContentActions`. Em `fieldset`, recebe `ContentMedia` e
|
|
232
|
+
`ContentTitle`, preservando a apresentação enquanto a legenda continua nomeando somente o grupo de
|
|
233
|
+
campos. `ContentBody` recebe o conteúdo; nenhum desses slots aceita `title` ou `level` próprios — a
|
|
234
|
+
hierarquia é declarada em `Content`. `ContentHeader` não existe fora de `Content`.
|
|
235
|
+
|
|
236
|
+
## Propriedades de ContentMedia
|
|
237
|
+
|
|
238
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
239
|
+
| ----------- | ------------------------------------------------------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
240
|
+
| `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'xs'` | Tamanho da moldura e do glifo na escala compartilhada dos controles. |
|
|
241
|
+
| `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Declara o significado da cor. |
|
|
242
|
+
| `variant` | `'subtle' \| 'outline'` | `'subtle'` | Usa preenchimento tonal ou acrescenta borda. |
|
|
243
|
+
| `className` | `string` | | Ajusta a moldura sem substituir a escala. |
|
|
244
|
+
| `asChild` | `boolean` | `false` | Cede o elemento para uma marca com visual próprio — um orbe, um logotipo — que ocupa o slot sem ganhar a moldura por cima. |
|
|
245
|
+
|
|
246
|
+
`ContentMedia` já é a moldura. Passe só o glifo e escolha `size`, `context` e `variant` no slot. Um
|
|
247
|
+
`IconTile` passado em `media` ou dentro de `ContentMedia` não vira moldura dentro de moldura: o slot
|
|
248
|
+
assume o lugar dele e herda o que ele declara. `media` com `null` ou `false` não desenha nada.
|
|
249
|
+
|
|
250
|
+
Em `fieldset`, a mídia é sempre decorativa: o `Content` a esconde da árvore de acessibilidade para
|
|
251
|
+
que o grupo seja anunciado apenas pelo título.
|
|
@@ -13,24 +13,24 @@ com tamanho de ícone e um nome acessível.
|
|
|
13
13
|
O tamanho controla a caixa e o glifo pela escala compartilhada dos controles. Um ícone que declara
|
|
14
14
|
uma classe `size-*` preserva a própria medida.
|
|
15
15
|
|
|
16
|
-
## Em
|
|
16
|
+
## Em slots de ícone e de mídia
|
|
17
17
|
|
|
18
|
-
Slots de ícone não criam a moldura
|
|
19
|
-
|
|
18
|
+
Slots genéricos de ícone, como `FieldLegend.icon`, não criam a moldura: passe o glifo quando quiser
|
|
19
|
+
só o ícone e componha `IconTile` quando a moldura reforçar a hierarquia.
|
|
20
|
+
|
|
21
|
+
Os slots de mídia das superfícies já são um `IconTile`: `CardMedia`, `ContentMedia`, `DialogMedia`,
|
|
22
|
+
`DrawerMedia` e `PaneMedia` desenham a moldura sozinhos. Passe só o glifo e escolha `size`,
|
|
23
|
+
`context` e `variant` no próprio slot. Em [`Content`](/ui/content), um `IconTile` passado em
|
|
24
|
+
`media` é adotado pelo slot, que herda o que ele declara, em vez de desenhar uma moldura dentro da
|
|
25
|
+
outra.
|
|
20
26
|
|
|
21
27
|
```tsx preview col md
|
|
22
28
|
<Content
|
|
23
29
|
as="fieldset"
|
|
24
30
|
framed
|
|
25
31
|
frameVariant="legend"
|
|
26
|
-
title=
|
|
27
|
-
|
|
28
|
-
<IconTile size="xs">
|
|
29
|
-
<CarFront />
|
|
30
|
-
</IconTile>
|
|
31
|
-
Veículo
|
|
32
|
-
</span>
|
|
33
|
-
}
|
|
32
|
+
title="Veículo"
|
|
33
|
+
media={<CarFront aria-hidden="true" />}
|
|
34
34
|
>
|
|
35
35
|
<FieldGroup>
|
|
36
36
|
<Field>
|
package/src/ui/meta.ts
CHANGED
|
@@ -123,7 +123,7 @@ export const componentMeta = {
|
|
|
123
123
|
name: "content-header",
|
|
124
124
|
ancestry: "opus",
|
|
125
125
|
whenToUse:
|
|
126
|
-
'Estruture uma região com título e corpo. No caso comum, declare título, descrição, contador e ações diretamente em Content; use `framed` quando a região precisar de limite próprio, `
|
|
126
|
+
'Estruture uma região com título e corpo. No caso comum, declare título, mídia, descrição, contador e ações diretamente em Content; use `framed` quando a região precisar de limite próprio, `frameVariant="legend"` para integrar visualmente o header à borda e `as="fieldset"` somente para agrupar campos sob uma legenda nativa. Componha ContentHeader apenas quando precisar controlar a anatomia.',
|
|
127
127
|
},
|
|
128
128
|
copyable: {
|
|
129
129
|
name: "copyable",
|