@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
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Cabeçalho de superfície (interno): a anatomia que Page e Content compartilham.
2
+ * Cabeçalho estrutural (interno): a anatomia que Page, Surface, Dialog, Drawer e Pane
3
+ * compartilham. O nome público `SurfaceHeader` pertence à família Surface (ADR 0045).
3
4
  *
4
5
  * Um título, metadata opcional e uma região de ações que vai para o extremo oposto
5
6
  * em larguras amplas e empilha abaixo do contexto nas estreitas. Cada família escolhe quais slots
@@ -17,11 +18,11 @@ import {
17
18
  } from "react";
18
19
  import { cn } from "../../lib/cn.ts";
19
20
 
20
- export type SurfaceHeaderVariant = "page" | "section";
21
+ export type StructuralHeaderVariant = "page" | "section";
21
22
 
22
- /** As classes por slot, por hierarquia visual — PageTitle e ContentTitle(page) são o mesmo. */
23
- export const surfaceHeaderClasses: Record<
24
- SurfaceHeaderVariant,
23
+ /** As classes por slot, por hierarquia visual — PageTitle e SurfaceTitle no nível 1 são o mesmo. */
24
+ export const structuralHeaderClasses: Record<
25
+ StructuralHeaderVariant,
25
26
  { title: string; description: string; count: string; actions: string }
26
27
  > = {
27
28
  page: {
@@ -38,7 +39,7 @@ export const surfaceHeaderClasses: Record<
38
39
  },
39
40
  };
40
41
 
