@softize/opus 22.2.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/bin/cli.mjs +3 -9
  3. package/bin/lib/check.mjs +242 -49
  4. package/bin/lib/copy.mjs +17 -35
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
  16. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  17. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  18. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  19. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  20. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  21. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  22. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  23. package/docs/code-style.md +1 -1
  24. package/package.json +5 -1
  25. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  26. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  27. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  28. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  29. package/registry/templates/app/src/App.tsx +24 -12
  30. package/src/core/contracts.ts +1 -1
  31. package/src/core/runtime.ts +4 -37
  32. package/src/core/types.ts +5 -23
  33. package/src/design/index.ts +14 -3
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/appearance.ts +149 -0
  36. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  37. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  38. package/src/ui/components/patterns/confirm.tsx +3 -1
  39. package/src/ui/components/patterns/fill-region.ts +8 -7
  40. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  41. package/src/ui/components/patterns/form.tsx +8 -7
  42. package/src/ui/components/patterns/list.tsx +64 -14
  43. package/src/ui/components/patterns/page.tsx +15 -11
  44. package/src/ui/components/patterns/pane.tsx +9 -240
  45. package/src/ui/components/patterns/presentation.tsx +14 -14
  46. package/src/ui/components/patterns/sidebar.tsx +7 -17
  47. package/src/ui/components/patterns/split.tsx +111 -59
  48. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  49. package/src/ui/components/patterns/structural-media.tsx +26 -0
  50. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  51. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  52. package/src/ui/components/patterns/surface.tsx +620 -0
  53. package/src/ui/components/patterns/trigger.tsx +11 -9
  54. package/src/ui/components/primitives/ask.tsx +155 -83
  55. package/src/ui/components/primitives/button.tsx +11 -9
  56. package/src/ui/components/primitives/checkbox.tsx +8 -8
  57. package/src/ui/components/primitives/command.tsx +1 -1
  58. package/src/ui/components/primitives/composer.tsx +47 -45
  59. package/src/ui/components/primitives/detail.tsx +9 -2
  60. package/src/ui/components/primitives/dialog.tsx +60 -26
  61. package/src/ui/components/primitives/drawer.tsx +58 -12
  62. package/src/ui/components/primitives/icon-picker.tsx +93 -70
  63. package/src/ui/components/primitives/input-group.tsx +35 -28
  64. package/src/ui/components/primitives/input-otp.tsx +16 -16
  65. package/src/ui/components/primitives/input.tsx +20 -10
  66. package/src/ui/components/primitives/item.tsx +9 -12
  67. package/src/ui/components/primitives/menu.tsx +44 -65
  68. package/src/ui/components/primitives/metric-card.tsx +75 -59
  69. package/src/ui/components/primitives/radio-group.tsx +9 -9
  70. package/src/ui/components/primitives/select.tsx +344 -209
  71. package/src/ui/components/primitives/switch.tsx +10 -10
  72. package/src/ui/components/primitives/table.tsx +29 -24
  73. package/src/ui/components/primitives/tabs.tsx +58 -88
  74. package/src/ui/components/primitives/textarea.tsx +6 -6
  75. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  76. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  77. package/src/ui/components/primitives/toggle.tsx +82 -16
  78. package/src/ui/docs/DocBrowser.tsx +77 -69
  79. package/src/ui/docs/content/action-form-card.md +9 -9
  80. package/src/ui/docs/content/action-list.md +85 -70
  81. package/src/ui/docs/content/action-view.md +33 -18
  82. package/src/ui/docs/content/actions.md +6 -7
  83. package/src/ui/docs/content/button.md +1 -1
  84. package/src/ui/docs/content/communication.md +1 -1
  85. package/src/ui/docs/content/customization.md +64 -11
  86. package/src/ui/docs/content/detail.md +8 -6
  87. package/src/ui/docs/content/dialog.md +100 -58
  88. package/src/ui/docs/content/drawer.md +63 -19
  89. package/src/ui/docs/content/field.md +14 -15
  90. package/src/ui/docs/content/icon-tile.md +14 -13
  91. package/src/ui/docs/content/page.md +34 -26
  92. package/src/ui/docs/content/pane.md +41 -290
  93. package/src/ui/docs/content/presentation.md +3 -3
  94. package/src/ui/docs/content/semantic-context.md +0 -1
  95. package/src/ui/docs/content/separator.md +6 -6
  96. package/src/ui/docs/content/sidebar.md +68 -266
  97. package/src/ui/docs/content/skeleton.md +20 -18
  98. package/src/ui/docs/content/split.md +14 -14
  99. package/src/ui/docs/content/surface.md +513 -0
  100. package/src/ui/docs/content/tabs.md +82 -83
  101. package/src/ui/docs/content/testing.md +2 -3
  102. package/src/ui/docs/content/toggle.md +120 -63
  103. package/src/ui/docs/content/tokens.md +124 -40
  104. package/src/ui/docs/registry.tsx +4 -6
  105. package/src/ui/drivers/react.tsx +4 -3
  106. package/src/ui/index.ts +30 -27
  107. package/src/ui/meta.ts +15 -27
  108. package/src/ui/react.tsx +42 -46
  109. package/src/ui/theme.css +311 -64
  110. package/src/vite/design.ts +23 -21
  111. package/src/ui/components/patterns/content-header.tsx +0 -532
  112. package/src/ui/components/patterns/surface-media.tsx +0 -69
  113. package/src/ui/components/primitives/card.tsx +0 -191
  114. package/src/ui/docs/content/card.md +0 -292
  115. package/src/ui/docs/content/content.md +0 -276
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Surface — a fundação pública para delimitar e nomear um trecho da tela: região de
3
+ * página, grupo de campos, entidade, item ou bloco clicável (ADR 0045).
4
+ *
5
+ * A semântica vem do elemento (`as` ou `asChild`), nunca do nome do componente. Três
6
+ * eixos visuais são independentes:
7
+ *
8
+ * framed borda, raio e recorte;
9
+ * elevation fundo próprio (`raised`) ou o fundo de quem hospeda (`none`);
10
+ * layout ritmo contínuo ou faixas com divisores derivados.
11
+ *
12
+ * Três formas de compor produzem a mesma anatomia:
13
+ *
14
+ * Caixa simples: <Surface framed className="p-4">…</Surface>
15
+ * Forma curta: <Surface as="section" title="Clientes" count={3}>…</Surface>
16
+ * Explícita: <Surface><SurfaceHeader>…</SurfaceHeader><SurfaceBody>…</SurfaceBody></Surface>
17
+ *
18
+ * O recuo das regiões deriva de a superfície ser delimitada (moldura ou elevação); sem
19
+ * delimitação, ela flui alinhada ao hospedeiro. As medidas vivem em `surface-anatomy.ts`.
20
+ */
21
+ import {
22
+ Children,
23
+ Fragment,
24
+ createContext,
25
+ isValidElement,
26
+ useContext,
27
+ useId,
28
+ type ComponentProps,
29
+ type ElementType,
30
+ type HTMLAttributes,
31
+ type ReactElement,
32
+ type ReactNode,
33
+ } from "react";
34
+ import { Slot } from "@radix-ui/react-slot";
35
+ import { cn } from "../../lib/cn.ts";
36
+ import {
37
+ FillBoundary,
38
+ fillRegionClassName,
39
+ useFillRegion,
40
+ } from "./fill-region.ts";
41
+ import { headingTags, type HeadingLevel } from "./heading-level.ts";
42
+ import { usePageHeadingPresence } from "./page-heading-context.tsx";
43
+ import {
44
+ StructuralHeader,
45
+ structuralHeaderClasses,
46
+ } from "./structural-header.tsx";
47
+ import {
48
+ surfaceDescriptionClasses,
49
+ surfaceHeaderRowClasses,
50
+ surfaceLegendClasses,
51
+ surfaceRegionClasses,
52
+ surfaceTitleClasses,
53
+ type SurfaceLayout,
54
+ } from "./surface-anatomy.ts";
55
+
56
+ export type SurfaceElement = "div" | "section" | "article" | "fieldset";
57
+ export type SurfaceElevation = "none" | "raised";
58
+
59
+ interface SurfaceContextValue {
60
+ /** Elemento da raiz; `null` quando `asChild` entrega a superfície ao filho. */
61
+ element: SurfaceElement | null;
62
+ /** Tem moldura ou elevação: as regiões carregam o recuo. */
63
+ bounded: boolean;
64
+ framed: boolean;
65
+ layout: SurfaceLayout;
66
+ level: HeadingLevel;
67
+ titleId: string;
68
+ variant: "page" | "section";
69
+ }
70
+
71
+ const SurfaceContext = createContext<SurfaceContextValue | null>(null);
72
+ const SurfaceHeaderContext = createContext<SurfaceContextValue | null>(null);
73
+
74
+ function requiredContext<T>(
75
+ value: T | null,
76
+ component: string,
77
+ parent: string,
78
+ ): T {
79
+ if (value === null)
80
+ throw new Error(`${component} deve estar dentro de ${parent}.`);
81
+ return value;
82
+ }
83
+
84
+ /** Como a `legend` de um fieldset delimitado se relaciona com a moldura. */
85
+ function legendPlacement(
86
+ surface: SurfaceContextValue,
87
+ ): "notch" | "band" | null {
88
+ if (surface.element !== "fieldset" || !surface.bounded) return null;
89
+ return surface.framed && surface.layout === "continuous" ? "notch" : "band";
90
+ }
91
+
92
+ interface SurfaceBaseProps {
93
+ /** Borda, raio e recorte. Não decide fundo nem semântica. */
94
+ framed?: boolean;
95
+ /**
96
+ * `raised` pinta o fundo de superfície elevada e decide a própria altura; `none` mostra
97
+ * o fundo de quem hospeda.
98
+ */
99
+ elevation?: SurfaceElevation;
100
+ /** Anatomia da superfície inteira: `continuous` (default) ou `sectioned`. */
101
+ layout?: SurfaceLayout;
102
+ className?: string;
103
+ children?: ReactNode;
104
+ }
105
+
106
+ interface SurfaceHeaderShorthandProps {
107
+ /** Forma curta: título da superfície. */
108
+ title: ReactNode;
109
+ /** Forma curta: conteúdo do slot de mídia, antes do título. */
110
+ media?: ReactNode;
111
+ /** Forma curta: total de itens ao lado do título. */
112
+ count?: number;
113
+ /** Forma curta: frase de apoio sob o título. */
114
+ description?: ReactNode;
115
+ /** Forma curta: ações sobre a superfície inteira, no extremo oposto ao título. */
116
+ actions?: ReactNode;
117
+ }
118
+
119
+ interface SurfaceLegendShorthandProps {
120
+ /** Forma curta: título do grupo de campos, renderizado como `legend`. */
121
+ title: ReactNode;
122
+ /** Forma curta: mídia decorativa que acompanha o título na legenda. */
123
+ media?: ReactNode;
124
+ count?: never;
125
+ description?: never;
126
+ actions?: never;
127
+ }
128
+
129
+ interface SurfaceNoShorthandProps {
130
+ title?: never;
131
+ media?: never;
132
+ count?: never;
133
+ description?: never;
134
+ actions?: never;
135
+ }
136
+
137
+ type NativeProps<T extends ElementType> = Omit<
138
+ ComponentProps<T>,
139
+ "title" | "children" | "className"
140
+ >;
141
+
142
+ type SurfaceDivProps = SurfaceBaseProps &
143
+ NativeProps<"div"> & {
144
+ /** Elemento da superfície. */
145
+ as?: "div";
146
+ asChild?: false;
147
+ /** Nível semântico do título; `1` é o heading principal da página. */
148
+ level?: HeadingLevel;
149
+ } & (SurfaceHeaderShorthandProps | SurfaceNoShorthandProps);
150
+
151
+ type SurfaceSectioningProps = SurfaceBaseProps &
152
+ NativeProps<"section"> & {
153
+ /** Região nomeada pelo título (`section`) ou conteúdo autossuficiente (`article`). */
154
+ as: "section" | "article";
155
+ asChild?: false;
156
+ level?: HeadingLevel;
157
+ } & (SurfaceHeaderShorthandProps | SurfaceNoShorthandProps);
158
+
159
+ type SurfaceFieldsetProps = SurfaceBaseProps &
160
+ NativeProps<"fieldset"> & {
161
+ /** Grupo de campos; o header vira a `legend` nativa. */
162
+ as: "fieldset";
163
+ asChild?: false;
164
+ level?: never;
165
+ } & (SurfaceLegendShorthandProps | SurfaceNoShorthandProps);
166
+
167
+ type SurfaceAsChildProps = SurfaceBaseProps &
168
+ NativeProps<"div"> & {
169
+ as?: never;
170
+ /** Entrega a superfície ao único filho, como um `button` ou um link. */
171
+ asChild: true;
172
+ level?: HeadingLevel;
173
+ } & SurfaceNoShorthandProps;
174
+
175
+ export type SurfaceProps =
176
+ | SurfaceDivProps
177
+ | SurfaceSectioningProps
178
+ | SurfaceFieldsetProps
179
+ | SurfaceAsChildProps;
180
+
181
+ /** Expande fragments: a composição pode agrupar regiões em um `<>`. */
182
+ function flatten(children: ReactNode): ReactNode[] {
183
+ return Children.toArray(children).flatMap((node) =>
184
+ isValidElement(node) && node.type === Fragment
185
+ ? flatten((node.props as { children?: ReactNode }).children)
186
+ : [node],
187
+ );
188
+ }
189
+
190
+ /** `null`, `false` e `undefined` não abrem o slot: uma mídia vazia não identifica nada. */
191
+ function hasMedia(media: ReactNode): boolean {
192
+ return media !== undefined && media !== null && media !== false;
193
+ }
194
+
195
+ export function Surface({
196
+ as,
197
+ asChild = false,
198
+ framed = false,
199
+ elevation = "none",
200
+ layout = "continuous",
201
+ level = 2,
202
+ title,
203
+ media,
204
+ count,
205
+ description,
206
+ actions,
207
+ className,
208
+ children,
209
+ ...props
210
+ }: SurfaceProps): ReactElement {
211
+ const titleId = useId();
212
+ const fill = useFillRegion();
213
+
214
+ if (asChild && as !== undefined) {
215
+ throw new Error("Surface não combina as com asChild: escolha um dos dois.");
216
+ }
217
+ const element: SurfaceElement | null = asChild ? null : (as ?? "div");
218
+ const raised = elevation === "raised";
219
+ const bounded = framed || raised;
220
+ if (layout === "sectioned" && !bounded) {
221
+ throw new Error(
222
+ 'Surface layout="sectioned" exige framed ou elevation="raised".',
223
+ );
224
+ }
225
+
226
+ const shorthand = title !== undefined;
227
+ if (
228
+ !shorthand &&
229
+ (hasMedia(media) ||
230
+ count !== undefined ||
231
+ description !== undefined ||
232
+ actions !== undefined)
233
+ ) {
234
+ throw new Error(
235
+ "Surface exige title para usar media, count, description ou actions.",
236
+ );
237
+ }
238
+ if (asChild && shorthand) {
239
+ throw new Error(
240
+ "Surface asChild não aceita a forma curta: componha os slots dentro do filho.",
241
+ );
242
+ }
243
+ if (asChild && !isValidElement(children)) {
244
+ throw new Error("Surface asChild exige exatamente um elemento filho.");
245
+ }
246
+
247
+ // Com `asChild`, a anatomia é a dos filhos do elemento que recebe a superfície.
248
+ const nodes = flatten(
249
+ asChild
250
+ ? (children as ReactElement<{ children?: ReactNode }>).props.children
251
+ : children,
252
+ );
253
+ const of = (type: ElementType) =>
254
+ nodes.filter((node) => isValidElement(node) && node.type === type);
255
+ const headers = of(SurfaceHeader);
256
+ const bodies = of(SurfaceBody);
257
+ const footers = of(SurfaceFooter);
258
+ const regions = headers.length + bodies.length + footers.length;
259
+
260
+ if (regions > 0 && regions !== nodes.length) {
261
+ throw new Error(
262
+ "Surface não mistura slots estruturais com filhos livres: envolva o conteúdo em SurfaceBody.",
263
+ );
264
+ }
265
+ if (headers.length > 1 || bodies.length > 1 || footers.length > 1) {
266
+ throw new Error(
267
+ "Surface aceita no máximo um SurfaceHeader, um SurfaceBody e um SurfaceFooter como filhos diretos.",
268
+ );
269
+ }
270
+ if (shorthand && headers.length > 0) {
271
+ throw new Error(
272
+ "Surface não permite misturar a forma curta com SurfaceHeader explícito.",
273
+ );
274
+ }
275
+
276
+ const explicitTitle =
277
+ headers.length === 1 && isValidElement(headers[0])
278
+ ? flatten(
279
+ (headers[0] as ReactElement<{ children?: ReactNode }>).props.children,
280
+ ).find((node) => isValidElement(node) && node.type === SurfaceTitle)
281
+ : undefined;
282
+ const resolvedTitleId =
283
+ (isValidElement(explicitTitle)
284
+ ? (explicitTitle.props as { id?: string }).id
285
+ : undefined) ?? titleId;
286
+
287
+ // Caixa simples: sem título e sem slots, os filhos são livres e a raiz não impõe ritmo.
288
+ const structured = shorthand || regions > 0;
289
+ const variant = level === 1 && element !== "fieldset" ? "page" : "section";
290
+ const context: SurfaceContextValue = {
291
+ element,
292
+ bounded,
293
+ framed,
294
+ layout,
295
+ level,
296
+ titleId: resolvedTitleId,
297
+ variant,
298
+ };
299
+
300
+ const anatomy = shorthand ? (
301
+ <>
302
+ <SurfaceHeader>
303
+ {hasMedia(media) && <SurfaceMedia>{media}</SurfaceMedia>}
304
+ <SurfaceTitle>{title}</SurfaceTitle>
305
+ {count !== undefined && <SurfaceMeta>{count}</SurfaceMeta>}
306
+ {description !== undefined && (
307
+ <SurfaceDescription>{description}</SurfaceDescription>
308
+ )}
309
+ {actions !== undefined && <SurfaceActions>{actions}</SurfaceActions>}
310
+ </SurfaceHeader>
311
+ {regions > 0
312
+ ? children
313
+ : nodes.length > 0 && <SurfaceBody>{children}</SurfaceBody>}
314
+ </>
315
+ ) : (
316
+ children
317
+ );
318
+
319
+ // Uma superfície elevada decide a própria altura; a caixa simples não é um elo. As
320
+ // demais repassam a altura de `Page layout="fill"` até a lista (ADR 0024).
321
+ const link = structured && !raised;
322
+ const named =
323
+ (element === "section" ||
324
+ element === "article" ||
325
+ element === "fieldset") &&
326
+ (shorthand || explicitTitle !== undefined) &&
327
+ props["aria-label"] === undefined;
328
+
329
+ const Root: ElementType = element ?? Slot;
330
+ const root = (
331
+ <Root
332
+ data-slot="surface"
333
+ data-as={element ?? undefined}
334
+ data-framed={framed}
335
+ data-elevation={elevation}
336
+ data-layout={layout}
337
+ {...(link ? fill.attributes : {})}
338
+ aria-labelledby={named ? resolvedTitleId : undefined}
339
+ className={cn(
340
+ framed && "rounded-xl border border-border",
341
+ // O divisor e o conteúdo sem recuo encostam na borda: o raio precisa recortar.
342
+ framed && layout === "sectioned" && "overflow-hidden",
343
+ framed && "has-[>[data-flush=true]]:overflow-hidden",
344
+ raised &&
345
+ "bg-card text-card-foreground [--surface-background:var(--card)]",
346
+ structured &&
347
+ !bounded &&
348
+ (variant === "page" ? "space-y-6" : "space-y-3"),
349
+ link && fill.filling && fillRegionClassName,
350
+ className,
351
+ )}
352
+ {...props}
353
+ >
354
+ {anatomy}
355
+ </Root>
356
+ );
357
+
358
+ return (
359
+ <SurfaceContext.Provider value={context}>
360
+ {raised ? (
361
+ <FillBoundary>{root}</FillBoundary>
362
+ ) : link ? (
363
+ fill.provide(root)
364
+ ) : (
365
+ root
366
+ )}
367
+ </SurfaceContext.Provider>
368
+ );
369
+ }
370
+
371
+ export type SurfaceHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
372
+
373
+ const legendInvalidMessage =
374
+ "SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição e ações ficam fora da legenda.";
375
+
376
+ /**
377
+ * Header da superfície; não existe fora dela. Monta a linha sozinho — mídia, título com
378
+ * contador e descrição de um lado, ações no extremo oposto —, qualquer que seja a ordem
379
+ * em que os slots são escritos. Em `fieldset`, é a `legend` nativa.
380
+ */
381
+ export function SurfaceHeader({
382
+ className,
383
+ children,
384
+ ...props
385
+ }: SurfaceHeaderProps): ReactElement {
386
+ const surface = requiredContext(
387
+ useContext(SurfaceContext),
388
+ "SurfaceHeader",
389
+ "Surface",
390
+ );
391
+ const legend = surface.element === "fieldset";
392
+ const placement = legendPlacement(surface);
393
+ return (
394
+ <SurfaceHeaderContext.Provider value={surface}>
395
+ <StructuralHeader
396
+ as={legend ? "legend" : "div"}
397
+ name="SurfaceHeader"
398
+ slot="surface"
399
+ slots={
400
+ legend
401
+ ? { media: SurfaceMedia, title: SurfaceTitle }
402
+ : {
403
+ media: SurfaceMedia,
404
+ title: SurfaceTitle,
405
+ count: SurfaceMeta,
406
+ description: SurfaceDescription,
407
+ actions: SurfaceActions,
408
+ }
409
+ }
410
+ leadingPlacement="inline"
411
+ titleRequired={legend}
412
+ rowClassName={surfaceHeaderRowClasses("surface")}
413
+ {...(legend ? { invalidMessage: legendInvalidMessage } : {})}
414
+ className={cn(
415
+ surface.bounded &&
416
+ placement !== "notch" &&
417
+ surfaceRegionClasses("surface", surface.layout, "header"),
418
+ placement !== null && surfaceLegendClasses[placement],
419
+ className,
420
+ )}
421
+ {...props}
422
+ >
423
+ {children}
424
+ </StructuralHeader>
425
+ </SurfaceHeaderContext.Provider>
426
+ );
427
+ }
428
+
429
+ export type SurfaceMediaProps = HTMLAttributes<HTMLElement>;
430
+
431
+ /**
432
+ * Slot de mídia: reserva a posição que abre a linha do header e centraliza o conteúdo.
433
+ * Não desenha moldura: componha `IconTile` para um ícone emoldurado e passe `Avatar`,
434
+ * `img` ou um glifo diretamente.
435
+ */
436
+ export function SurfaceMedia({
437
+ className,
438
+ ...props
439
+ }: SurfaceMediaProps): ReactElement {
440
+ const surface = requiredContext(
441
+ useContext(SurfaceHeaderContext),
442
+ "SurfaceMedia",
443
+ "SurfaceHeader",
444
+ );
445
+ const legend = surface.element === "fieldset";
446
+ // Dentro de `legend` só cabe conteúdo de frase; fora dela, o slot hospeda blocos.
447
+ const Media = legend ? "span" : "div";
448
+ return (
449
+ <Media
450
+ data-slot="surface-media"
451
+ className={cn(
452
+ "flex shrink-0 items-center justify-center [&>svg:not([class*='size-'])]:size-4",
453
+ className,
454
+ )}
455
+ {...props}
456
+ // Na legenda, a mídia é sempre decorativa: o grupo é anunciado só pelo título.
457
+ aria-hidden={legend ? true : props["aria-hidden"]}
458
+ />
459
+ );
460
+ }
461
+
462
+ export function SurfaceTitle({
463
+ className,
464
+ id,
465
+ ...props
466
+ }: HTMLAttributes<HTMLElement>): ReactElement {
467
+ const surface = requiredContext(
468
+ useContext(SurfaceHeaderContext),
469
+ "SurfaceTitle",
470
+ "SurfaceHeader",
471
+ );
472
+ const legend = surface.element === "fieldset";
473
+ const Heading = legend ? "span" : headingTags[surface.level];
474
+ usePageHeadingPresence(!legend && surface.level === 1);
475
+ return (
476
+ <Heading
477
+ id={id ?? surface.titleId}
478
+ data-slot="surface-title"
479
+ className={cn(
480
+ surface.variant === "page"
481
+ ? structuralHeaderClasses.page.title
482
+ : surfaceTitleClasses("surface", surface.layout),
483
+ className,
484
+ )}
485
+ {...props}
486
+ />
487
+ );
488
+ }
489
+
490
+ export function SurfaceDescription({
491
+ className,
492
+ ...props
493
+ }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
494
+ const surface = requiredContext(
495
+ useContext(SurfaceHeaderContext),
496
+ "SurfaceDescription",
497
+ "SurfaceHeader",
498
+ );
499
+ return (
500
+ <p
501
+ data-slot="surface-description"
502
+ className={cn(
503
+ surface.variant === "page"
504
+ ? structuralHeaderClasses.page.description
505
+ : surfaceDescriptionClasses("surface"),
506
+ className,
507
+ )}
508
+ {...props}
509
+ />
510
+ );
511
+ }
512
+
513
+ /** Metadado ao lado do título — o slot do `count` da forma curta. */
514
+ export function SurfaceMeta({
515
+ className,
516
+ ...props
517
+ }: HTMLAttributes<HTMLSpanElement>): ReactElement {
518
+ const surface = requiredContext(
519
+ useContext(SurfaceHeaderContext),
520
+ "SurfaceMeta",
521
+ "SurfaceHeader",
522
+ );
523
+ return (
524
+ <span
525
+ data-slot="surface-meta"
526
+ className={cn(structuralHeaderClasses[surface.variant].count, className)}
527
+ {...props}
528
+ />
529
+ );
530
+ }
531
+
532
+ /** Ações do header — a família as posiciona no extremo oposto ao título. */
533
+ export function SurfaceActions({
534
+ className,
535
+ ...props
536
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
537
+ const surface = requiredContext(
538
+ useContext(SurfaceHeaderContext),
539
+ "SurfaceActions",
540
+ "SurfaceHeader",
541
+ );
542
+ return (
543
+ <div
544
+ data-slot="surface-actions"
545
+ className={cn(
546
+ structuralHeaderClasses[surface.variant].actions,
547
+ className,
548
+ )}
549
+ {...props}
550
+ />
551
+ );
552
+ }
553
+
554
+ export interface SurfaceBodyProps extends HTMLAttributes<HTMLDivElement> {
555
+ /**
556
+ * Entrega a largura inteira ao filho que já controla o próprio recuo, como
557
+ * `DetailGroup` com divisores, lista ou tabela.
558
+ */
559
+ flush?: boolean;
560
+ }
561
+
562
+ export function SurfaceBody({
563
+ flush = false,
564
+ className,
565
+ children,
566
+ ...props
567
+ }: SurfaceBodyProps): ReactElement {
568
+ const surface = requiredContext(
569
+ useContext(SurfaceContext),
570
+ "SurfaceBody",
571
+ "Surface",
572
+ );
573
+ const fill = useFillRegion();
574
+ const placement = legendPlacement(surface);
575
+ return (
576
+ <div
577
+ data-slot="surface-body"
578
+ data-flush={flush ? "true" : undefined}
579
+ {...fill.attributes}
580
+ className={cn(
581
+ surfaceRegionClasses("surface", surface.layout, "body", {
582
+ flush: !surface.bounded || flush,
583
+ }),
584
+ placement === "notch" && surfaceLegendClasses.afterNotch,
585
+ placement === "band" && surfaceLegendClasses.afterBand,
586
+ fill.filling && fillRegionClassName,
587
+ className,
588
+ )}
589
+ {...props}
590
+ >
591
+ {fill.provide(children)}
592
+ </div>
593
+ );
594
+ }
595
+
596
+ /** Rodapé de ações persistentes; vale para regiões e para entidades. */
597
+ export function SurfaceFooter({
598
+ className,
599
+ ...props
600
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
601
+ const surface = requiredContext(
602
+ useContext(SurfaceContext),
603
+ "SurfaceFooter",
604
+ "Surface",
605
+ );
606
+ const placement = legendPlacement(surface);
607
+ return (
608
+ <div
609
+ data-slot="surface-footer"
610
+ className={cn(
611
+ "flex items-center justify-end gap-2",
612
+ surface.bounded &&
613
+ surfaceRegionClasses("surface", surface.layout, "footer"),
614
+ placement === "band" && surfaceLegendClasses.afterBand,
615
+ className,
616
+ )}
617
+ {...props}
618
+ />
619
+ );
620
+ }
@@ -230,15 +230,17 @@ export function ActionTrigger<TInput, TData>({
230
230
  id={confirmBodyId}
231
231
  className="text-sm text-muted-foreground"
232
232
  >
233
- {itemLabel !== undefined && (
234
- <span className="font-medium text-foreground">
235
- “{itemLabel}”
236
- </span>
237
- )}
238
- {itemLabel !== undefined && confirm.body !== undefined
239
- ? " — "
240
- : ""}
241
- {confirm.body}
233
+ <div>
234
+ {itemLabel !== undefined && (
235
+ <span className="font-medium text-foreground">
236
+ “{itemLabel}”
237
+ </span>
238
+ )}
239
+ {itemLabel !== undefined && confirm.body !== undefined
240
+ ? " — "
241
+ : ""}
242
+ {confirm.body}
243
+ </div>
242
244
  </DialogBody>
243
245
  )}
244
246
  <DialogFooter>