@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
@@ -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,49 +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
- validateSurfaceIntro,
35
41
  structuralHeaderClasses,
42
+ structuralMetaClasses,
36
43
  } from "./structural-header.tsx";
37
- import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
38
- import { FillRoot, useFillRegion } from "./fill-region.ts";
44
+ import { Surface } from "./surface.tsx";
39
45
 
40
46
  import {
47
+ surfaceTitleClasses,
41
48
  surfaceHeaderRowClasses,
42
49
  surfaceRegionClasses,
43
- type SurfaceLayout,
44
50
  } from "./surface-anatomy.ts";
45
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
+
46
59
  interface PageContextValue {
47
- surfaceLayout: SurfaceLayout;
60
+ layout: "flow" | "fill";
48
61
  contentClassName?: string;
49
62
  }
50
63
 
51
64
  const PageContext = createContext<PageContextValue | null>(null);
52
65
  const PageHeaderContext = createContext(false);
53
- const PageIntroContext = createContext(false);
54
66
  const PageBodyContext = createContext(false);
55
67
  const PageIntegralStateContext = createContext(false);
56
68
  const PageActionsTargetContext = createContext<HTMLElement | null>(null);
57
69
  interface PageShellContextValue {
58
70
  navigation?: ReactNode;
71
+ width?: PageWidth;
59
72
  actions?: ReactNode;
73
+ footer?: ReactNode;
60
74
  }
75
+ const PageConsumerContext = createContext<(() => () => void) | null>(null);
61
76
  const PageShellContext = createContext<PageShellContextValue | null>(null);
62
77
 
