@softize/opus 30.0.0 → 31.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -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/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  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/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. package/src/ui/theme.css +44 -33
@@ -2,9 +2,10 @@
2
2
  * <Page /> — esqueleto composto de página do back-office.
3
3
  *
4
4
  * A forma curta cobre o caso comum; PageShell coordena contexto conhecido pelo shell sem criar
5
- * uma segunda região visual. Todas preservam o container centralizado com teto de 80rem.
5
+ * uma segunda região visual. O header reúne título e ações; largura e rolagem são independentes.
6
6
  */
7
7
 
8
+ import { ArrowLeft } from "lucide-react";
8
9
  import {
9
10
  Children,
10
11
  Fragment,
@@ -15,50 +16,63 @@ import {
15
16
  useLayoutEffect,
16
17
  useRef,
17
18
  useState,
18
- type HTMLAttributes,
19
19
  type ComponentProps,
20
+ type HTMLAttributes,
20
21
  type ReactElement,
21
22
  type ReactNode,
22
23
  } from "react";
23
24
  import { createPortal } from "react-dom";
24
- import { ArrowLeft } from "lucide-react";
25
25
  import { cn } from "../../lib/cn.ts";
26
26
  import { Button } from "../primitives/button.tsx";
27
- import { Surface } from "./surface.tsx";
27
+ import { ButtonGroup } from "../primitives/button-group.tsx";
28
+ import {
29
+ Tooltip,
30
+ TooltipContent,
31
+ TooltipTrigger,
32
+ } from "../primitives/tooltip.tsx";
33
+ import { FillRoot, useFillRegion } from "./fill-region.ts";
28
34
  import {
29
35
  PageHeadingPresenceProvider,
30
36
  usePageHeadingPresence,
31
37
  } from "./page-heading-context.tsx";
38
+ import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
32
39
  import {
33
40
  StructuralHeader,
34
- structuralMetaClasses,
35
- validateSurfaceIntro,
36
41
  structuralHeaderClasses,
42
+ structuralMetaClasses,
37
43
  } from "./structural-header.tsx";
38
- import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
39
- import { FillRoot, useFillRegion } from "./fill-region.ts";
44
+ import { Surface } from "./surface.tsx";
40
45
 
41
46
  import {
47
+ surfaceTitleClasses,
42
48
  surfaceHeaderRowClasses,
43
49
  surfaceRegionClasses,
44
- type SurfaceLayout,
45
50
  } from "./surface-anatomy.ts";
46
51
 
52
+ type PageWidth = "narrow" | "default" | "wide";
53
+ const pageWidthClasses: Record<PageWidth, string> = {
54
+ narrow: "max-w-[42rem]",
55
+ default: "max-w-[58rem]",
56
+ wide: "max-w-none",
57
+ };
58
+
47
59
  interface PageContextValue {
48
- surfaceLayout: SurfaceLayout;
60
+ layout: "flow" | "fill";
49
61
  contentClassName?: string;
50
62
  }
51
63
 
52
64
  const PageContext = createContext<PageContextValue | null>(null);
53
65
  const PageHeaderContext = createContext(false);
54
- const PageIntroContext = createContext(false);
55
66
  const PageBodyContext = createContext(false);
56
67
  const PageIntegralStateContext = createContext(false);
57
68
  const PageActionsTargetContext = createContext<HTMLElement | null>(null);
58
69
  interface PageShellContextValue {
59
70
  navigation?: ReactNode;
71
+ width?: PageWidth;
60
72
  actions?: ReactNode;
73
+ footer?: ReactNode;
61
74
  }
75
+ const PageConsumerContext = createContext<(() => () => void) | null>(null);
62
76
  const PageShellContext = createContext<PageShellContextValue | null>(null);
63
77
 
64
78
  function hasRenderableNode(node: ReactNode): boolean {
@@ -73,17 +87,10 @@ function hasRenderableNode(node: ReactNode): boolean {
73
87
  return true;
74
88
  }
75
89
 
76
- /** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
77
- export function useInsidePageShell(): boolean {
78
- return useContext(PageShellContext) !== null;
79
- }
80
-
81
90
  export interface PageShellProps extends Omit<
82
91
  HTMLAttributes<HTMLDivElement>,
83
92
  "children"
84
93
  > {
85
- /** Navegação contextual conhecida pelo shell, normalmente um Breadcrumb. */
86
- navigation?: ReactNode;
87
94
  /** Ações do shell ou do recurso que antecedem as ações declaradas pela Page. */
88
95
  actions?: ReactNode;
89
96
  /** Uma Page descendente, diretamente ou através da rota ativa. */
@@ -92,14 +99,12 @@ export interface PageShellProps extends Omit<
92
99
 
93
100
  /** Moldura que coordena contexto do shell com o PageHeader da página ativa. */
94
101
  export function PageShell({
95
- navigation,
96
102
  actions,
97
103
  children,
98
104
  className,
99
105
  ...props
100
106
  }: PageShellProps): ReactElement {
101
107
  const shell = {
102
- ...(hasRenderableNode(navigation) ? { navigation } : {}),
103
108
  ...(hasRenderableNode(actions) ? { actions } : {}),
104
109
  };
105
110
  return (
@@ -123,6 +128,60 @@ export function PageShell({
123
128
  );
124
129
  }
125
130
 
131
+ /** Contribuições da Presentation à Page completa fornecida pelo consumidor. */
132
+ export function PageShellContribution({
133
+ navigation,
134
+ width,
135
+ actions,
136
+ footer,
137
+ children,
138
+ }: {
139
+ navigation?: ReactNode;
140
+ width?: PageWidth;
141
+ actions?: ReactNode;
142
+ footer?: ReactNode;
143
+ children: ReactNode;
144
+ }): ReactElement {
145
+ const parent = useContext(PageShellContext);
146
+ const consumers = useRef(0);
147
+ const register = useCallback(() => {
148
+ consumers.current += 1;
149
+ return () => {
150
+ consumers.current -= 1;
151
+ };
152
+ }, []);
153
+ useLayoutEffect(() => {
154
+ if (consumers.current !== 1)
155
+ throw new Error(
156
+ "Presentation com heading de conteúdo exige uma Page completa.",
157
+ );
158
+ });
159
+ return (
160
+ <PageConsumerContext.Provider value={register}>
161
+ <PageShellContext.Provider
162
+ value={{
163
+ ...parent,
164
+ ...(navigation === undefined ? {} : { navigation }),
165
+ ...(width === undefined ? {} : { width }),
166
+ ...(hasRenderableNode(actions)
167
+ ? {
168
+ actions: (
169
+ <>
170
+ {parent?.actions}
171
+ {actions}
172
+ </>
173
+ ),
174
+ }
175
+ : {}),
176
+ ...(hasRenderableNode(footer) ? { footer } : {}),
177
+ }}
178
+ >
179
+ {children}
180
+ </PageShellContext.Provider>
181
+ </PageConsumerContext.Provider>
182
+ );
183
+ }
184
+
126
185
  export function PageActionsTarget({
127
186
  target,
128
187
  children,
@@ -149,17 +208,17 @@ interface PageBaseProps extends Omit<
149
208
  HTMLAttributes<HTMLElement>,
150
209
  "children" | "title"
151
210
  > {
152
- /** Classes do container externo. O conteúdo do Body mantém o teto de 80rem. */
211
+ /** Classes da coluna. O conteúdo do Body ocupa a largura disponível. */
153
212
  className?: string;
154
- /** Classes do conteúdo centralizado, incluindo largura máxima e alinhamento. */
213
+ /** Classes adicionais do conteúdo do body. */
155
214
  contentClassName?: string;
156
215
  /**
157
- * `flow` mantém a rolagem no body; `fill` permite que regiões internas assumam a rolagem.
216
+ * `flow` rola título e corpo na Page; `fill` mantém o header visível e delega às regiões internas.
158
217
  * A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Surface` (ADR 0024).
159
218
  */
160
219
  layout?: "flow" | "fill";
161
- /** Divisão visual das regiões, independente da distribuição de altura. */
162
- surfaceLayout?: SurfaceLayout;
220
+ /** Limita a coluna inteira, mantendo header e body alinhados. */
221
+ width?: PageWidth;
163
222
  }
164
223
 
165
224
  interface PageShorthandProps extends PageBaseProps {
@@ -203,7 +262,6 @@ function countDirectChildrenOfType(
203
262
 
204
263
  function countPageBodyHeadings(node: ReactNode): {
205
264
  content: number;
206
- intro: number;
207
265
  state: number;
208
266
  } {
209
267
  return Children.toArray(node).reduce(
@@ -217,7 +275,6 @@ function countPageBodyHeadings(node: ReactNode): {
217
275
  if (child.type === PageState && props.status !== "ready") {
218
276
  return {
219
277
  content: total.content,
220
- intro: total.intro,
221
278
  state: total.state + 1,
222
279
  };
223
280
  }
@@ -227,11 +284,10 @@ function countPageBodyHeadings(node: ReactNode): {
227
284
  total.content +
228
285
  (child.type === Surface && props.level === 1 ? 1 : 0) +
229
286
  nested.content,
230
- intro: total.intro + (child.type === PageIntro ? 1 : 0) + nested.intro,
231
287
  state: total.state + nested.state,
232
288
  };
233
289
  },
234
- { content: 0, intro: 0, state: 0 },
290
+ { content: 0, state: 0 },
235
291
  );
236
292
  }
237
293
 
@@ -253,13 +309,14 @@ export function Page({
253
309
  meta,
254
310
  actions,
255
311
  layout = "flow",
256
- surfaceLayout = "sectioned",
312
+ width: widthProp,
257
313
  contentClassName,
258
314
  className,
259
315
  children,
260
316
  ...props
261
317
  }: PageProps): ReactElement {
262
318
  const shell = useContext(PageShellContext);
319
+ const width = widthProp ?? shell?.width ?? "wide";
263
320
  const shorthand = title !== undefined;
264
321
  const nodes = flattenPageHeaderChildren(children);
265
322
  const headers = nodes.filter(
@@ -271,6 +328,8 @@ export function Page({
271
328
  const footers = nodes.filter(
272
329
  (node) => isValidElement(node) && node.type === PageFooter,
273
330
  );
331
+ const registerConsumer = useContext(PageConsumerContext);
332
+ useLayoutEffect(() => registerConsumer?.(), [registerConsumer]);
274
333
  const activeStateCount = useRef(0);
275
334
  const headingCount = useRef(0);
276
335
  const [integralState, setIntegralState] = useState(false);
@@ -323,7 +382,6 @@ export function Page({
323
382
  );
324
383
  }
325
384
  const bodyHeadingSources = bodies.reduce<{
326
- intro: number;
327
385
  content: number;
328
386
  state: number;
329
387
  }>(
@@ -333,20 +391,21 @@ export function Page({
333
391
  );
334
392
  return {
335
393
  content: total.content + count.content,
336
- intro: total.intro + count.intro,
337
394
  state: total.state + count.state,
338
395
  };
339
396
  },
340
- { content: 0, intro: 0, state: 0 },
397
+ { content: 0, state: 0 },
341
398
  );
399
+ if (bodyHeadingSources.content > 0)
400
+ throw new Error(
401
+ "Page explícito exige exatamente um heading principal em PageHeader ou PageState ativo dentro de PageBody.",
402
+ );
342
403
  const structuralHeadingSources =
343
404
  headers.reduce<number>(
344
405
  (total, header) =>
345
406
  total + countDirectChildrenOfType(header as ReactElement, PageTitle),
346
407
  0,
347
- ) +
348
- bodyHeadingSources.content +
349
- bodyHeadingSources.intro;
408
+ ) + bodyHeadingSources.content;
350
409
  const headingSources =
351
410
  bodyHeadingSources.state > 0
352
411
  ? bodyHeadingSources.state + bodyHeadingSources.content
@@ -358,7 +417,7 @@ export function Page({
358
417
  );
359
418
  if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
360
419
  throw new Error(
361
- "Page explícito exige exatamente um heading principal em PageHeader, PageIntro, Surface level={1} ou PageState ativo dentro de PageBody.",
420
+ "Page explícito exige exatamente um heading principal em PageHeader ou PageState ativo dentro de PageBody.",
362
421
  );
363
422
  }
364
423
  }
@@ -367,53 +426,64 @@ export function Page({
367
426
  if (integralState !== hasActiveIntegralState) return;
368
427
  if (headingCount.current !== 1) {
369
428
  throw new Error(
370
- "Page explícito exige exatamente um heading principal em PageHeader, PageIntro, Surface level={1} ou PageState ativo dentro de PageBody.",
429
+ "Page explícito exige exatamente um heading principal em PageHeader ou PageState ativo dentro de PageBody.",
371
430
  );
372
431
  }
373
432
  });
433
+ if (shell?.footer !== undefined && footers.length > 0) {
434
+ throw new Error(
435
+ "Page recebe o rodapé da Presentation; não declare outro PageFooter no conteúdo.",
436
+ );
437
+ }
438
+
374
439
  const content = shorthand ? (
375
440
  <>
376
441
  <PageHeader>
442
+ <PageTitle>{title}</PageTitle>
443
+ {description !== undefined && (
444
+ <PageDescription>{description}</PageDescription>
445
+ )}
446
+ {meta !== undefined && <PageMeta>{meta}</PageMeta>}
377
447
  {actions !== undefined && <PageActions>{actions}</PageActions>}
378
448
  </PageHeader>
379
- <PageBody>
380
- <PageIntro>
381
- <PageTitle>{title}</PageTitle>
382
- {description !== undefined && (
383
- <PageDescription>{description}</PageDescription>
384
- )}
385
- {meta !== undefined && <PageMeta>{meta}</PageMeta>}
386
- </PageIntro>
387
- {children}
388
- </PageBody>
449
+ <PageBody>{children}</PageBody>
450
+ {shell?.footer === undefined ? null : (
451
+ <PageFooter>{shell.footer}</PageFooter>
452
+ )}
389
453
  </>
390
454
  ) : (
391
455
  <>
392
- {headers.length === 0 &&
393
- (shell?.navigation !== undefined || shell?.actions !== undefined) ? (
394
- <PageHeader />
395
- ) : null}
396
456
  {children}
457
+ {shell?.footer === undefined || footers.length > 0 ? null : (
458
+ <PageFooter>{shell.footer}</PageFooter>
459
+ )}
397
460
  </>
398
461
  );
399
462
 
400
463
  return (
401
- <PageContext.Provider value={{ surfaceLayout, contentClassName }}>
464
+ <PageContext.Provider value={{ contentClassName, layout }}>
402
465
  <PageIntegralStateContext.Provider value={integralState}>
403
466
  <PageStatePresenceProvider register={registerIntegralState}>
404
467
  <PageHeadingPresenceProvider register={registerHeading}>
405
468
  <main
406
469
  data-slot="page"
407
470
  data-layout={layout}
408
- data-surface-layout={surfaceLayout}
471
+ data-width={width}
409
472
  className={cn(
410
- "flex h-full min-h-0 min-w-[64rem] flex-1 flex-col overflow-hidden",
473
+ "flex h-full min-h-0 min-w-[64rem] flex-1 flex-col",
474
+ layout === "flow" ? "overflow-y-auto" : "overflow-hidden",
411
475
  )}
412
476
  {...props}
413
477
  >
414
478
  <div
479
+ data-slot="page-column"
415
480
  className={cn(
416
- "flex h-full min-h-0 w-full flex-1 flex-col",
481
+ "mx-auto flex w-full flex-col",
482
+ pageWidthClasses[width],
483
+ "gap-4 p-10",
484
+ layout === "flow"
485
+ ? "min-h-full shrink-0"
486
+ : "h-full min-h-0 flex-1",
417
487
  className,
418
488
  )}
419
489
  >
@@ -428,7 +498,10 @@ export function Page({
428
498
  }
429
499
 
430
500
  /** Cabeçalho único da Page: navegação contextual, título e ações. */
431
- export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {}
501
+ export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {
502
+ /** TabsList abaixo do título; acompanha a rolagem definida por layout. */
503
+ tabs?: ReactNode;
504
+ }
432
505
 
433
506
  export function PageHeader({
434
507
  className,
@@ -442,39 +515,40 @@ export function PageHeader({
442
515
  if (integralState) return <></>;
443
516
 
444
517
  const nodes = flattenPageHeaderChildren(children);
445
- const localNavigation = nodes.filter(
446
- (node) =>
447
- isValidElement(node) &&
448
- node.type === PageNavigation &&
449
- hasRenderableNode((node.props as { children?: ReactNode }).children),
450
- );
451
518
  const localActions = nodes.filter(
452
519
  (node) =>
453
520
  isValidElement(node) &&
454
521
  node.type === PageActions &&
455
522
  hasRenderableNode((node.props as { children?: ReactNode }).children),
456
523
  ) as ReactElement<HTMLAttributes<HTMLDivElement>>[];
457
- if (shell?.navigation !== undefined && localNavigation.length > 0) {
458
- throw new Error(
459
- "PageHeader não combina a navegação de PageShell com PageNavigation local.",
460
- );
461
- }
462
524
  if (localActions.length > 1) {
463
525
  throw new Error("PageHeader aceita no máximo um PageActions.");
464
526
  }
527
+ const backs = nodes.filter(
528
+ (node) => isValidElement(node) && node.type === PageBack,
529
+ );
530
+ if (backs.length > 1)
531
+ throw new Error(
532
+ "PageHeader aceita no máximo um de cada slot, incluindo PageBack.",
533
+ );
465
534
  const structuralChildren = nodes.filter(
466
535
  (node) =>
467
536
  !(
468
537
  isValidElement(node) &&
469
- [PageNavigation, PageActions].includes(
470
- node.type as typeof PageNavigation,
471
- )
538
+ [PageBack, PageActions].includes(node.type as typeof PageActions)
472
539
  ),
473
540
  );
474
541
  const pageActions = localActions[0];
475
542
  const mergedActions =
476
- shell?.actions === undefined && pageActions === undefined ? null : (
543
+ shell?.actions === undefined &&
544
+ shell?.navigation === undefined &&
545
+ pageActions === undefined &&
546
+ !nodes.some(
547
+ (node) => isValidElement(node) && node.type === PageBack,
548
+ ) ? null : (
477
549
  <PageActions {...pageActions?.props}>
550
+ {nodes.filter((node) => isValidElement(node) && node.type === PageBack)}
551
+ {backs.length === 0 ? shell?.navigation : null}
478
552
  {shell?.actions}
479
553
  {pageActions?.props.children}
480
554
  </PageActions>
@@ -482,8 +556,7 @@ export function PageHeader({
482
556
  if (
483
557
  structuralChildren.length === 0 &&
484
558
  mergedActions === null &&
485
- localNavigation.length === 0 &&
486
- shell?.navigation === undefined
559
+ !hasRenderableNode(props.tabs)
487
560
  ) {
488
561
  return <></>;
489
562
  }
@@ -493,38 +566,23 @@ export function PageHeader({
493
566
  name="PageHeader"
494
567
  slot="page"
495
568
  slots={{
496
- leading: [PageBack, PageNavigation],
497
569
  title: PageTitle,
498
570
  description: PageDescription,
499
571
  meta: PageMeta,
500
572
  actions: PageActions,
501
573
  }}
502
- leadingPlacement={
503
- nodes.some(
504
- (node) => isValidElement(node) && node.type === PageTitle,
505
- ) ||
506
- (localNavigation.length === 0 &&
507
- shell?.navigation === undefined &&
508
- !nodes.some(
509
- (node) => isValidElement(node) && node.type === PageBack,
510
- ))
511
- ? "above"
512
- : "inline"
513
- }
514
- titleRequired={false}
574
+ leadingPlacement="inline"
575
+ titleRequired
515
576
  rowClassName={surfaceHeaderRowClasses("page")}
516
577
  className={cn(
517
- "shrink-0",
518
- surfaceRegionClasses("page", page!.surfaceLayout, "header"),
578
+ "mb-2 shrink-0",
579
+ surfaceRegionClasses("page", "header", {
580
+ bounded: false,
581
+ }),
519
582
  className,
520
583
  )}
521
584
  {...props}
522
585
  >
523
- {shell?.navigation === undefined ? (
524
- localNavigation
525
- ) : (
526
- <PageNavigation>{shell.navigation}</PageNavigation>
527
- )}
528
586
  {structuralChildren}
529
587
  {mergedActions}
530
588
  </StructuralHeader>
@@ -532,47 +590,14 @@ export function PageHeader({
532
590
  );
533
591
  }
534
592
 
535
- /** Primeiro bloco do corpo, separado dos controles do cabeçalho. */
536
- export function PageIntro({
537
- className,
538
- children,
539
- ...props
540
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
541
- requireParent(useContext(PageBodyContext), "PageIntro", "PageBody");
542
- const integralState = useContext(PageIntegralStateContext);
543
- if (integralState) return <></>;
544
- return (
545
- <PageIntroContext.Provider value>
546
- <StructuralHeader
547
- name="PageIntro"
548
- slot="page-intro"
549
- slots={{
550
- title: PageTitle,
551
- description: PageDescription,
552
- meta: PageMeta,
553
- actions: PageIntroActions,
554
- }}
555
- leadingPlacement="inline"
556
- titleRequired
557
- className={className}
558
- {...props}
559
- >
560
- {children}
561
- </StructuralHeader>
562
- </PageIntroContext.Provider>
563
- );
564
- }
565
-
566
593
  /** Badges, datas e autoria abaixo do título e da orientação. */
567
594
  export function PageMeta({
568
595
  className,
569
596
  ...props
570
597
  }: HTMLAttributes<HTMLDivElement>) {
571
598
  const header = useContext(PageHeaderContext);
572
- const intro = useContext(PageIntroContext);
573
- const inside = header || intro;
574
- if (!inside)
575
- throw new Error("PageMeta deve estar dentro de PageHeader ou PageIntro.");
599
+ const inside = header;
600
+ if (!inside) throw new Error("PageMeta deve estar dentro de PageHeader.");
576
601
  return (
577
602
  <div
578
603
  data-slot="page-meta"
@@ -582,37 +607,17 @@ export function PageMeta({
582
607
  );
583
608
  }
584
609
 
585
- /** Ações locais do conteúdo, distintas das ações da superfície em PageActions. */
586
- export function PageIntroActions({
587
- className,
588
- ...props
589
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
590
- requireParent(useContext(PageIntroContext), "PageIntroActions", "PageIntro");
591
- return (
592
- <div
593
- data-slot="page-intro-actions"
594
- className={cn(structuralHeaderClasses.page.actions, className)}
595
- {...props}
596
- />
597
- );
598
- }
599
-
600
610
  export function PageTitle({
601
611
  className,
602
612
  ...props
603
613
  }: HTMLAttributes<HTMLHeadingElement>): ReactElement {
604
614
  const insideHeader = useContext(PageHeaderContext);
605
- const insideIntro = useContext(PageIntroContext);
606
- requireParent(
607
- insideHeader || insideIntro,
608
- "PageTitle",
609
- "PageHeader ou PageIntro",
610
- );
615
+ requireParent(insideHeader, "PageTitle", "PageHeader");
611
616
  usePageHeadingPresence();
612
617
  return (
613
618
  <h1
614
619
  data-slot="page-title"
615
- className={cn(structuralHeaderClasses.page.title, className)}
620
+ className={cn(surfaceTitleClasses("page"), "text-3xl", className)}
616
621
  {...props}
617
622
  />
618
623
  );
@@ -624,8 +629,7 @@ export function PageDescription({
624
629
  ...props
625
630
  }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
626
631
  const header = useContext(PageHeaderContext);
627
- const intro = useContext(PageIntroContext);
628
- requireParent(header || intro, "PageDescription", "PageHeader ou PageIntro");
632
+ requireParent(header, "PageDescription", "PageHeader");
629
633
  return (
630
634
  <p
631
635
  data-slot="page-description"
@@ -643,41 +647,29 @@ export function PageActions({
643
647
  requireParent(insideHeader, "PageActions", "PageHeader");
644
648
  const target = useContext(PageActionsTargetContext);
645
649
  const actions = (
646
- <div
650
+ <ButtonGroup
651
+ mode="spaced"
647
652
  data-slot="page-actions"
648
- className={cn(structuralHeaderClasses.page.actions, className)}
653
+ className={cn("w-auto shrink-0 items-center", className)}
649
654
  {...props}
650
655
  />
651
656
  );
652
657
  return target === null ? actions : createPortal(actions, target);
653
658
  }
654
659
 
655
- export interface PageBackProps extends Omit<
656
- ComponentProps<"a">,
657
- "children" | "href"
658
- > {
660
+ export type PageBackProps = {
659
661
  /** Nome visível do destino pai e base do nome acessível. */
660
662
  children: ReactNode;
661
- /** Destino explícito. Obrigatório: sem `href` o elemento não é tabulável nem expõe
662
- * `role="link"`. */
663
- href: string;
664
- }
665
-
666
- /** Região introdutória para uma trilha estrutural com mais de um ancestral relevante. */
667
- export function PageNavigation({
668
- className,
669
- ...props
670
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
671
- const insideHeader = useContext(PageHeaderContext);
672
- requireParent(insideHeader, "PageNavigation", "PageHeader");
673
- return (
674
- <div
675
- data-slot="page-navigation-content"
676
- className={cn("min-w-0", className)}
677
- {...props}
678
- />
679
- );
680
- }
663
+ className?: string;
664
+ "aria-label"?: string;
665
+ } & (
666
+ | (Omit<ComponentProps<"a">, "children" | "href"> & {
667
+ href: string;
668
+ onBack?: never;
669
+ disabled?: never;
670
+ })
671
+ | { onBack: () => void; href?: never; disabled?: boolean }
672
+ );
681
673
 
682
674
  export function PageBack({
683
675
  children,
@@ -690,42 +682,73 @@ export function PageBack({
690
682
  const destination =
691
683
  typeof children === "string" ? children : "a página anterior";
692
684
  const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
693
- const back = (
694
- <Button asChild variant="ghost" size="icon-sm">
695
- <a
685
+ const content = (
686
+ <>
687
+ <ArrowLeft aria-hidden="true" />
688
+ <span className="sr-only">{children}</span>
689
+ </>
690
+ );
691
+ const back =
692
+ props.onBack !== undefined ? (
693
+ <Button
694
+ context="neutral"
695
+ variant="ghost"
696
+ size="icon"
696
697
  data-slot="page-back"
697
698
  aria-label={accessibleLabel}
698
- title={accessibleLabel}
699
699
  className={className}
700
- {...props}
700
+ disabled={props.disabled}
701
+ onClick={props.onBack}
701
702
  >
702
- <ArrowLeft aria-hidden="true" />
703
- <span className="sr-only">{children}</span>
704
- </a>
705
- </Button>
703
+ {content}
704
+ </Button>
705
+ ) : (
706
+ <Button asChild context="neutral" variant="ghost" size="icon">
707
+ <a
708
+ data-slot="page-back"
709
+ aria-label={accessibleLabel}
710
+ className={className}
711
+ {...props}
712
+ >
713
+ {content}
714
+ </a>
715
+ </Button>
716
+ );
717
+ return (
718
+ <Tooltip>
719
+ <TooltipTrigger asChild>{back}</TooltipTrigger>
720
+ <TooltipContent>{accessibleLabel}</TooltipContent>
721
+ </Tooltip>
706
722
  );
707
- return back;
708
723
  }
709
724
 
710
725
  export function PageBody({
726
+ flush = false,
711
727
  className,
712
728
  contentClassName,
713
729
  children,
714
730
  ...props
715
731
  }: HTMLAttributes<HTMLDivElement> & {
716
732
  contentClassName?: string;
733
+ /** Canvas e outras regiões que governam o próprio recuo. */
734
+ flush?: boolean;
717
735
  }): ReactElement {
718
736
  const page = useContext(PageContext);
719
737
  const fill = useFillRegion();
738
+ const integralState = useContext(PageIntegralStateContext);
720
739
  requireParent(page !== null, "PageBody", "Page");
721
- validateSurfaceIntro(children, PageIntro, "PageBody");
722
740
  return (
723
741
  <div
724
742
  data-slot="page-body"
725
743
  {...fill.attributes}
726
744
  className={cn(
727
- "flex min-h-0 flex-1 flex-col",
728
- fill.filling ? "overflow-hidden" : "overflow-y-auto",
745
+ "flex flex-col",
746
+ page!.layout === "flow" && !integralState
747
+ ? "shrink-0"
748
+ : [
749
+ "min-h-0 flex-1",
750
+ fill.filling ? "overflow-hidden" : "overflow-y-auto",
751
+ ],
729
752
  className,
730
753
  )}
731
754
  {...props}
@@ -735,9 +758,14 @@ export function PageBody({
735
758
  data-slot="page-body-content"
736
759
  {...fill.attributes}
737
760
  className={cn(
738
- "mx-auto w-full max-w-7xl",
739
- surfaceRegionClasses("page", page!.surfaceLayout, "body"),
740
- fill.filling ? "min-h-0 flex-1" : "min-h-full shrink-0",
761
+ "w-full",
762
+ surfaceRegionClasses("page", "body", {
763
+ flush,
764
+ bounded: false,
765
+ }),
766
+ fill.filling || integralState
767
+ ? "min-h-0 flex-1"
768
+ : "min-h-full shrink-0",
741
769
  page!.contentClassName,
742
770
  contentClassName,
743
771
  )}
@@ -760,7 +788,9 @@ export function PageFooter({
760
788
  data-slot="page-footer"
761
789
  className={cn(
762
790
  "mt-auto flex min-h-[3.75rem] shrink-0 items-center justify-end",
763
- surfaceRegionClasses("page", page!.surfaceLayout, "footer"),
791
+ surfaceRegionClasses("page", "footer", {
792
+ bounded: false,
793
+ }),
764
794
  className,
765
795
  )}
766
796
  {...props}