@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 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 | não | não | sim (`DialogMedia`) | sim | 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "20.0.0",
3
+ "version": "20.1.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- frameVariant?: "default";
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 && "rounded-lg border border-border p-4",
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
- <legend
260
- data-slot="content-header"
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
- </legend>
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
- <span className="flex items-center gap-2">
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: ElementType;
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 = of(slots.actions);
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
- <div
204
+ <Wrapper
183
205
  data-slot={`${slot}-heading`}
184
206
  className={cn("min-w-0", leadingPlacement === "inline" && "flex-1")}
185
207
  >
186
- {titles.length + counts.length > 0 && (
187
- <div className="flex items-baseline gap-2">
188
- {titles}
189
- {counts}
190
- </div>
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
- {descriptions}
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
- <div
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
- </div>
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
- <div className={cn("flex min-w-0 items-center gap-3", rowClassName)}>
251
+ <Wrapper className={cn("flex min-w-0 items-center gap-3", rowClassName)}>
219
252
  {row}
220
- </div>
253
+ </Wrapper>
221
254
  ) : leading.length > 0 ? (
222
255
  <>
223
- <div data-slot={`${slot}-navigation`} className="mb-4">
256
+ <Wrapper data-slot={`${slot}-navigation`} className="mb-4">
224
257
  {leading}
225
- </div>
226
- <div
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
- </div>
268
+ </Wrapper>
236
269
  </>
237
270
  ) : (
238
271
  <>
@@ -242,7 +275,7 @@ export function SurfaceHeader({
242
275
  );
243
276
 
244
277
  return (
245
- <div
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
- <div data-slot={`${slot}-header-content`} className={contentClassName}>
293
+ <Wrapper data-slot={`${slot}-header-content`} className={contentClassName}>
261
294
  {layout}
262
- </div>
295
+ </Wrapper>
263
296
  )}
264
- </div>
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. Quando os grupos precisarem de limites mais explícitos,
100
- `advancedFilters.sectionVariant: 'framed'` envolve cada seção e pede ao `Content` o acabamento
101
- `frameVariant="legend"`, integrando visualmente a legenda à borda sem atribuir esse design à
102
- semântica de `fieldset`.
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: <IconTile size="xs"><UserRound /></IconTile>,
142
- Datas: <IconTile size="xs"><CalendarDays /></IconTile>,
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
- <span className="flex items-center gap-2">
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. Nesse modo, contador, descrição e ações não entram na legenda.
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 as="fieldset" framed title="Proprietário">
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
- Quando a legenda precisar integrar visualmente a borda, declare essa escolha separadamente. Use
83
- `frameVariant="legend"` em agrupamentos compactos, como as seções de filtros avançados.
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 contador é próprio de
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'` | Em `fieldset` com moldura, mantém a borda contínua ou integra visualmente a legenda à borda. Não altera a semântica. |
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, `ContentHeader` recebe `ContentTitle`, `ContentMeta`, `ContentDescription` e
182
- `ContentActions`, e `ContentBody` recebe o conteúdo; nenhum desses slots aceita `title` ou `level`
183
- próprios — a hierarquia é declarada em `Content`. `ContentHeader` não existe fora de `Content`.
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 uma legenda
16
+ ## Em slots de ícone e de mídia
17
17
 
18
- Slots de ícone não criam a moldura automaticamente. Componha `IconTile` somente quando ela fizer
19
- sentido para a superfície, como na legenda de um [`Content`](/ui/content) emoldurado.
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
- <span className="flex items-center gap-2">
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, `as="fieldset"` para agrupar campos sob uma legenda nativa e `frameVariant="legend"` somente quando essa legenda precisar integrar visualmente a borda. Componha ContentHeader apenas quando precisar controlar a anatomia.',
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",
package/src/ui/react.tsx CHANGED
@@ -487,6 +487,7 @@ export type {
487
487
  export {
488
488
  Content,
489
489
  ContentHeader,
490
+ ContentMedia,
490
491
  ContentTitle,
491
492
  ContentDescription,
492
493
  ContentMeta,