63
78
  function hasRenderableNode(node: ReactNode): boolean {
@@ -72,17 +87,10 @@ function hasRenderableNode(node: ReactNode): boolean {
72
87
  return true;
73
88
  }
74
89
 
75
- /** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
76
- export function useInsidePageShell(): boolean {
77
- return useContext(PageShellContext) !== null;
78
- }
79
-
80
90
  export interface PageShellProps extends Omit<
81
91
  HTMLAttributes<HTMLDivElement>,
82
92
  "children"
83
93
  > {
84
- /** Navegação contextual conhecida pelo shell, normalmente um Breadcrumb. */
85
- navigation?: ReactNode;
86
94
  /** Ações do shell ou do recurso que antecedem as ações declaradas pela Page. */
87
95
  actions?: ReactNode;
88
96
  /** Uma Page descendente, diretamente ou através da rota ativa. */
@@ -91,14 +99,12 @@ export interface PageShellProps extends Omit<
91
99
 
92
100
  /** Moldura que coordena contexto do shell com o PageHeader da página ativa. */
93
101
  export function PageShell({
94
- navigation,
95
102
  actions,
96
103
  children,
97
104
  className,
98
105
  ...props
99
106
  }: PageShellProps): ReactElement {
100
107
  const shell = {
101
- ...(hasRenderableNode(navigation) ? { navigation } : {}),
102
108
  ...(hasRenderableNode(actions) ? { actions } : {}),
103
109
  };
104
110
  return (
@@ -122,6 +128,60 @@ export function PageShell({
122
128
  );
123
129
  }
124
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
+
125
185
  export function PageActionsTarget({
126
186
  target,
127
187
  children,
@@ -148,23 +208,25 @@ interface PageBaseProps extends Omit<
148
208
  HTMLAttributes<HTMLElement>,
149
209
  "children" | "title"
150
210
  > {
151
- /** 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. */
152
212
  className?: string;
153
- /** Classes do conteúdo centralizado, incluindo largura máxima e alinhamento. */
213
+ /** Classes adicionais do conteúdo do body. */
154
214
  contentClassName?: string;
155
215
  /**
156
- * `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.
157
217
  * A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Surface` (ADR 0024).
158
218
  */
159
219
  layout?: "flow" | "fill";
160
- /** Divisão visual das regiões, independente da distribuição de altura. */
161
- surfaceLayout?: SurfaceLayout;
220
+ /** Limita a coluna inteira, mantendo header e body alinhados. */
221
+ width?: PageWidth;
162
222
  }
163
223
 
164
224
  interface PageShorthandProps extends PageBaseProps {
165
225
  title: ReactNode;
166
226
  /** Orientação opcional sobre a finalidade da página; regras pertencem ao corpo. */
167
227
  description?: ReactNode;
228
+ /** Badges, datas e autoria sob a orientação. */
229
+ meta?: ReactNode;
168
230
  actions?: ReactNode;
169
231
  children: ReactNode;
170
232
  }
@@ -172,6 +234,7 @@ interface PageShorthandProps extends PageBaseProps {
172
234
  interface PageComposedProps extends PageBaseProps {
173
235
  title?: never;
174
236
  description?: never;
237
+ meta?: never;
175
238
  actions?: never;
176
239
  children: ReactNode;
177
240
  }
@@ -199,7 +262,6 @@ function countDirectChildrenOfType(
199
262
 
200
263
  function countPageBodyHeadings(node: ReactNode): {
201
264
  content: number;
202
- intro: number;
203
265
  state: number;
204
266
  } {
205
267
  return Children.toArray(node).reduce(
@@ -213,7 +275,6 @@ function countPageBodyHeadings(node: ReactNode): {
213
275
  if (child.type === PageState && props.status !== "ready") {
214
276
  return {
215
277
  content: total.content,
216
- intro: total.intro,
217
278
  state: total.state + 1,
218
279
  };
219
280
  }
@@ -223,11 +284,10 @@ function countPageBodyHeadings(node: ReactNode): {
223
284
  total.content +
224
285
  (child.type === Surface && props.level === 1 ? 1 : 0) +
225
286
  nested.content,
226
- intro: total.intro + (child.type === PageIntro ? 1 : 0) + nested.intro,
227
287
  state: total.state + nested.state,
228
288
  };
229
289
  },
230
- { content: 0, intro: 0, state: 0 },
290
+ { content: 0, state: 0 },
231
291
  );
232
292
  }
233
293
 
@@ -246,15 +306,17 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
246
306
  export function Page({
247
307
  title,
248
308
  description,
309
+ meta,
249
310
  actions,
250
311
  layout = "flow",
251
- surfaceLayout = "sectioned",
312
+ width: widthProp,
252
313
  contentClassName,
253
314
  className,
254
315
  children,
255
316
  ...props
256
317
  }: PageProps): ReactElement {
257
318
  const shell = useContext(PageShellContext);
319
+ const width = widthProp ?? shell?.width ?? "wide";
258
320
  const shorthand = title !== undefined;
259
321
  const nodes = flattenPageHeaderChildren(children);
260
322
  const headers = nodes.filter(
@@ -266,6 +328,8 @@ export function Page({
266
328
  const footers = nodes.filter(
267
329
  (node) => isValidElement(node) && node.type === PageFooter,
268
330
  );
331
+ const registerConsumer = useContext(PageConsumerContext);
332
+ useLayoutEffect(() => registerConsumer?.(), [registerConsumer]);
269
333
  const activeStateCount = useRef(0);
270
334
  const headingCount = useRef(0);
271
335
  const [integralState, setIntegralState] = useState(false);
@@ -318,7 +382,6 @@ export function Page({
318
382
  );
319
383
  }
320
384
  const bodyHeadingSources = bodies.reduce<{
321
- intro: number;
322
385
  content: number;
323
386
  state: number;
324
387
  }>(
@@ -328,20 +391,21 @@ export function Page({
328
391
  );
329
392
  return {
330
393
  content: total.content + count.content,
331
- intro: total.intro + count.intro,
332
394
  state: total.state + count.state,
333
395
  };
334
396
  },
335
- { content: 0, intro: 0, state: 0 },
397
+ { content: 0, state: 0 },
336
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
+ );
337
403
  const structuralHeadingSources =
338
404
  headers.reduce<number>(
339
405
  (total, header) =>
340
406
  total + countDirectChildrenOfType(header as ReactElement, PageTitle),
341
407
  0,
342
- ) +
343
- bodyHeadingSources.content +
344
- bodyHeadingSources.intro;
408
+ ) + bodyHeadingSources.content;
345
409
  const headingSources =
346
410
  bodyHeadingSources.state > 0
347
411
  ? bodyHeadingSources.state + bodyHeadingSources.content
@@ -353,7 +417,7 @@ export function Page({
353
417
  );
354
418
  if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
355
419
  throw new Error(
356
- "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.",
357
421
  );
358
422
  }
359
423
  }
@@ -362,52 +426,64 @@ export function Page({
362
426
  if (integralState !== hasActiveIntegralState) return;
363
427
  if (headingCount.current !== 1) {
364
428
  throw new Error(
365
- "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.",
366
430
  );
367
431
  }
368
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
+
369
439
  const content = shorthand ? (
370
440
  <>
371
441
  <PageHeader>
442
+ <PageTitle>{title}</PageTitle>
443
+ {description !== undefined && (
444
+ <PageDescription>{description}</PageDescription>
445
+ )}
446
+ {meta !== undefined && <PageMeta>{meta}</PageMeta>}
372
447
  {actions !== undefined && <PageActions>{actions}</PageActions>}
373
448
  </PageHeader>
374
- <PageBody>
375
- <PageIntro>
376
- <PageTitle>{title}</PageTitle>
377
- {description !== undefined && (
378
- <PageDescription>{description}</PageDescription>
379
- )}
380
- </PageIntro>
381
- {children}
382
- </PageBody>
449
+ <PageBody>{children}</PageBody>
450
+ {shell?.footer === undefined ? null : (
451
+ <PageFooter>{shell.footer}</PageFooter>
452
+ )}
383
453
  </>
384
454
  ) : (
385
455
  <>
386
- {headers.length === 0 &&
387
- (shell?.navigation !== undefined || shell?.actions !== undefined) ? (
388
- <PageHeader />
389
- ) : null}
390
456
  {children}
457
+ {shell?.footer === undefined || footers.length > 0 ? null : (
458
+ <PageFooter>{shell.footer}</PageFooter>
459
+ )}
391
460
  </>
392
461
  );
393
462
 
394
463
  return (
395
- <PageContext.Provider value={{ surfaceLayout, contentClassName }}>
464
+ <PageContext.Provider value={{ contentClassName, layout }}>
396
465
  <PageIntegralStateContext.Provider value={integralState}>
397
466
  <PageStatePresenceProvider register={registerIntegralState}>
398
467
  <PageHeadingPresenceProvider register={registerHeading}>
399
468
  <main
400
469
  data-slot="page"
401
470
  data-layout={layout}
402
- data-surface-layout={surfaceLayout}
471
+ data-width={width}
403
472
  className={cn(
404
- "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",
405
475
  )}
406
476
  {...props}
407
477
  >
408
478
  <div
479
+ data-slot="page-column"
409
480
  className={cn(
410
- "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",
411
487
  className,
412
488
  )}
413
489
  >
@@ -422,7 +498,10 @@ export function Page({
422
498
  }
423
499
 
424
500
  /** Cabeçalho único da Page: navegação contextual, título e ações. */
425
- 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
+ }
426
505
 
427
506
  export function PageHeader({
428
507
  className,
@@ -436,39 +515,40 @@ export function PageHeader({
436
515
  if (integralState) return <></>;
437
516
 
438
517
  const nodes = flattenPageHeaderChildren(children);
439
- const localNavigation = nodes.filter(
440
- (node) =>
441
- isValidElement(node) &&
442
- node.type === PageNavigation &&
443
- hasRenderableNode((node.props as { children?: ReactNode }).children),
444
- );
445
518
  const localActions = nodes.filter(
446
519
  (node) =>
447
520
  isValidElement(node) &&
448
521
  node.type === PageActions &&
449
522
  hasRenderableNode((node.props as { children?: ReactNode }).children),
450
523
  ) as ReactElement<HTMLAttributes<HTMLDivElement>>[];
451
- if (shell?.navigation !== undefined && localNavigation.length > 0) {
452
- throw new Error(
453
- "PageHeader não combina a navegação de PageShell com PageNavigation local.",
454
- );
455
- }
456
524
  if (localActions.length > 1) {
457
525
  throw new Error("PageHeader aceita no máximo um PageActions.");
458
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
+ );
459
534
  const structuralChildren = nodes.filter(
460
535
  (node) =>
461
536
  !(
462
537
  isValidElement(node) &&
463
- [PageNavigation, PageActions].includes(
464
- node.type as typeof PageNavigation,
465
- )
538
+ [PageBack, PageActions].includes(node.type as typeof PageActions)
466
539
  ),
467
540
  );
468
541
  const pageActions = localActions[0];
469
542
  const mergedActions =
470
- 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 : (
471
549
  <PageActions {...pageActions?.props}>
550
+ {nodes.filter((node) => isValidElement(node) && node.type === PageBack)}
551
+ {backs.length === 0 ? shell?.navigation : null}
472
552
  {shell?.actions}
473
553
  {pageActions?.props.children}
474
554
  </PageActions>
@@ -476,8 +556,7 @@ export function PageHeader({
476
556
  if (
477
557
  structuralChildren.length === 0 &&
478
558
  mergedActions === null &&
479
- localNavigation.length === 0 &&
480
- shell?.navigation === undefined
559
+ !hasRenderableNode(props.tabs)
481
560
  ) {
482
561
  return <></>;
483
562
  }
@@ -487,37 +566,23 @@ export function PageHeader({
487
566
  name="PageHeader"
488
567
  slot="page"
489
568
  slots={{
490
- leading: [PageBack, PageNavigation],
491
569
  title: PageTitle,
492
570
  description: PageDescription,
571
+ meta: PageMeta,
493
572
  actions: PageActions,
494
573
  }}
495
- leadingPlacement={
496
- nodes.some(
497
- (node) => isValidElement(node) && node.type === PageTitle,
498
- ) ||
499
- (localNavigation.length === 0 &&
500
- shell?.navigation === undefined &&
501
- !nodes.some(
502
- (node) => isValidElement(node) && node.type === PageBack,
503
- ))
504
- ? "above"
505
- : "inline"
506
- }
507
- titleRequired={false}
574
+ leadingPlacement="inline"
575
+ titleRequired
508
576
  rowClassName={surfaceHeaderRowClasses("page")}
509
577
  className={cn(
510
- "shrink-0",
511
- surfaceRegionClasses("page", page!.surfaceLayout, "header"),
578
+ "mb-2 shrink-0",
579
+ surfaceRegionClasses("page", "header", {
580
+ bounded: false,
581
+ }),
512
582
  className,
513
583
  )}
514
584
  {...props}
515
585
  >
516
- {shell?.navigation === undefined ? (
517
- localNavigation
518
- ) : (
519
- <PageNavigation>{shell.navigation}</PageNavigation>
520
- )}
521
586
  {structuralChildren}
522
587
  {mergedActions}
523
588
  </StructuralHeader>
@@ -525,46 +590,18 @@ export function PageHeader({
525
590
  );
526
591
  }
527
592
 
528
- /** Primeiro bloco do corpo, separado dos controles do cabeçalho. */
529
- export function PageIntro({
530
- className,
531
- children,
532
- ...props
533
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
534
- requireParent(useContext(PageBodyContext), "PageIntro", "PageBody");
535
- const integralState = useContext(PageIntegralStateContext);
536
- if (integralState) return <></>;
537
- return (
538
- <PageIntroContext.Provider value>
539
- <StructuralHeader
540
- name="PageIntro"
541
- slot="page-intro"
542
- slots={{
543
- title: PageTitle,
544
- description: PageDescription,
545
- actions: PageIntroActions,
546
- }}
547
- leadingPlacement="inline"
548
- titleRequired
549
- className={className}
550
- {...props}
551
- >
552
- {children}
553
- </StructuralHeader>
554
- </PageIntroContext.Provider>
555
- );
556
- }
557
-
558
- /** Ações locais do conteúdo, distintas das ações da superfície em PageActions. */
559
- export function PageIntroActions({
593
+ /** Badges, datas e autoria abaixo do título e da orientação. */
594
+ export function PageMeta({
560
595
  className,
561
596
  ...props
562
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
563
- requireParent(useContext(PageIntroContext), "PageIntroActions", "PageIntro");
597
+ }: HTMLAttributes<HTMLDivElement>) {
598
+ const header = useContext(PageHeaderContext);
599
+ const inside = header;
600
+ if (!inside) throw new Error("PageMeta deve estar dentro de PageHeader.");
564
601
  return (
565
602
  <div
566
- data-slot="page-intro-actions"
567
- className={cn(structuralHeaderClasses.page.actions, className)}
603
+ data-slot="page-meta"
604
+ className={cn(structuralMetaClasses, className)}
568
605
  {...props}
569
606
  />
570
607
  );
@@ -575,17 +612,12 @@ export function PageTitle({
575
612
  ...props
576
613
  }: HTMLAttributes<HTMLHeadingElement>): ReactElement {
577
614
  const insideHeader = useContext(PageHeaderContext);
578
- const insideIntro = useContext(PageIntroContext);
579
- requireParent(
580
- insideHeader || insideIntro,
581
- "PageTitle",
582
- "PageHeader ou PageIntro",
583
- );
615
+ requireParent(insideHeader, "PageTitle", "PageHeader");
584
616
  usePageHeadingPresence();
585
617
  return (
586
618
  <h1
587
619
  data-slot="page-title"
588
- className={cn(structuralHeaderClasses.page.title, className)}
620
+ className={cn(surfaceTitleClasses("page"), "text-3xl", className)}
589
621
  {...props}
590
622
  />
591
623
  );
@@ -597,8 +629,7 @@ export function PageDescription({
597
629
  ...props
598
630
  }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
599
631
  const header = useContext(PageHeaderContext);
600
- const intro = useContext(PageIntroContext);
601
- requireParent(header || intro, "PageDescription", "PageHeader ou PageIntro");
632
+ requireParent(header, "PageDescription", "PageHeader");
602
633
  return (
603
634
  <p
604
635
  data-slot="page-description"
@@ -616,41 +647,29 @@ export function PageActions({
616
647
  requireParent(insideHeader, "PageActions", "PageHeader");
617
648
  const target = useContext(PageActionsTargetContext);
618
649
  const actions = (
619
- <div
650
+ <ButtonGroup
651
+ mode="spaced"
620
652
  data-slot="page-actions"
621
- className={cn(structuralHeaderClasses.page.actions, className)}
653
+ className={cn("w-auto shrink-0 items-center", className)}
622
654
  {...props}
623
655
  />
624
656
  );
625
657
  return target === null ? actions : createPortal(actions, target);
626
658
  }
627
659
 
628
- export interface PageBackProps extends Omit<
629
- ComponentProps<"a">,
630
- "children" | "href"
631
- > {
660
+ export type PageBackProps = {
632
661
  /** Nome visível do destino pai e base do nome acessível. */
633
662
  children: ReactNode;
634
- /** Destino explícito. Obrigatório: sem `href` o elemento não é tabulável nem expõe
635
- * `role="link"`. */
636
- href: string;
637
- }
638
-
639
- /** Região introdutória para uma trilha estrutural com mais de um ancestral relevante. */
640
- export function PageNavigation({
641
- className,
642
- ...props
643
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
644
- const insideHeader = useContext(PageHeaderContext);
645
- requireParent(insideHeader, "PageNavigation", "PageHeader");
646
- return (
647
- <div
648
- data-slot="page-navigation-content"
649
- className={cn("min-w-0", className)}
650
- {...props}
651
- />
652
- );
653
- }
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
+ );
654
673
 
655
674
  export function PageBack({
656
675
  children,
@@ -663,42 +682,73 @@ export function PageBack({
663
682
  const destination =
664
683
  typeof children === "string" ? children : "a página anterior";
665
684
  const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
666
- const back = (
667
- <Button asChild variant="ghost" size="icon-sm">
668
- <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"
669
697
  data-slot="page-back"
670
698
  aria-label={accessibleLabel}
671
- title={accessibleLabel}
672
699
  className={className}
673
- {...props}
700
+ disabled={props.disabled}
701
+ onClick={props.onBack}
674
702
  >
675
- <ArrowLeft aria-hidden="true" />
676
- <span className="sr-only">{children}</span>
677
- </a>
678
- </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>
679
722
  );
680
- return back;
681
723
  }
682
724
 
683
725
  export function PageBody({
726
+ flush = false,
684
727
  className,
685
728
  contentClassName,
686
729
  children,
687
730
  ...props
688
731
  }: HTMLAttributes<HTMLDivElement> & {
689
732
  contentClassName?: string;
733
+ /** Canvas e outras regiões que governam o próprio recuo. */
734
+ flush?: boolean;
690
735
  }): ReactElement {
691
736
  const page = useContext(PageContext);
692
737
  const fill = useFillRegion();
738
+ const integralState = useContext(PageIntegralStateContext);
693
739
  requireParent(page !== null, "PageBody", "Page");
694
- validateSurfaceIntro(children, PageIntro, "PageBody");
695
740
  return (
696
741
  <div
697
742
  data-slot="page-body"
698
743
  {...fill.attributes}
699
744
  className={cn(
700
- "flex min-h-0 flex-1 flex-col",
701
- 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
+ ],
702
752
  className,
703
753
  )}
704
754
  {...props}
@@ -708,9 +758,14 @@ export function PageBody({
708
758
  data-slot="page-body-content"
709
759
  {...fill.attributes}
710
760
  className={cn(
711
- "mx-auto w-full max-w-7xl",
712
- surfaceRegionClasses("page", page!.surfaceLayout, "body"),
713
- 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",
714
769
  page!.contentClassName,
715
770
  contentClassName,
716
771
  )}
@@ -733,7 +788,9 @@ export function PageFooter({
733
788
  data-slot="page-footer"
734
789
  className={cn(
735
790
  "mt-auto flex min-h-[3.75rem] shrink-0 items-center justify-end",
736
- surfaceRegionClasses("page", page!.surfaceLayout, "footer"),
791
+ surfaceRegionClasses("page", "footer", {
792
+ bounded: false,
793
+ }),
737
794
  className,
738
795
  )}
739
796
  {...props}