@softize/opus 27.1.0 → 28.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 (37) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/bin/lib/check.mjs +139 -30
  3. package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
  4. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
  5. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
  6. package/docs/adr/README.md +2 -0
  7. package/package.json +1 -1
  8. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  9. package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
  10. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
  11. package/src/ui/components/patterns/list.tsx +62 -53
  12. package/src/ui/components/patterns/page.tsx +146 -38
  13. package/src/ui/components/patterns/presentation.tsx +44 -59
  14. package/src/ui/components/patterns/structural-header.tsx +42 -6
  15. package/src/ui/components/patterns/surface-anatomy.ts +52 -22
  16. package/src/ui/components/patterns/surface.tsx +2 -3
  17. package/src/ui/components/primitives/control.ts +2 -2
  18. package/src/ui/components/primitives/dialog.tsx +7 -2
  19. package/src/ui/components/primitives/drawer.tsx +7 -2
  20. package/src/ui/components/primitives/icon-picker.tsx +271 -34
  21. package/src/ui/components/primitives/pagination.tsx +5 -4
  22. package/src/ui/components/primitives/tabs.tsx +15 -2
  23. package/src/ui/components/primitives/toggle.tsx +1 -1
  24. package/src/ui/docs/content/action-list.md +11 -2
  25. package/src/ui/docs/content/button.md +7 -7
  26. package/src/ui/docs/content/compositions.md +35 -23
  27. package/src/ui/docs/content/dialog.md +18 -6
  28. package/src/ui/docs/content/drawer.md +17 -5
  29. package/src/ui/docs/content/icon-picker.md +37 -19
  30. package/src/ui/docs/content/input.md +1 -1
  31. package/src/ui/docs/content/page.md +169 -69
  32. package/src/ui/docs/content/pagination.md +7 -8
  33. package/src/ui/docs/content/presentation.md +4 -3
  34. package/src/ui/docs/content/surface.md +8 -5
  35. package/src/ui/docs/content/tabs.md +12 -4
  36. package/src/ui/meta.ts +1 -1
  37. package/src/ui/react.tsx +9 -1
@@ -47,6 +47,7 @@ import {
47
47
  DialogContent,
48
48
  DialogFooter,
49
49
  DialogHeader,
50
+ DialogIntro,
50
51
  DialogNavigation,
51
52
  DialogTitle,
52
53
  } from "../primitives/dialog.tsx";
@@ -57,6 +58,7 @@ import {
57
58
  DrawerContent,
58
59
  DrawerFooter,
59
60
  DrawerHeader,
61
+ DrawerIntro,
60
62
  DrawerNavigation,
61
63
  DrawerTitle,
62
64
  } from "../primitives/drawer.tsx";
