@softize/opus 29.0.0 → 31.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 (59) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/lib/check.mjs +178 -94
  3. package/bin/lib/copy.mjs +2970 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
  7. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  8. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  9. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  10. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  11. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  12. package/docs/adr/README.md +8 -0
  13. package/package.json +1 -1
  14. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  15. package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
  17. package/src/core/presentation.ts +938 -32
  18. package/src/core/types.ts +2 -1
  19. package/src/ui/components/patterns/form.tsx +2 -0
  20. package/src/ui/components/patterns/grouped-list.ts +2 -2
  21. package/src/ui/components/patterns/list.tsx +588 -365
  22. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  23. package/src/ui/components/patterns/page.tsx +238 -181
  24. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  25. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  26. package/src/ui/components/patterns/presentation.tsx +1142 -229
  27. package/src/ui/components/patterns/structural-header.tsx +47 -34
  28. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  29. package/src/ui/components/patterns/surface.tsx +55 -37
  30. package/src/ui/components/patterns/trigger.tsx +10 -2
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/detail.tsx +1 -1
  34. package/src/ui/components/primitives/dialog.tsx +59 -89
  35. package/src/ui/components/primitives/drawer.tsx +52 -87
  36. package/src/ui/components/primitives/field.tsx +1 -1
  37. package/src/ui/components/primitives/pagination.tsx +3 -2
  38. package/src/ui/components/primitives/table.tsx +3 -3
  39. package/src/ui/components/primitives/tabs.tsx +13 -2
  40. package/src/ui/docs/content/action-form.md +2 -1
  41. package/src/ui/docs/content/action-list.md +146 -73
  42. package/src/ui/docs/content/action-trigger.md +5 -0
  43. package/src/ui/docs/content/button.md +5 -6
  44. package/src/ui/docs/content/command.md +62 -50
  45. package/src/ui/docs/content/compositions.md +129 -459
  46. package/src/ui/docs/content/dialog.md +72 -101
  47. package/src/ui/docs/content/drawer.md +69 -92
  48. package/src/ui/docs/content/field.md +2 -1
  49. package/src/ui/docs/content/page.md +207 -386
  50. package/src/ui/docs/content/presentation.md +371 -23
  51. package/src/ui/docs/content/separator.md +1 -1
  52. package/src/ui/docs/content/surface.md +94 -116
  53. package/src/ui/docs/content/table.md +7 -0
  54. package/src/ui/docs/content/tokens.md +18 -4
  55. package/src/ui/drivers/context.tsx +11 -4
  56. package/src/ui/drivers/react.tsx +27 -7
  57. package/src/ui/meta.ts +5 -5
  58. package/src/ui/react.tsx +38 -8
  59. package/src/ui/theme.css +44 -33
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * framed borda, raio e recorte;
9
9
  * elevation fundo próprio (`raised`) ou o fundo de quem hospeda (`none`);
10
- * layout ritmo contínuo ou faixas com divisores derivados.
11
10
  *
12
11
  * Três formas de compor produzem a mesma anatomia:
13
12
  *
@@ -42,6 +41,7 @@ import { headingTags, type HeadingLevel } from "./heading-level.ts";
42
41
  import { usePageHeadingPresence } from "./page-heading-context.tsx";
43
42
  import {
44
43
  StructuralHeader,
44
+ structuralMetaClasses,
45
45
  structuralHeaderClasses,
46
46
  } from "./structural-header.tsx";