41
- interface SurfaceHeaderSlots {
42
+ interface StructuralHeaderSlots {
42
43
  leading?: ElementType | readonly ElementType[];
43
44
  /** Ícone ou imagem que abre a linha, antes do heading (ADR 0023). */
44
45
  media?: ElementType;
@@ -48,15 +49,15 @@ interface SurfaceHeaderSlots {
48
49
  actions?: ElementType;
49
50
  }
50
51
 
51
- export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
52
+ export interface StructuralHeaderProps extends HTMLAttributes<HTMLDivElement> {
52
53
  /** Elemento estrutural; `legend` preserva a mesma anatomia com wrappers phrasing. */
53
54
  as?: "div" | "legend";
54
- /** Nome do componente público, para as mensagens de erro (`PageHeader`, `ContentHeader`). */
55
+ /** Nome do componente público, para as mensagens de erro (`PageHeader`, `SurfaceHeader`). */
55
56
  name: string;
56
57
  /** Prefixo dos `data-slot` (`page` → `page-header`, `page-heading`). */
57
58
  slot: string;
58
59
  /** Os componentes de slot que este cabeçalho reconhece. */
59
- slots: SurfaceHeaderSlots;
60
+ slots: StructuralHeaderSlots;
60
61
  /** Posição de um slot introdutório opcional, antes da linha ou dentro dela. */
61
62
  leadingPlacement?: "above" | "inline";
62
63
  /** Mantém a mídia na linha geral ou a agrupa com o heading. */
@@ -72,7 +73,7 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
72
73
  /**
73
74
  * Aceita conteúdo livre além dos slots. Vale para a superfície genérica — um pane
74
75
  * hospeda marca, busca ou chrome que não são título nem ação. As famílias que sempre
75
- * NOMEIAM algo (Card, Dialog, Drawer, Page, Content) continuam estritas.
76
+ * NOMEIAM algo (Surface, Dialog, Drawer, Page) continuam estritas.
76
77
  */
77
78
  allowFreeContent?: boolean;
78
79
  /**
@@ -91,7 +92,7 @@ function flatten(children: ReactNode): ReactNode[] {
91
92
  );
92
93
  }
93
94
 
94
- export function SurfaceHeader({
95
+ export function StructuralHeader({
95
96
  as: Root = "div",
96
97
  name,
97
98
  slot,
@@ -107,7 +108,7 @@ export function SurfaceHeader({
107
108
  className,
108
109
  children,
109
110
  ...props
110
- }: SurfaceHeaderProps): ReactElement {
111
+ }: StructuralHeaderProps): ReactElement {
111
112
  const Wrapper = Root === "legend" ? "span" : "div";
112
113
  const nodes = flatten(children);
113
114
  const of = (type: ElementType) =>
@@ -124,8 +125,7 @@ export function SurfaceHeader({
124
125
  const counts = slots.count === undefined ? [] : of(slots.count);
125
126
  const descriptions =
126
127
  slots.description === undefined ? [] : of(slots.description);
127
- const actionSlots =
128
- slots.actions === undefined ? [] : of(slots.actions);
128
+ const actionSlots = slots.actions === undefined ? [] : of(slots.actions);
129
129
  const slotTypes = [
130
130
  ...leadingTypes,
131
131
  slots.media,
@@ -137,7 +137,9 @@ export function SurfaceHeader({
137
137
  const free = allowFreeContent
138
138
  ? nodes.filter(
139
139
  (node) =>
140
- !(isValidElement(node) && slotTypes.includes(node.type as ElementType)),
140
+ !(
141
+ isValidElement(node) && slotTypes.includes(node.type as ElementType)
142
+ ),
141
143
  )
142
144
  : [];
143
145
  const recognized =
@@ -242,16 +244,26 @@ export function SurfaceHeader({
242
244
  )}
243
245
  {mediaPlacement === "row" && medias}
244
246
  {heading}
247
+ {!heading && leading.length === 0 && (
248
+ <Wrapper
249
+ data-slot={`${slot}-header-spacer`}
250
+ className="min-w-0 flex-1"
251
+ aria-hidden="true"
252
+ />
253
+ )}
245
254
  {actionSlots}
246
255
  {trailing}
247
256
  </>
248
257
  );
249
258
  const layout =
250
259
  leadingPlacement === "inline" ? (
251
- <Wrapper className={cn("flex min-w-0 items-center gap-3", rowClassName)}>
260
+ <Wrapper
261
+ data-slot={`${slot}-header-row`}
262
+ className={cn("flex min-w-0 items-center gap-3", rowClassName)}
263
+ >
252
264
  {row}
253
265
  </Wrapper>
254
- ) : leading.length > 0 ? (
266
+ ) : leading.length > 0 && heading ? (
255
267
  <>
256
268
  <Wrapper data-slot={`${slot}-navigation`} className="mb-4">
257
269
  {leading}
@@ -267,6 +279,19 @@ export function SurfaceHeader({
267
279
  {actionSlots}
268
280
  </Wrapper>
269
281
  </>
282
+ ) : leading.length > 0 ? (
283
+ <Wrapper
284
+ data-slot={`${slot}-header-row`}
285
+ className="flex min-w-0 items-center gap-4"
286
+ >
287
+ <Wrapper
288
+ data-slot={`${slot}-navigation`}
289
+ className="min-w-0 flex-1"
290
+ >
291
+ {leading}
292
+ </Wrapper>
293
+ {actionSlots}
294
+ </Wrapper>
270
295
  ) : (
271
296
  <>
272
297
  {heading}
@@ -282,7 +307,10 @@ export function SurfaceHeader({
282
307
  leadingPlacement !== "inline" &&
283
308
  leading.length === 0 &&
284
309
  actionSlots.length > 0 &&
285
- "flex flex-row items-end justify-between gap-4",
310
+ cn(
311
+ "flex flex-row items-end gap-4",
312
+ heading ? "justify-between" : "justify-end",
313
+ ),
286
314
  className,
287
315
  )}
288
316
  {...props}
@@ -290,7 +318,10 @@ export function SurfaceHeader({
290
318
  {contentClassName === undefined ? (
291
319
  layout
292
320
  ) : (
293
- <Wrapper data-slot={`${slot}-header-content`} className={contentClassName}>
321
+ <Wrapper
322
+ data-slot={`${slot}-header-content`}
323
+ className={contentClassName}
324
+ >
294
325
  {layout}
295
326
  </Wrapper>
296
327
  )}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Slot de mídia estrutural (interno): abre a linha do header de Dialog, Drawer e Pane
3
+ * sem decidir a apresentação do conteúdo. A moldura, quando necessária, é composta
4
+ * explicitamente com `IconTile` pelo consumidor.
5
+ */
6
+ import type { ReactElement } from "react";
7
+ import { cn } from "../../lib/cn.ts";
8
+
9
+ export interface StructuralMediaProps extends React.ComponentProps<"div"> {}
10
+
11
+ export function StructuralMedia({
12
+ slot,
13
+ className,
14
+ ...props
15
+ }: StructuralMediaProps & { slot: string }): ReactElement {
16
+ return (
17
+ <div
18
+ data-slot={slot}
19
+ className={cn(
20
+ "flex shrink-0 items-center justify-center [&>svg:not([class*='size-'])]:size-4",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Anatomia de superfície (interno): as medidas que Card, Dialog, Drawer e Pane
3
- * compartilham — ADR 0023.
2
+ * Anatomia de superfície (interno): as medidas que Surface, Dialog e Drawer
3
+ * compartilham — ADRs 0023, 0045 e 0047.
4
4
  *
5
5
  * Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
6
6
  * escala do título, posição das ações e a regra dos divisores. As famílias públicas
@@ -10,13 +10,12 @@
10
10
  *
11
11
  * - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
12
12
  * última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
13
- * o mesmo respiro do topo. É o card de leitura.
13
+ * o mesmo respiro do topo. É a superfície de leitura.
14
14
  * - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
15
15
  * modal, o drawer e o painel.
16
16
  *
17
17
  * O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
18
- * transversal verifica. Entre famílias diferentes ele pode divergir: o card de leitura
19
- * é mais folgado que um painel de operação.
18
+ * transversal verifica.
20
19
  */
21
20
 
22
21
  import {
@@ -28,7 +27,7 @@ import {
28
27
  } from "react";
29
28
  import { FillBoundary } from "./fill-region.ts";
30
29
 
31
- export type SurfaceFamily = "card" | "dialog" | "drawer" | "pane";
30
+ export type SurfaceFamily = "surface" | "dialog" | "drawer";
32
31
  export type SurfaceLayout = "continuous" | "sectioned";
33
32
  export type SurfaceRegion = "header" | "body" | "footer";
34
33
 
@@ -70,44 +69,56 @@ interface FamilyAnatomy {
70
69
  }
71
70
 
72
71
  /**
73
- * O recuo seccionado de Card, Dialog e Drawer: 1rem, a densidade das superfícies de
74
- * operação, e o que permite um card seccionado conviver com um drawer sem degrau
75
- * visível. O pane tem a sua (`px-3`, a do chrome), declarada na tabela abaixo.
72
+ * O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
73
+ * A Surface usa o mesmo recuo lateral, para conviver com um drawer sem degrau visível, e
74
+ * faixas de header e footer mais baixas.
76
75
  */
77
76
  const sectionedRegions: RegionAnatomy = {
78
77
  header: "p-4",
79
- body: "p-4",
78
+ body: "px-4 py-6",
80
79
  footer: "p-4",
81
80
  };
82
81
 
82
+ /**
83
+ * Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
84
+ * Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
85
+ */
86
+ const structuralBodyRhythm = "flex flex-col gap-6 *:shrink-0";
87
+
83
88
  export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
84
- card: {
89
+ // Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
90
+ // não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
91
+ surface: {
85
92
  regions: {
86
- // O `pb-3` do header e do body encurtam a distância até a região seguinte; quando
87
- // a região é a última, ela reassume o respiro de baixo (`[&:last-child]:pb-5`) —
88
- // inclusive o header sozinho, que sem isso fecharia o card mais apertado embaixo
89
- // do que em cima.
93
+ // O espaço entre regiões soma o ritmo estrutural. Quando o header fica sozinho, ele
94
+ // reassume o respiro compacto da faixa; quando o body abre ou fecha a superfície,
95
+ // recebe o respiro inteiro do conteúdo.
90
96
  continuous: {
91
- header: "px-5 pt-5 pb-3 [&:last-child]:pb-5",
92
- body: "px-5 py-3 [&:last-child]:pb-5 [&:first-child]:pt-5",
93
- footer: "px-5 pt-3 pb-5",
97
+ header: "px-4 pt-4 pb-6 [&:last-child]:pb-4",
98
+ body: "px-4 pt-0 pb-6 [&:first-child]:pt-6",
99
+ footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
100
+ },
101
+ sectioned: {
102
+ header: "px-4 py-3",
103
+ body: "px-4 py-6",
104
+ footer: "px-4 py-3",
94
105
  },
95
- sectioned: sectionedRegions,
96
106
  },
107
+ // Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
97
108
  title: {
98
- continuous: "text-base font-semibold leading-none",
99
- sectioned: "text-sm font-semibold leading-none",
109
+ continuous: "text-sm font-semibold",
110
+ sectioned: "text-sm font-semibold",
100
111
  },
101
- description: "text-sm text-muted-foreground",
102
- headerGap: "gap-3",
112
+ description: "mt-0.5 text-xs text-muted-foreground",
113
+ headerGap: "gap-2",
103
114
  footerBand: "",
104
115
  },
105
116
  dialog: {
106
117
  regions: {
107
118
  continuous: {
108
- header: "p-4 pb-2 [&:last-child]:pb-4",
109
- body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
110
- footer: "p-4 pt-2",
119
+ header: "p-4 pb-3 [&:last-child]:pb-4",
120
+ body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
121
+ footer: "p-4 pt-3",
111
122
  },
112
123
  sectioned: sectionedRegions,
113
124
  },
@@ -122,9 +133,9 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
122
133
  drawer: {
123
134
  regions: {
124
135
  continuous: {
125
- header: "p-4 pb-2 [&:last-child]:pb-4",
126
- body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
127
- footer: "p-4 pt-2",
136
+ header: "p-4 pb-3 [&:last-child]:pb-4",
137
+ body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
138
+ footer: "p-4 pt-3",
128
139
  },
129
140
  sectioned: sectionedRegions,
130
141
  },
@@ -136,29 +147,6 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
136
147
  headerGap: "gap-4",
137
148
  footerBand: "bg-muted/20 dark:bg-input/20",
138
149
  },
139
- pane: {
140
- regions: {
141
- continuous: {
142
- header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
143
- body: "p-3 [&:first-child]:pt-3",
144
- footer: "px-3 pt-2 pb-3",
145
- },
146
- sectioned: {
147
- // A faixa tem a altura da barra do shell; `flex items-center` centraliza a linha
148
- // dentro dela, e `shrink-0` impede que um corpo longo comprima a faixa.
149
- header: "flex h-[3.75rem] shrink-0 items-center px-3",
150
- body: "p-3",
151
- footer: "px-3 py-2",
152
- },
153
- },
154
- title: {
155
- continuous: "text-sm font-semibold leading-none",
156
- sectioned: "text-sm font-semibold leading-none",
157
- },
158
- description: "text-xs text-muted-foreground",
159
- headerGap: "gap-2",
160
- footerBand: "",
161
- },
162
150
  };
163
151
 
164
152
  /** Classes da região, já com o divisor que o layout deriva da vizinhança. */
@@ -166,14 +154,22 @@ export function surfaceRegionClasses(
166
154
  family: SurfaceFamily,
167
155
  layout: SurfaceLayout,
168
156
  region: SurfaceRegion,
157
+ { flush = false }: { flush?: boolean } = {},
169
158
  ): string {
170
159
  const anatomy = surfaceAnatomy[family];
171
- const base = anatomy.regions[layout][region];
172
- if (layout !== "sectioned") return base;
173
- if (region === "header") return `${base} ${headerDivider}`;
160
+ const base =
161
+ region === "body" && flush
162
+ ? structuralBodyRhythm
163
+ : anatomy.regions[layout][region];
164
+ const regionClasses =
165
+ region === "body" && !flush ? `${base} ${structuralBodyRhythm}` : base;
166
+ if (layout !== "sectioned") return regionClasses;
167
+ if (region === "header") return `${regionClasses} ${headerDivider}`;
174
168
  if (region === "footer")
175
- return [base, footerDivider, anatomy.footerBand].filter(Boolean).join(" ");
176
- return base;
169
+ return [regionClasses, footerDivider, anatomy.footerBand]
170
+ .filter(Boolean)
171
+ .join(" ");
172
+ return regionClasses;
177
173
  }
178
174
 
179
175
  /** Escala visual do título — o nível do heading vem da composição, nunca daqui. */
@@ -191,8 +187,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
191
187
  /**
192
188
  * A linha do header: mídia, heading que absorve a folga, ações no extremo oposto.
193
189
  *
194
- * `flex-1` não é decoração: quando a própria região é flex — o pane, que tem altura de
195
- * barra —, a linha vira um item que se mede pelo conteúdo e as ações param no meio.
190
+ * `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
196
191
  */
197
192
  export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
198
193
  return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
@@ -201,6 +196,22 @@ export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
201
196
  /** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
202
197
  export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
203
198
 
199
+ /**
200
+ * A `legend` de uma Surface `fieldset` delimitada (ADR 0045).
201
+ *
202
+ * - `notch`: moldura contínua. A legend nativa ocupa a borda, afastada do canto pelo
203
+ * mesmo recuo lateral das regiões; a região seguinte recebe o intervalo que a legend
204
+ * não tem como declarar.
205
+ * - `band`: a legend é a faixa do header dentro da superfície. Uma legend só ocupa a
206
+ * largura inteira fora do fluxo de borda, e a região seguinte precisa limpar o float.
207
+ */
208
+ export const surfaceLegendClasses = {
209
+ notch: "ml-2 w-auto px-2",
210
+ afterNotch: "pt-3",
211
+ band: "float-left w-full",
212
+ afterBand: "clear-left",
213
+ } as const;
214
+
204
215
  /**
205
216
  * O layout é do root e chega aos slots por contexto — ADR 0023. Slot não recebe
206
217
  * `layout`: o divisor depende de quem está do outro lado, então a decisão precisa valer
@@ -230,10 +241,9 @@ export function SurfaceLayoutProvider({
230
241
  }
231
242
 
232
243
  const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
233
- card: "continuous",
244
+ surface: "continuous",
234
245
  dialog: "sectioned",
235
246
  drawer: "sectioned",
236
- pane: "sectioned",
237
247
  };
238
248
 
239
249
  /** O layout vigente: o declarado pelo root da superfície, ou o default da família. */
@@ -45,7 +45,6 @@ export function SurfaceAssistant({
45
45
  key="surface-content"
46
46
  id="surface-content"
47
47
  grow
48
- inset="none"
49
48
  className={cn("h-full", contentClassName)}
50
49
  >
51
50
  {children}
@@ -57,13 +56,12 @@ export function SurfaceAssistant({
57
56
  initialSize={initialSize}
58
57
  minSize={minSize}
59
58
  maxSize={maxSize}
60
- inset="none"
61
- className={cn("h-full bg-background", assistantClassName)}
59
+ className={cn("h-full", assistantClassName)}
62
60
  >
63
61
  <aside
64
62
  data-slot="surface-assistant-panel"
65
63
  aria-label={assistantLabel}
66
- className="h-full min-w-0 overflow-hidden"
64
+ className="h-full min-w-0 overflow-hidden bg-background"
67
65
  >
68
66
  {assistant}
69
67
  </aside>