@@ -66,6 +68,7 @@ import {
66
68
  PageBody,
67
69
  PageFooter,
68
70
  PageHeader,
71
+ PageIntro,
69
72
  PageNavigation,
70
73
  PageTitle,
71
74
  useInsidePageShell,
@@ -114,11 +117,13 @@ const bodyClassName =
114
117
  * a lista do body é filha direta dele e assume `fill` sem classe no consumidor.
115
118
  */
116
119
  function PresentationBody({
120
+ page = false,
117
121
  layout,
118
122
  className,
119
123
  children,
120
124
  }: {
121
125
  layout: "flow" | "fill";
126
+ page?: boolean;
122
127
  className?: string;
123
128
  children: ReactNode;
124
129
  }): ReactElement {
@@ -128,7 +133,7 @@ function PresentationBody({
128
133
  data-slot="presentation-body"
129
134
  {...fill.attributes}
130
135
  className={cn(
131
- bodyClassName,
136
+ !page && bodyClassName,
132
137
  (layout === "fill" || fill.filling) && fillRegionClassName,
133
138
  className,
134
139
  )}
@@ -195,14 +200,14 @@ function PresentationFrame({
195
200
  </PresentationActions>
196
201
  );
197
202
  const body = (
198
- <PresentationBody layout={layout} className={bodyClassNameProp}>
203
+ <PresentationBody
204
+ page={surface === "page"}
205
+ layout={layout}
206
+ className={bodyClassNameProp}
207
+ >
199
208
  {typeof children === "function" ? children(footerTarget) : children}
200
209
  </PresentationBody>
201
210
  );
202
- // Viewport entre o PageBody e o conteúdo. Em `fill`, é coluna flexível para a altura
203
- // chegar ao conteúdo; a rolagem continua disponível como recurso de última instância.
204
- const viewportFillClassName =
205
- layout === "fill" ? fillRegionClassName : undefined;
206
211
  const withAssistant = (content: ReactElement): ReactElement =>
207
212
  assistantLabel === undefined || assistantPanel === undefined ? (
208
213
  content
@@ -248,18 +253,7 @@ function PresentationFrame({
248
253
  )}
249
254
  </PageHeader>
250
255
  )}
251
- <PageBody className="min-h-0 flex-1 overflow-hidden">
252
- {withAssistant(
253
- <div
254
- className={cn(
255
- "h-full overflow-x-hidden overflow-y-auto",
256
- viewportFillClassName,
257
- )}
258
- >
259
- {content}
260
- </div>,
261
- )}
262
- </PageBody>
256
+ <PageBody>{withAssistant(content)}</PageBody>
263
257
  {hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
264
258
  </Page>
265
259
  );
@@ -278,7 +272,6 @@ function PresentationFrame({
278
272
  {navigation === undefined ? null : (
279
273
  <PageNavigation>{navigation}</PageNavigation>
280
274
  )}
281
- {contentOwnsHeading ? null : <PageTitle>{title}</PageTitle>}
282
275
  {headerActions === undefined &&
283
276
  assistantAction === undefined ? null : (
284
277
  <PageActions>
@@ -290,36 +283,24 @@ function PresentationFrame({
290
283
  )}
291
284
  </PageHeader>
292
285
  )}
293
- <PageBody className="min-h-0 flex-1 overflow-hidden">
294
- {withAssistant(
295
- <div
296
- className={cn(
297
- "flex h-full min-h-0 flex-col overflow-x-hidden",
298
- layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
299
- viewportFillClassName,
300
- )}
301
- >
302
- {body}
303
- </div>,
286
+ <PageBody>
287
+ {contentOwnsHeading ? null : (
288
+ <PageIntro>
289
+ <PageTitle>{title}</PageTitle>
290
+ </PageIntro>
304
291
  )}
292
+ {withAssistant(body)}
305
293
  </PageBody>
306
294
  {hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
307
295
  </Page>
308
296
  );
309
297
  }
310
298
  return (
311
- <Page
312
- layout={layout}
313
- className={cn(
314
- "flex min-h-full max-w-none flex-col space-y-0 px-0 py-0",
315
- className,
316
- )}
317
- >
299
+ <Page layout={layout} className={className} contentClassName="max-w-none">
318
300
  <PageHeader>
319
301
  {navigation === undefined ? null : (
320
302
  <PageNavigation>{navigation}</PageNavigation>
321
303
  )}
322
- <PageTitle className="text-lg leading-none">{title}</PageTitle>
323
304
  {headerActions === undefined &&
324
305
  assistantAction === undefined ? null : (
325
306
  <PageActions>
@@ -330,17 +311,11 @@ function PresentationFrame({
330
311
  </PageActions>
331
312
  )}
332
313
  </PageHeader>
333
- <PageBody className="min-h-0 flex-1 overflow-hidden">
334
- {withAssistant(
335
- <div
336
- className={cn(
337
- "h-full overflow-x-hidden overflow-y-auto px-8 py-8",
338
- viewportFillClassName,
339
- )}
340
- >
341
- {body}
342
- </div>,
343
- )}
314
+ <PageBody>
315
+ <PageIntro>
316
+ <PageTitle>{title}</PageTitle>
317
+ </PageIntro>
318
+ {withAssistant(body)}
344
319
  </PageBody>
345
320
  {hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
346
321
  </Page>
@@ -364,7 +339,6 @@ function PresentationFrame({
364
339
  {navigation === undefined ? null : (
365
340
  <DialogNavigation>{navigation}</DialogNavigation>
366
341
  )}
367
- <DialogTitle>{title}</DialogTitle>
368
342
  {headerActions === undefined &&
369
343
  assistantAction === undefined ? null : (
370
344
  <DialogActions>
@@ -375,7 +349,14 @@ function PresentationFrame({
375
349
  </DialogActions>
376
350
  )}
377
351
  </DialogHeader>
378
- {withAssistant(<DialogBody>{body}</DialogBody>)}
352
+ {withAssistant(
353
+ <DialogBody>
354
+ <DialogIntro>
355
+ <DialogTitle>{title}</DialogTitle>
356
+ </DialogIntro>
357
+ {body}
358
+ </DialogBody>,
359
+ )}
379
360
  {hasFooter ? (
380
361
  <DialogFooter>{renderFooterActions()}</DialogFooter>
381
362
  ) : null}
@@ -402,7 +383,6 @@ function PresentationFrame({
402
383
  {navigation === undefined ? null : (
403
384
  <DrawerNavigation>{navigation}</DrawerNavigation>
404
385
  )}
405
- <DrawerTitle>{title}</DrawerTitle>
406
386
  {headerActions === undefined &&
407
387
  assistantAction === undefined ? null : (
408
388
  <DrawerActions>
@@ -413,7 +393,14 @@ function PresentationFrame({
413
393
  </DrawerActions>
414
394
  )}
415
395
  </DrawerHeader>
416
- {withAssistant(<DrawerBody>{body}</DrawerBody>)}
396
+ {withAssistant(
397
+ <DrawerBody>
398
+ <DrawerIntro>
399
+ <DrawerTitle>{title}</DrawerTitle>
400
+ </DrawerIntro>
401
+ {body}
402
+ </DrawerBody>,
403
+ )}
417
404
  {hasFooter ? (
418
405
  <DrawerFooter>{renderFooterActions()}</DrawerFooter>
419
406
  ) : null}
@@ -635,9 +622,7 @@ export function Presentation({
635
622
  string,
636
623
  unknown
637
624
  >;
638
- // Header e footer são regiões de decisão da superfície, não toolbars operacionais.
639
- // Seus comandos textuais usam a linha padrão; listas e grids continuam donos da
640
- // densidade compacta de suas próprias ações.
625
+ // A região chamadora escolhe a escala; o footer conserva o botão padrão.
641
626
  if (action.kind === "simple") {
642
627
  return (
643
628
  <PresentationCommandTrigger
@@ -784,7 +769,7 @@ export function Presentation({
784
769
 
785
770
  const headerActions = definition.actions
786
771
  .filter((command) => command.placement === "header")
787
- .map((command) => renderCommand(command));
772
+ .map((command) => renderCommand(command, "sm"));
788
773
  const footerActions = definition.actions
789
774
  .filter((command) => command.placement === "footer")
790
775
  .map((command) => renderCommand(command));
@@ -794,7 +779,7 @@ export function Presentation({
794
779
  (invocation.surface === "page" || parentSurface !== "page");
795
780
  const navigation = !canGoBack ? undefined : (
796
781
  <Button
797
- size="icon"
782
+ size="icon-sm"
798
783
  variant="ghost"
799
784
  disabled={surfaceBlocked}
800
785
  aria-label="Voltar"
@@ -807,7 +792,7 @@ export function Presentation({
807
792
  const assistantAction =
808
793
  definition.assistant === undefined || assistantOpen ? undefined : (
809
794
  <Button
810
- size="icon"
795
+ size="icon-sm"
811
796
  variant="ghost"
812
797
  aria-label={definition.assistant.triggerLabel}
813
798
  title={definition.assistant.triggerLabel}
@@ -110,7 +110,9 @@ export function StructuralHeader({
110
110
  ...props
111
111
  }: StructuralHeaderProps): ReactElement {
112
112
  const Wrapper = Root === "legend" ? "span" : "div";
113
- const nodes = flatten(children);
113
+ const nodes = flatten(children).filter(
114
+ (node) => typeof node !== "string" || node.trim() !== "",
115
+ );
114
116
  const of = (type: ElementType) =>
115
117
  nodes.filter((node) => isValidElement(node) && node.type === type);
116
118
  const leadingTypes: readonly ElementType[] =
@@ -305,6 +307,7 @@ export function StructuralHeader({
305
307
  className={cn(
306
308
  actionSlots.length > 0 &&
307
309
  "flex flex-row items-end justify-between gap-4",
310
+ rowClassName,
308
311
  )}
309
312
  >
310
313
  {heading}
@@ -314,16 +317,24 @@ export function StructuralHeader({
314
317
  ) : leading.length > 0 ? (
315
318
  <Wrapper
316
319
  data-slot={`${slot}-header-row`}
317
- className="flex min-w-0 items-center gap-4"
320
+ className={cn("flex min-w-0 items-center gap-4", rowClassName)}
318
321
  >
319
- <Wrapper
320
- data-slot={`${slot}-navigation`}
321
- className="min-w-0 flex-1"
322
- >
322
+ <Wrapper data-slot={`${slot}-navigation`} className="min-w-0 flex-1">
323
323
  {leading}
324
324
  </Wrapper>
325
325
  {actionSlots}
326
326
  </Wrapper>
327
+ ) : rowClassName !== undefined ? (
328
+ <Wrapper
329
+ data-slot={`${slot}-header-row`}
330
+ className={cn(
331
+ rowClassName,
332
+ heading ? "justify-between" : "justify-end",
333
+ )}
334
+ >
335
+ {heading}
336
+ {actionSlots}
337
+ </Wrapper>
327
338
  ) : (
328
339
  <>
329
340
  {heading}
@@ -360,3 +371,28 @@ export function StructuralHeader({
360
371
  </Root>
361
372
  );
362
373
  }
374
+
375
+ /** A intro visível abre o corpo; wrappers opacos são verificados quando materializam seus slots. */
376
+ export function validateSurfaceIntro(
377
+ children: ReactNode,
378
+ intro: ElementType,
379
+ name: string,
380
+ ): void {
381
+ const flatten = (nodes: ReactNode): ReactNode[] =>
382
+ Children.toArray(nodes).flatMap((node) =>
383
+ isValidElement(node) && node.type === Fragment
384
+ ? flatten((node.props as { children?: ReactNode }).children)
385
+ : [node],
386
+ );
387
+ const nodes = flatten(children).filter(
388
+ (node) => typeof node !== "string" || node.trim() !== "",
389
+ );
390
+ const indexes = nodes.flatMap((node, index) =>
391
+ isValidElement(node) && node.type === intro ? [index] : [],
392
+ );
393
+ if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
394
+ throw new Error(
395
+ `${name} aceita uma única intro como primeiro bloco do corpo.`,
396
+ );
397
+ }
398
+ }
@@ -27,7 +27,7 @@ import {
27
27
  } from "react";
28
28
  import { FillBoundary } from "./fill-region.ts";
29
29
 
30
- export type SurfaceFamily = "surface" | "dialog" | "drawer";
30
+ export type SurfaceFamily = "surface" | "dialog" | "drawer" | "page";
31
31
  export type SurfaceLayout = "continuous" | "sectioned";
32
32
  export type SurfaceRegion = "header" | "body" | "footer";
33
33
 
@@ -64,18 +64,22 @@ interface FamilyAnatomy {
64
64
  description: string;
65
65
  /** Espaço entre mídia, título e ações na linha do header. */
66
66
  headerGap: string;
67
+ /** Altura mínima da linha, equivalente ao controle padrão da família. */
68
+ headerMinHeight: string;
67
69
  /** Faixa do rodapé no layout seccionado, quando a família declara uma. */
68
70
  footerBand: string;
69
71
  }
70
72
 
71
73
  /**
72
74
  * 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.
75
+ * O header é comum à família; corpos e rodapés conservam o ritmo de cada superfície.
75
76
  */
77
+ const structuralHeaderPadding = "p-4";
78
+ const structuralHeaderMinHeight = "min-h-8";
79
+
76
80
  const sectionedRegions: RegionAnatomy = {
77
- header: "p-4",
78
- body: "px-4 py-6",
81
+ header: structuralHeaderPadding,
82
+ body: "p-4",
79
83
  footer: "p-4",
80
84
  };
81
85
 
@@ -83,25 +87,47 @@ const sectionedRegions: RegionAnatomy = {
83
87
  * Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
84
88
  * Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
85
89
  */
86
- const structuralBodyRhythm = "flex flex-col gap-6 *:shrink-0";
90
+ export const structuralBodyRhythm = "flex flex-col gap-4 *:shrink-0";
91
+ export const structuralRegionSpacing = "space-y-4";
92
+ export const structuralRegionGap = "gap-4";
87
93
 
88
94
  export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
95
+ page: {
96
+ regions: {
97
+ continuous: {
98
+ header: structuralHeaderPadding,
99
+ body: "px-8 pt-0 pb-0 [[data-slot=page-body]:first-child>&]:pt-4",
100
+ footer: "px-8 py-4",
101
+ },
102
+ sectioned: {
103
+ header: sectionedRegions.header,
104
+ body: "px-8 pt-4 pb-0 [[data-slot=page-body]:has(+[data-slot=page-footer])>&]:pb-4",
105
+ footer: "px-8 py-4",
106
+ },
107
+ },
108
+ title: {
109
+ continuous: "text-2xl font-semibold",
110
+ sectioned: "text-2xl font-semibold",
111
+ },
112
+ description: "text-sm text-muted-foreground",
113
+ headerGap: "gap-4",
114
+ headerMinHeight: structuralHeaderMinHeight,
115
+ footerBand: "bg-muted/30",
116
+ },
89
117
  // Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
90
118
  // não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
91
119
  surface: {
92
120
  regions: {
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.
121
+ // No contínuo, a região anterior fornece o intervalo; a primeira abre o recuo.
96
122
  continuous: {
97
- header: "px-4 pt-4 pb-6 [&:last-child]:pb-4",
98
- body: "px-4 pt-0 pb-6 [&:first-child]:pt-6",
123
+ header: structuralHeaderPadding,
124
+ body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
99
125
  footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
100
126
  },
101
127
  sectioned: {
102
- header: "px-4 py-3",
103
- body: "px-4 py-6",
104
- footer: "px-4 py-3",
128
+ header: structuralHeaderPadding,
129
+ body: "p-4",
130
+ footer: "p-4",
105
131
  },
106
132
  },
107
133
  // Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
@@ -111,14 +137,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
111
137
  },
112
138
  description: "mt-0.5 text-xs text-muted-foreground",
113
139
  headerGap: "gap-2",
140
+ headerMinHeight: structuralHeaderMinHeight,
114
141
  footerBand: "",
115
142
  },
116
143
  dialog: {
117
144
  regions: {
118
145
  continuous: {
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",
146
+ header: structuralHeaderPadding,
147
+ body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
148
+ footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
122
149
  },
123
150
  sectioned: sectionedRegions,
124
151
  },
@@ -128,14 +155,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
128
155
  },
129
156
  description: "text-sm text-muted-foreground",
130
157
  headerGap: "gap-4",
158
+ headerMinHeight: structuralHeaderMinHeight,
131
159
  footerBand: "bg-muted/20 dark:bg-input/20",
132
160
  },
133
161
  drawer: {
134
162
  regions: {
135
163
  continuous: {
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",
164
+ header: structuralHeaderPadding,
165
+ body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
166
+ footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
139
167
  },
140
168
  sectioned: sectionedRegions,
141
169
  },
@@ -145,6 +173,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
145
173
  },
146
174
  description: "text-sm text-muted-foreground",
147
175
  headerGap: "gap-4",
176
+ headerMinHeight: structuralHeaderMinHeight,
148
177
  footerBand: "bg-muted/20 dark:bg-input/20",
149
178
  },
150
179
  };
@@ -190,7 +219,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
190
219
  * `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
191
220
  */
192
221
  export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
193
- return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
222
+ return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap} ${surfaceAnatomy[family].headerMinHeight}`;
194
223
  }
195
224
 
196
225
  /** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
@@ -207,7 +236,7 @@ export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
207
236
  */
208
237
  export const surfaceLegendClasses = {
209
238
  notch: "ml-2 w-auto px-2",
210
- afterNotch: "pt-3",
239
+ afterNotch: "pt-4",
211
240
  band: "float-left w-full",
212
241
  afterBand: "clear-left",
213
242
  } as const;
@@ -241,6 +270,7 @@ export function SurfaceLayoutProvider({
241
270
  }
242
271
 
243
272
  const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
273
+ page: "sectioned",
244
274
  surface: "continuous",
245
275
  dialog: "sectioned",
246
276
  drawer: "sectioned",
@@ -49,6 +49,7 @@ import {
49
49
  surfaceHeaderRowClasses,
50
50
  surfaceLegendClasses,
51
51
  surfaceRegionClasses,
52
+ structuralRegionSpacing,
52
53
  surfaceTitleClasses,
53
54
  type SurfaceLayout,
54
55
  } from "./surface-anatomy.ts";
@@ -343,9 +344,7 @@ export function Surface({
343
344
  framed && "has-[>[data-flush=true]]:overflow-hidden",
344
345
  raised &&
345
346
  "bg-card text-card-foreground [--surface-background:var(--card)]",
346
- structured &&
347
- !bounded &&
348
- (variant === "page" ? "space-y-6" : "space-y-3"),
347
+ structured && !bounded && structuralRegionSpacing,
349
348
  link && fill.filling && fillRegionClassName,
350
349
  className,
351
350
  )}
@@ -7,7 +7,7 @@ export type ControlShape = 'default' | 'pill'
7
7
  * Cada componente aceita o subconjunto coerente com seu papel, nunca um nome com outra medida.
8
8
  *
9
9
  * - Altura de linha (`xs` · `sm` · `default` · `lg`): 1.5 · 2 · 2.25 · 2.5rem.
10
- * - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 · 1.75 · 2.25 · 2.5rem.
10
+ * - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 · 2 · 2.25 · 2.5rem.
11
11
  * `icon-xs` é o degrau da ação que mora dentro de uma linha densa. `icon-sm` atende
12
12
  * composições compactas que ainda precisam de mais presença.
13
13
  * - Glifo (o svg dentro do controle daquele tamanho): 0.875 · 0.875 · 1 · 1.25rem. O quadrado
@@ -27,7 +27,7 @@ export const controlHeight: Record<ControlSize, string> = {
27
27
 
28
28
  export const controlSquare: Record<ControlIconSize, string> = {
29
29
  'icon-xs': 'size-6',
30
- 'icon-sm': 'size-7',
30
+ 'icon-sm': 'size-8',
31
31
  icon: 'size-9',
32
32
  'icon-lg': 'size-10',
33
33
  }
@@ -14,7 +14,11 @@ import {
14
14
  } from "../patterns/surface-anatomy.ts";
15
15
  import { FillBoundary } from "../patterns/fill-region.ts";
16
16
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
17
- import { StructuralHeader } from "../patterns/structural-header.tsx";
17
+ import {
18
+ StructuralHeader,
19
+ structuralHeaderClasses,
20
+ validateSurfaceIntro,
21
+ } from "../patterns/structural-header.tsx";
18
22
  import {
19
23
  StructuralMedia,
20
24
  type StructuralMediaProps,
@@ -698,6 +702,7 @@ export function DialogBody({
698
702
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
699
703
  const { mode } = useDialogContext();
700
704
  const layout = useSurfaceLayout("dialog");
705
+ validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
701
706
  return (
702
707
  <div
703
708
  data-slot="dialog-body"
@@ -761,7 +766,7 @@ export function DialogTitle({
761
766
  ? "min-w-0 whitespace-normal"
762
767
  : "min-w-0 truncate",
763
768
  intro
764
- ? "text-xl font-semibold leading-tight tracking-tight"
769
+ ? structuralHeaderClasses.page.title
765
770
  : surfaceTitleClasses("dialog", layout),
766
771
  className,
767
772
  );
@@ -13,7 +13,11 @@ import {
13
13
  type SurfaceLayout,
14
14
  } from "../patterns/surface-anatomy.ts";
15
15
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
16
- import { StructuralHeader } from "../patterns/structural-header.tsx";
16
+ import {
17
+ StructuralHeader,
18
+ structuralHeaderClasses,
19
+ validateSurfaceIntro,
20
+ } from "../patterns/structural-header.tsx";
17
21
  import {
18
22
  StructuralMedia,
19
23
  type StructuralMediaProps,
@@ -361,6 +365,7 @@ function DrawerBody({
361
365
  ...props
362
366
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
363
367
  const layout = useSurfaceLayout("drawer");
368
+ validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
364
369
  return (
365
370
  <div
366
371
  data-slot="drawer-body"
@@ -408,7 +413,7 @@ function DrawerTitle({
408
413
  const classes = cn(
409
414
  intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
410
415
  intro
411
- ? "text-xl font-semibold leading-tight tracking-tight"
416
+ ? structuralHeaderClasses.page.title
412
417
  : surfaceTitleClasses("drawer", layout),
413
418
  className,
414
419
  );