47
47
  import {
@@ -51,7 +51,6 @@ import {
51
51
  surfaceRegionClasses,
52
52
  structuralRegionSpacing,
53
53
  surfaceTitleClasses,
54
- type SurfaceLayout,
55
54
  } from "./surface-anatomy.ts";
56
55
 
57
56
  export type SurfaceElement = "div" | "section" | "article" | "fieldset";
@@ -63,7 +62,6 @@ interface SurfaceContextValue {
63
62
  /** Tem moldura ou elevação: as regiões carregam o recuo. */
64
63
  bounded: boolean;
65
64
  framed: boolean;
66
- layout: SurfaceLayout;
67
65
  level: HeadingLevel;
68
66
  titleId: string;
69
67
  variant: "page" | "section";
@@ -83,11 +81,9 @@ function requiredContext<T>(
83
81
  }
84
82
 
85
83
  /** Como a `legend` de um fieldset delimitado se relaciona com a moldura. */
86
- function legendPlacement(
87
- surface: SurfaceContextValue,
88
- ): "notch" | "band" | null {
84
+ function legendPlacement(surface: SurfaceContextValue): "band" | null {
89
85
  if (surface.element !== "fieldset" || !surface.bounded) return null;
90
- return surface.framed && surface.layout === "continuous" ? "notch" : "band";
86
+ return "band";
91
87
  }
92
88
 
93
89
  interface SurfaceBaseProps {
@@ -98,8 +94,6 @@ interface SurfaceBaseProps {
98
94
  * o fundo de quem hospeda.
99
95
  */
100
96
  elevation?: SurfaceElevation;
101
- /** Anatomia da superfície inteira: `continuous` (default) ou `sectioned`. */
102
- layout?: SurfaceLayout;
103
97
  className?: string;
104
98
  children?: ReactNode;
105
99
  }
@@ -113,6 +107,8 @@ interface SurfaceHeaderShorthandProps {
113
107
  count?: number;
114
108
  /** Forma curta: frase de apoio sob o título. */
115
109
  description?: ReactNode;
110
+ /** Forma curta: badges, datas e autoria sob a orientação. */
111
+ meta?: ReactNode;
116
112
  /** Forma curta: ações sobre a superfície inteira, no extremo oposto ao título. */
117
113
  actions?: ReactNode;
118
114
  }
@@ -124,6 +120,7 @@ interface SurfaceLegendShorthandProps {
124
120
  media?: ReactNode;
125
121
  count?: never;
126
122
  description?: never;
123
+ meta?: never;
127
124
  actions?: never;
128
125
  }
129
126
 
@@ -132,6 +129,7 @@ interface SurfaceNoShorthandProps {
132
129
  media?: never;
133
130
  count?: never;
134
131
  description?: never;
132
+ meta?: never;
135
133
  actions?: never;
136
134
  }
137
135
 
@@ -198,12 +196,12 @@ export function Surface({
198
196
  asChild = false,
199
197
  framed = false,
200
198
  elevation = "none",
201
- layout = "continuous",
202
199
  level = 2,
203
200
  title,
204
201
  media,
205
202
  count,
206
203
  description,
204
+ meta,
207
205
  actions,
208
206
  className,
209
207
  children,
@@ -218,11 +216,6 @@ export function Surface({
218
216
  const element: SurfaceElement | null = asChild ? null : (as ?? "div");
219
217
  const raised = elevation === "raised";
220
218
  const bounded = framed || raised;
221
- if (layout === "sectioned" && !bounded) {
222
- throw new Error(
223
- 'Surface layout="sectioned" exige framed ou elevation="raised".',
224
- );
225
- }
226
219
 
227
220
  const shorthand = title !== undefined;
228
221
  if (
@@ -230,10 +223,11 @@ export function Surface({
230
223
  (hasMedia(media) ||
231
224
  count !== undefined ||
232
225
  description !== undefined ||
226
+ meta !== undefined ||
233
227
  actions !== undefined)
234
228
  ) {
235
229
  throw new Error(
236
- "Surface exige title para usar media, count, description ou actions.",
230
+ "Surface exige title para usar media, count, description, meta ou actions.",
237
231
  );
238
232
  }
239
233
  if (asChild && shorthand) {
@@ -292,7 +286,6 @@ export function Surface({
292
286
  element,
293
287
  bounded,
294
288
  framed,
295
- layout,
296
289
  level,
297
290
  titleId: resolvedTitleId,
298
291
  variant,
@@ -303,10 +296,11 @@ export function Surface({
303
296
  <SurfaceHeader>
304
297
  {hasMedia(media) && <SurfaceMedia>{media}</SurfaceMedia>}
305
298
  <SurfaceTitle>{title}</SurfaceTitle>
306
- {count !== undefined && <SurfaceMeta>{count}</SurfaceMeta>}
299
+ {count !== undefined && <SurfaceCount>{count}</SurfaceCount>}
307
300
  {description !== undefined && (
308
301
  <SurfaceDescription>{description}</SurfaceDescription>
309
302
  )}
303
+ {meta !== undefined && <SurfaceMeta>{meta}</SurfaceMeta>}
310
304
  {actions !== undefined && <SurfaceActions>{actions}</SurfaceActions>}
311
305
  </SurfaceHeader>
312
306
  {regions > 0
@@ -334,13 +328,12 @@ export function Surface({
334
328
  data-as={element ?? undefined}
335
329
  data-framed={framed}
336
330
  data-elevation={elevation}
337
- data-layout={layout}
338
331
  {...(link ? fill.attributes : {})}
339
332
  aria-labelledby={named ? resolvedTitleId : undefined}
340
333
  className={cn(
341
334
  framed && "rounded-xl border border-border",
342
335
  // O divisor e o conteúdo sem recuo encostam na borda: o raio precisa recortar.
343
- framed && layout === "sectioned" && "overflow-hidden",
336
+ framed && "overflow-hidden",
344
337
  framed && "has-[>[data-flush=true]]:overflow-hidden",
345
338
  raised &&
346
339
  "bg-card text-card-foreground [--surface-background:var(--card)]",
@@ -367,10 +360,16 @@ export function Surface({
367
360
  );
368
361
  }
369
362
 
370
- export type SurfaceHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
363
+ export type SurfaceHeaderProps = Omit<
364
+ HTMLAttributes<HTMLDivElement>,
365
+ "title"
366
+ > & {
367
+ /** TabsList abaixo do título; indisponível em fieldset. */
368
+ tabs?: ReactNode;
369
+ };
371
370
 
372
371
  const legendInvalidMessage =
373
- "SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição e ações ficam fora da legenda.";
372
+ "SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição, metadados e ações ficam fora da legenda.";
374
373
 
375
374
  /**
376
375
  * Header da superfície; não existe fora dela. Monta a linha sozinho — mídia, título com
@@ -401,7 +400,8 @@ export function SurfaceHeader({
401
400
  : {
402
401
  media: SurfaceMedia,
403
402
  title: SurfaceTitle,
404
- count: SurfaceMeta,
403
+ count: SurfaceCount,
404
+ meta: SurfaceMeta,
405
405
  description: SurfaceDescription,
406
406
  actions: SurfaceActions,
407
407
  }
@@ -411,9 +411,9 @@ export function SurfaceHeader({
411
411
  rowClassName={surfaceHeaderRowClasses("surface")}
412
412
  {...(legend ? { invalidMessage: legendInvalidMessage } : {})}
413
413
  className={cn(
414
- surface.bounded &&
415
- placement !== "notch" &&
416
- surfaceRegionClasses("surface", surface.layout, "header"),
414
+ surfaceRegionClasses("surface", "header", {
415
+ bounded: surface.bounded,
416
+ }),
417
417
  placement !== null && surfaceLegendClasses[placement],
418
418
  className,
419
419
  )}
@@ -470,7 +470,7 @@ export function SurfaceTitle({
470
470
  );
471
471
  const legend = surface.element === "fieldset";
472
472
  const Heading = legend ? "span" : headingTags[surface.level];
473
- usePageHeadingPresence(!legend && surface.level === 1);
473
+ usePageHeadingPresence(!legend && surface.level === 1, "surface");
474
474
  return (
475
475
  <Heading
476
476
  id={id ?? surface.titleId}
@@ -478,7 +478,7 @@ export function SurfaceTitle({
478
478
  className={cn(
479
479
  surface.variant === "page"
480
480
  ? structuralHeaderClasses.page.title
481
- : surfaceTitleClasses("surface", surface.layout),
481
+ : surfaceTitleClasses("surface"),
482
482
  className,
483
483
  )}
484
484
  {...props}
@@ -509,25 +509,44 @@ export function SurfaceDescription({
509
509
  );
510
510
  }
511
511
 
512
- /** Metadado ao lado do título — o slot do `count` da forma curta. */
513
- export function SurfaceMeta({
512
+ /** Contador ao lado do título — o slot do `count` da forma curta. */
513
+ export function SurfaceCount({
514
514
  className,
515
515
  ...props
516
516
  }: HTMLAttributes<HTMLSpanElement>): ReactElement {
517
517
  const surface = requiredContext(
518
518
  useContext(SurfaceHeaderContext),
519
- "SurfaceMeta",
519
+ "SurfaceCount",
520
520
  "SurfaceHeader",
521
521
  );
522
522
  return (
523
523
  <span
524
- data-slot="surface-meta"
524
+ data-slot="surface-count"
525
525
  className={cn(structuralHeaderClasses[surface.variant].count, className)}
526
526
  {...props}
527
527
  />
528
528
  );
529
529
  }
530
530
 
531
+ /** Badges, datas e autoria sob o título e a orientação da superfície. */
532
+ export function SurfaceMeta({
533
+ className,
534
+ ...props
535
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
536
+ requiredContext(
537
+ useContext(SurfaceHeaderContext),
538
+ "SurfaceMeta",
539
+ "SurfaceHeader",
540
+ );
541
+ return (
542
+ <div
543
+ data-slot="surface-meta"
544
+ className={cn(structuralMetaClasses, className)}
545
+ {...props}
546
+ />
547
+ );
548
+ }
549
+
531
550
  /** Ações do header — a família as posiciona no extremo oposto ao título. */
532
551
  export function SurfaceActions({
533
552
  className,
@@ -577,10 +596,10 @@ export function SurfaceBody({
577
596
  data-flush={flush ? "true" : undefined}
578
597
  {...fill.attributes}
579
598
  className={cn(
580
- surfaceRegionClasses("surface", surface.layout, "body", {
581
- flush: !surface.bounded || flush,
599
+ surfaceRegionClasses("surface", "body", {
600
+ bounded: surface.bounded,
601
+ flush,
582
602
  }),
583
- placement === "notch" && surfaceLegendClasses.afterNotch,
584
603
  placement === "band" && surfaceLegendClasses.afterBand,
585
604
  fill.filling && fillRegionClassName,
586
605
  className,
@@ -608,8 +627,7 @@ export function SurfaceFooter({
608
627
  data-slot="surface-footer"
609
628
  className={cn(
610
629
  "flex items-center justify-end gap-2",
611
- surface.bounded &&
612
- surfaceRegionClasses("surface", surface.layout, "footer"),
630
+ surfaceRegionClasses("surface", "footer", { bounded: surface.bounded }),
613
631
  placement === "band" && surfaceLegendClasses.afterBand,
614
632
  className,
615
633
  )}
@@ -52,6 +52,8 @@ export interface ActionTriggerProps<TInput, TData> {
52
52
  input: TInput;
53
53
  /** Texto do botão. Default usa action.label. */
54
54
  label?: string;
55
+ /** Nome acessível completo quando o texto visível é abreviado. */
56
+ "aria-label"?: string;
55
57
  /** Contexto visual. Por padrão, ações destrutivas usam `danger`. */
56
58
  context?: ButtonProps["context"];
57
59
  /** Tratamento visual do Button. */
@@ -70,6 +72,8 @@ export interface ActionTriggerProps<TInput, TData> {
70
72
  actionLabel?: string;
71
73
  cancelLabel?: string;
72
74
  };
75
+ /** Mensagem de sucesso desta ocorrência; substitui a mensagem do contrato. */
76
+ successMessage?: string;
73
77
  /** Callback adicional pós-sucesso (cache já foi invalidado). */
74
78
  onSuccess?: (data: TData) => void;
75
79
  /** Notifica o renderer que coordena bloqueio entre várias actions. */
@@ -87,12 +91,14 @@ export function ActionTrigger<TInput, TData>({
87
91
  action,
88
92
  input,
89
93
  label,
94
+ "aria-label": ariaLabel,
90
95
  context,
91
96
  variant,
92
97
  size,
93
98
  disabled = false,
94
99
  confirm: confirmProp,
95
100
  onSuccess,
101
+ successMessage,
96
102
  onLoadingChange,
97
103
  icon,
98
104
  itemLabel,
@@ -102,7 +108,9 @@ export function ActionTrigger<TInput, TData>({
102
108
  const [open, setOpen] = useState(false);
103
109
  const { trigger, isLoading } = useTriggerAction(action, {
104
110
  onSuccess: (data) => {
105
- toast.success(msgText(action.messages?.success, "Concluído"));
111
+ toast.success(
112
+ successMessage ?? msgText(action.messages?.success, "Concluído"),
113
+ );
106
114
  onSuccess?.(data);
107
115
  setOpen(false);
108
116
  },
@@ -184,7 +192,7 @@ export function ActionTrigger<TInput, TData>({
184
192
  size={size ?? (icon !== undefined ? "icon-xs" : "default")}
185
193
  busy={isLoading}
186
194
  disabled={disabled}
187
- aria-label={icon !== undefined ? buttonLabel : undefined}
195
+ aria-label={ariaLabel ?? (icon !== undefined ? buttonLabel : undefined)}
188
196
  onClick={(e) => {
189
197
  e.stopPropagation();
190
198
  onClick();
@@ -3,22 +3,30 @@ import { XIcon } from "lucide-react";
3
3
 
4
4
  import { Button, type ButtonProps } from "./button.tsx";
5
5
 
6
- export type CloseButtonSize = "xs" | "sm";
6
+ export type CloseButtonSize = "xs" | "sm" | "default";
7
7
 
8
8
  const buttonSizeByCloseSize = {
9
+ default: "icon",
9
10
  xs: "icon-xs",
10
11
  sm: "icon-sm",
11
12
  } as const;
12
13
 
13
14
  export interface CloseButtonProps extends Omit<
14
15
  ButtonProps,
15
- "busy" | "children" | "context" | "icon" | "shape" | "size" | "type" | "variant"
16
+ | "busy"
17
+ | "children"
18
+ | "context"
19
+ | "icon"
20
+ | "shape"
21
+ | "size"
22
+ | "type"
23
+ | "variant"
16
24
  > {
17
25
  size?: CloseButtonSize;
18
26
  }
19
27
 
20
28
  export function CloseButton({
21
- size = "sm",
29
+ size = "xs",
22
30
  disabled = false,
23
31
  "aria-label": ariaLabel = "Fechar",
24
32
  ...props
@@ -38,9 +38,6 @@ function CommandDialog({
38
38
  return (
39
39
  <Dialog {...props}>
40
40
  <DialogContent
41
- // A paleta é uma superfície contínua: a lista já separa os grupos, e uma faixa
42
- // sob o cabeçalho competiria com ela.
43
- layout="continuous"
44
41
  className={cn("overflow-hidden p-0", className)}
45
42
  showCloseButton={showCloseButton}
46
43
  aria-describedby={descriptionId}
@@ -50,11 +47,8 @@ function CommandDialog({
50
47
  <p id={descriptionId} className="sr-only">
51
48
  {description}
52
49
  </p>
53
- {/* O recuo e a escala são os da paleta, não os da família: a caixa de busca
54
- manda na densidade, e o título aqui é um rótulo acima dela. É a única
55
- superfície do SDK que diverge nos dois, e por isso está escrito aqui. */}
56
- <DialogHeader className="px-3 py-2">
57
- <DialogTitle className="text-sm">{title}</DialogTitle>
50
+ <DialogHeader>
51
+ <DialogTitle>{title}</DialogTitle>
58
52
  </DialogHeader>
59
53
  <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
60
54
  {children}
@@ -122,7 +122,7 @@ function DetailField({
122
122
  !dividedHorizontalField &&
123
123
  "group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)] group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3",
124
124
  dividedHorizontalField && "items-stretch gap-x-0",
125
- "group-data-[orientation=vertical]/detail-group:gap-y-1",
125
+ "group-data-[orientation=vertical]/detail-group:gap-y-1.5",
126
126
  "group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border",
127
127
  "group-data-[variant=framed]/detail-group:bg-transparent",
128
128
  className,
@@ -3,21 +3,17 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
3
  import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
4
4
  import { cn } from "../../lib/cn.ts";
5
5
  import {
6
- SurfaceLayoutProvider,
7
6
  surfaceActionsClasses,
8
7
  surfaceDescriptionClasses,
9
8
  surfaceHeaderRowClasses,
10
9
  surfaceRegionClasses,
11
10
  surfaceTitleClasses,
12
- useSurfaceLayout,
13
- type SurfaceLayout,
14
11
  } from "../patterns/surface-anatomy.ts";
15
12
  import { FillBoundary } from "../patterns/fill-region.ts";
16
13
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
17
14
  import {
18
15
  StructuralHeader,
19
- structuralHeaderClasses,
20
- validateSurfaceIntro,
16
+ structuralMetaClasses,
21
17
  } from "../patterns/structural-header.tsx";
22
18
  import {
23
19
  StructuralMedia,
@@ -64,10 +60,10 @@ interface DialogContentContextValue {
64
60
  const DialogContentContext = React.createContext<DialogContentContextValue>({
65
61
  showCloseButton: false,
66
62
  closeDisabled: false,
67
- closeSize: "sm",
63
+ closeSize: "xs",
68
64
  });
69
65
 
70
- const DialogIntroContext = React.createContext(false);
66
+ const DialogMetaContext = React.createContext(false);
71
67
 
72
68
  /**
73
69
  * Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
@@ -415,8 +411,7 @@ export function DialogContent({
415
411
  children,
416
412
  showCloseButton,
417
413
  closeDisabled = false,
418
- closeSize = "sm",
419
- layout = "sectioned",
414
+ closeSize = "xs",
420
415
  "aria-describedby": ariaDescribedBy,
421
416
  onEscapeKeyDown,
422
417
  onPointerDownOutside,
@@ -436,8 +431,6 @@ export function DialogContent({
436
431
  closeDisabled?: boolean;
437
432
  /** Tamanho da ação circular de fechar no cabeçalho. */
438
433
  closeSize?: CloseButtonSize;
439
- /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
440
- layout?: SurfaceLayout;
441
434
  }) {
442
435
  const { mode, open, modal } = useDialogContext();
443
436
  const layer = useLayerPortal();
@@ -527,7 +520,6 @@ export function DialogContent({
527
520
  ref={composedRef}
528
521
  data-slot="dialog-content"
529
522
  data-mode="default"
530
- data-layout={layout}
531
523
  {...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
532
524
  className={cn(
533
525
  contentClasses,
@@ -548,13 +540,13 @@ export function DialogContent({
548
540
  {...props}
549
541
  >
550
542
  <LayerPortalProvider value={nestedPortal}>
551
- <SurfaceLayoutProvider value={layout}>
543
+ <FillBoundary>
552
544
  <DialogContentContext.Provider
553
545
  value={{ showCloseButton: showClose, closeDisabled, closeSize }}
554
546
  >
555
547
  {children}
556
548
  </DialogContentContext.Provider>
557
- </SurfaceLayoutProvider>
549
+ </FillBoundary>
558
550
  </LayerPortalProvider>
559
551
  </DialogPrimitive.Content>
560
552
  </DialogPortal>
@@ -564,16 +556,21 @@ export function DialogContent({
564
556
  export function DialogHeader({
565
557
  className,
566
558
  children,
559
+ tabs,
567
560
  ...props
568
- }: React.ComponentProps<"div">) {
561
+ }: React.ComponentProps<"div"> & {
562
+ tabs?: React.ReactNode;
563
+ }) {
569
564
  const { mode } = useDialogContext();
570
- const layout = useSurfaceLayout("dialog");
571
565
  const { showCloseButton, closeDisabled, closeSize } =
572
566
  React.useContext(DialogContentContext);
573
567
 
574
568
  // O modo de alerta é a exceção declarada da ADR 0023: a resposta obrigatória empilha
575
569
  // mídia, título e corpo centralizados, e o padding mora na raiz do conteúdo.
576
570
  if (mode === "alert") {
571
+ if (tabs !== undefined && tabs !== null && tabs !== false) {
572
+ throw new Error("DialogHeader de alerta não aceita abas.");
573
+ }
577
574
  return (
578
575
  <div
579
576
  data-slot="dialog-header"
@@ -589,33 +586,37 @@ export function DialogHeader({
589
586
  }
590
587
 
591
588
  return (
592
- <StructuralHeader
593
- name="DialogHeader"
594
- slot="dialog"
595
- slots={{
596
- leading: DialogNavigation,
597
- media: DialogMedia,
598
- title: DialogTitle,
599
- description: DialogDescription,
600
- actions: DialogActions,
601
- }}
602
- leadingPlacement="inline"
603
- titleRequired={false}
604
- rowClassName={surfaceHeaderRowClasses("dialog")}
605
- className={cn(
606
- "text-left",
607
- surfaceRegionClasses("dialog", layout, "header"),
608
- className,
609
- )}
610
- trailing={
611
- showCloseButton ? (
612
- <DialogCloseButton disabled={closeDisabled} size={closeSize} />
613
- ) : undefined
614
- }
615
- {...props}
616
- >
617
- {children}
618
- </StructuralHeader>
589
+ <DialogMetaContext.Provider value>
590
+ <StructuralHeader
591
+ tabs={tabs}
592
+ name="DialogHeader"
593
+ slot="dialog"
594
+ slots={{
595
+ leading: DialogNavigation,
596
+ media: DialogMedia,
597
+ title: DialogTitle,
598
+ description: DialogDescription,
599
+ meta: DialogMeta,
600
+ actions: DialogActions,
601
+ }}
602
+ leadingPlacement="inline"
603
+ titleRequired
604
+ rowClassName={surfaceHeaderRowClasses("dialog")}
605
+ className={cn(
606
+ "text-left",
607
+ surfaceRegionClasses("dialog", "header"),
608
+ className,
609
+ )}
610
+ trailing={
611
+ showCloseButton ? (
612
+ <DialogCloseButton disabled={closeDisabled} size={closeSize} />
613
+ ) : undefined
614
+ }
615
+ {...props}
616
+ >
617
+ {children}
618
+ </StructuralHeader>
619
+ </DialogMetaContext.Provider>
619
620
  );
620
621
  }
621
622
 
@@ -647,42 +648,22 @@ export function DialogActions({
647
648
  );
648
649
  }
649
650
 
650
- /** Título e ações da composição principal, como primeiro bloco de DialogBody. */
651
- export function DialogIntro({
652
- className,
653
- children,
654
- ...props
655
- }: React.ComponentProps<"div">) {
656
- return (
657
- <DialogIntroContext.Provider value>
658
- <StructuralHeader
659
- name="DialogIntro"
660
- slot="dialog-intro"
661
- slots={{
662
- title: DialogTitle,
663
- description: DialogDescription,
664
- actions: DialogIntroActions,
665
- }}
666
- leadingPlacement="inline"
667
- titleRequired
668
- className={cn("text-left", className)}
669
- {...props}
670
- >
671
- {children}
672
- </StructuralHeader>
673
- </DialogIntroContext.Provider>
674
- );
675
- }
676
-
677
- /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
678
- export function DialogIntroActions({
651
+ /** Badges, datas e autoria abaixo do título e da orientação. */
652
+ export function DialogMeta({
679
653
  className,
680
654
  ...props
681
655
  }: React.ComponentProps<"div">) {
656
+ const { mode } = useDialogContext();
657
+ const inside = React.useContext(DialogMetaContext);
658
+ if (mode === "alert")
659
+ throw new Error(
660
+ 'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
661
+ );
662
+ if (!inside) throw new Error("DialogMeta deve estar dentro de DialogHeader.");
682
663
  return (
683
664
  <div
684
- data-slot="dialog-intro-actions"
685
- className={cn(surfaceActionsClasses, className)}
665
+ data-slot="dialog-meta"
666
+ className={cn(structuralMetaClasses, className)}
686
667
  {...props}
687
668
  />
688
669
  );
@@ -701,8 +682,6 @@ export function DialogBody({
701
682
  ...props
702
683
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
703
684
  const { mode } = useDialogContext();
704
- const layout = useSurfaceLayout("dialog");
705
- validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
706
685
  return (
707
686
  <div
708
687
  data-slot="dialog-body"
@@ -711,7 +690,7 @@ export function DialogBody({
711
690
  ? "flex min-w-0 flex-col gap-6"
712
691
  : cn(
713
692
  "min-h-0 flex-1 overflow-y-auto",
714
- surfaceRegionClasses("dialog", layout, "body", { flush }),
693
+ surfaceRegionClasses("dialog", "body", { flush }),
715
694
  ),
716
695
  className,
717
696
  )}
@@ -727,7 +706,6 @@ export function DialogFooter({
727
706
  ...props
728
707
  }: React.ComponentProps<"div">) {
729
708
  const { mode } = useDialogContext();
730
- const layout = useSurfaceLayout("dialog");
731
709
  return (
732
710
  <div
733
711
  data-slot="dialog-footer"
@@ -736,7 +714,7 @@ export function DialogFooter({
736
714
  ? "w-full"
737
715
  : cn(
738
716
  "flex shrink-0 flex-row justify-end gap-2",
739
- surfaceRegionClasses("dialog", layout, "footer"),
717
+ surfaceRegionClasses("dialog", "footer"),
740
718
  ),
741
719
  className,
742
720
  )}
@@ -755,19 +733,11 @@ export function DialogTitle({
755
733
  level?: HeadingLevel;
756
734
  }) {
757
735
  const { mode } = useDialogContext();
758
- const intro = React.useContext(DialogIntroContext);
759
- const layout = useSurfaceLayout("dialog");
760
736
  const Title =
761
737
  mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
762
738
  const classes = cn(
763
- mode === "alert"
764
- ? undefined
765
- : intro
766
- ? "min-w-0 whitespace-normal"
767
- : "min-w-0 truncate",
768
- intro
769
- ? structuralHeaderClasses.page.title
770
- : surfaceTitleClasses("dialog", layout),
739
+ mode === "alert" ? undefined : "min-w-0 truncate",
740
+ surfaceTitleClasses("dialog"),
771
741
  className,
772
742
  );
773
743
  // O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro