@softize/opus 17.1.0 → 18.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 (42) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/bin/lib/check.mjs +212 -45
  3. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +13 -11
  4. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +14 -2
  5. package/docs/adr/0016-list-collection-header-belongs-to-content.md +80 -0
  6. package/package.json +1 -1
  7. package/registry/skills/build-opus-ui/SKILL.md +31 -20
  8. package/registry/skills/build-opus-ui/references/evaluations.md +10 -3
  9. package/registry/skills/build-opus-ui/references/ui-patterns.md +28 -16
  10. package/src/core/presentation.ts +223 -24
  11. package/src/core/runtime.ts +3 -0
  12. package/src/core/types.ts +2 -0
  13. package/src/mcp/index.ts +1 -0
  14. package/src/ui/components/patterns/action-form-card.tsx +8 -1
  15. package/src/ui/components/patterns/confirm.tsx +194 -157
  16. package/src/ui/components/patterns/content-header.tsx +17 -2
  17. package/src/ui/components/patterns/form-dialog.tsx +28 -14
  18. package/src/ui/components/patterns/form.tsx +340 -222
  19. package/src/ui/components/patterns/list.tsx +43 -44
  20. package/src/ui/components/patterns/page-heading-context.tsx +34 -0
  21. package/src/ui/components/patterns/page-state.tsx +2 -0
  22. package/src/ui/components/patterns/page.tsx +195 -83
  23. package/src/ui/components/patterns/presentation.tsx +140 -86
  24. package/src/ui/components/patterns/surface-header.tsx +5 -6
  25. package/src/ui/components/patterns/trigger.tsx +112 -82
  26. package/src/ui/components/primitives/button.tsx +2 -2
  27. package/src/ui/components/primitives/chat.tsx +19 -5
  28. package/src/ui/components/primitives/control.ts +9 -3
  29. package/src/ui/components/primitives/dialog.tsx +16 -9
  30. package/src/ui/components/primitives/drawer.tsx +9 -6
  31. package/src/ui/docs/content/action-form-card.md +9 -8
  32. package/src/ui/docs/content/action-form-dialog.md +11 -12
  33. package/src/ui/docs/content/action-form.md +25 -25
  34. package/src/ui/docs/content/action-list.md +101 -70
  35. package/src/ui/docs/content/chat.md +4 -4
  36. package/src/ui/docs/content/content.md +29 -13
  37. package/src/ui/docs/content/dialog.md +27 -21
  38. package/src/ui/docs/content/drawer.md +8 -6
  39. package/src/ui/docs/content/page.md +42 -46
  40. package/src/ui/docs/content/presentation.md +39 -28
  41. package/src/ui/docs/doc-client.tsx +1 -1
  42. package/src/ui/meta.ts +4 -4
@@ -38,7 +38,7 @@ import {
38
38
  Calendar as CalendarIcon,
39
39
  Eraser,
40
40
  Filter,
41
- RefreshCw,
41
+ RotateCw,
42
42
  Settings2,
43
43
  Table2,
44
44
  X,
@@ -877,27 +877,27 @@ function AdvancedFiltersDialog({
877
877
  ))}
878
878
  </DialogBody>
879
879
  <DialogFooter>
880
- <Button
881
- variant="outline"
882
- size="sm"
883
- onClick={() => {
884
- const cleared = { ...draft };
885
- for (const [name] of specs) delete cleared[name];
886
- onApply(cleared);
887
- onOpenChange(false);
888
- }}
889
- >
890
- Limpar
891
- </Button>
892
- <Button
893
- size="sm"
894
- onClick={() => {
895
- onApply(draft);
896
- onOpenChange(false);
897
- }}
898
- >
899
- Aplicar
900
- </Button>
880
+ <ButtonGroup mode="spaced" distribution="equal">
881
+ <Button
882
+ variant="outline"
883
+ onClick={() => {
884
+ const cleared = { ...draft };
885
+ for (const [name] of specs) delete cleared[name];
886
+ onApply(cleared);
887
+ onOpenChange(false);
888
+ }}
889
+ >
890
+ Limpar
891
+ </Button>
892
+ <Button
893
+ onClick={() => {
894
+ onApply(draft);
895
+ onOpenChange(false);
896
+ }}
897
+ >
898
+ Aplicar
899
+ </Button>
900
+ </ButtonGroup>
901
901
  </DialogFooter>
902
902
  </DialogContent>
903
903
  </Dialog>
@@ -1083,7 +1083,7 @@ export function ActionFilterBar({
1083
1083
  onClick={() => void onRefresh()}
1084
1084
  disabled={refreshing}
1085
1085
  >
1086
- <RefreshCw
1086
+ <RotateCw
1087
1087
  className={cn(
1088
1088
  "h-3.5 w-3.5",
1089
1089
  refreshing && "animate-spin",
@@ -1845,27 +1845,26 @@ export function ActionList<TInput, TItem>({
1845
1845
  </DialogBody>
1846
1846
  )}
1847
1847
  <DialogFooter>
1848
- <DialogClose initialFocus asChild>
1849
- <Button variant="ghost" size="sm">
1850
- Cancelar
1851
- </Button>
1852
- </DialogClose>
1853
- <DialogClose asChild>
1854
- <Button
1855
- size="sm"
1856
- context={
1857
- confirming.destructive === true ? "danger" : "primary"
1858
- }
1859
- variant="solid"
1860
- onClick={() => {
1861
- const a = confirming;
1862
- setConfirming(null);
1863
- void runBatch(a);
1864
- }}
1865
- >
1866
- {confirming.label}
1867
- </Button>
1868
- </DialogClose>
1848
+ <ButtonGroup mode="spaced" distribution="equal">
1849
+ <DialogClose initialFocus asChild>
1850
+ <Button variant="outline">Cancelar</Button>
1851
+ </DialogClose>
1852
+ <DialogClose asChild>
1853
+ <Button
1854
+ context={
1855
+ confirming.destructive === true ? "danger" : "primary"
1856
+ }
1857
+ variant="solid"
1858
+ onClick={() => {
1859
+ const a = confirming;
1860
+ setConfirming(null);
1861
+ void runBatch(a);
1862
+ }}
1863
+ >
1864
+ {confirming.label}
1865
+ </Button>
1866
+ </DialogClose>
1867
+ </ButtonGroup>
1869
1868
  </DialogFooter>
1870
1869
  </DialogContent>
1871
1870
  </Dialog>
@@ -0,0 +1,34 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useLayoutEffect,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ } from "react";
8
+
9
+ const PageHeadingPresenceContext = createContext<
10
+ (() => () => void) | null
11
+ >(null);
12
+
13
+ export function PageHeadingPresenceProvider({
14
+ register,
15
+ children,
16
+ }: {
17
+ register: () => () => void;
18
+ children: ReactNode;
19
+ }): ReactElement {
20
+ return (
21
+ <PageHeadingPresenceContext.Provider value={register}>
22
+ {children}
23
+ </PageHeadingPresenceContext.Provider>
24
+ );
25
+ }
26
+
27
+ /** Integração interna para headings que podem nascer dentro de boundaries intermediários. */
28
+ export function usePageHeadingPresence(active = true): void {
29
+ const register = useContext(PageHeadingPresenceContext);
30
+ useLayoutEffect(() => {
31
+ if (!active || register === null) return;
32
+ return register();
33
+ }, [active, register]);
34
+ }
@@ -14,6 +14,7 @@ import {
14
14
  type ReactNode,
15
15
  } from "react";
16
16
  import { cn } from "../../lib/cn.ts";
17
+ import { usePageHeadingPresence } from "./page-heading-context.tsx";
17
18
  import {
18
19
  DEFAULT_RETRY_LABEL,
19
20
  EmptySurface,
@@ -77,6 +78,7 @@ export function PageState({
77
78
  className,
78
79
  }: PageStateProps): React.ReactElement {
79
80
  const registerPresence = useContext(PageStatePresenceContext);
81
+ usePageHeadingPresence(status !== "ready");
80
82
  useLayoutEffect(() => {
81
83
  if (status === "ready" || registerPresence === null) return;
82
84
  return registerPresence();
@@ -11,6 +11,7 @@ import {
11
11
  isValidElement,
12
12
  useCallback,
13
13
  useContext,
14
+ useLayoutEffect,
14
15
  useRef,
15
16
  useState,
16
17
  type HTMLAttributes,
@@ -22,8 +23,13 @@ import { createPortal } from "react-dom";
22
23
  import { ArrowLeft } from "lucide-react";
23
24
  import { cn } from "../../lib/cn.ts";
24
25
  import { Button } from "../primitives/button.tsx";
26
+ import { Content } from "./content-header.tsx";
27
+ import {
28
+ PageHeadingPresenceProvider,
29
+ usePageHeadingPresence,
30
+ } from "./page-heading-context.tsx";
25
31
  import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
26
- import { PageStatePresenceProvider } from "./page-state.tsx";
32
+ import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
27
33
 
28
34
  interface PageContextValue {}
29
35
 
@@ -34,11 +40,10 @@ const PageHeaderContext = createContext<PageHeadingRegion | null>(null);
34
40
  const PageIntegralStateContext = createContext(false);
35
41
  const PageActionsTargetContext = createContext<HTMLElement | null>(null);
36
42
  const PageNavigationTargetContext = createContext<HTMLElement | null>(null);
37
- const PageTitleTargetContext = createContext<HTMLElement | null>(null);
38
43
  const PageShellContext = createContext(false);
39
44
  const pageBarClassName =
40
- "flex h-12 shrink-0 items-center border-b border-border bg-background text-foreground";
41
- const pageBarContentClassName = "w-full py-2";
45
+ "flex h-[3.75rem] shrink-0 items-center border-b border-border bg-background text-foreground";
46
+ const pageBarContentClassName = "w-full p-3";
42
47
 
43
48
  /** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
44
49
  export function useInsidePageShell(): boolean {
@@ -124,7 +129,6 @@ export function PageShell({
124
129
  );
125
130
  const [navigationTarget, setNavigationTarget] =
126
131
  useState<HTMLDivElement | null>(null);
127
- const [titleTarget, setTitleTarget] = useState<HTMLDivElement | null>(null);
128
132
 
129
133
  return (
130
134
  <div
@@ -152,20 +156,18 @@ export function PageShell({
152
156
  <PageShellNavigationSlot targetRef={setNavigationTarget}>
153
157
  {navigation}
154
158
  </PageShellNavigationSlot>
155
- <PageShellTitleSlot targetRef={setTitleTarget} />
159
+ <PageShellTitleSlot />
156
160
  <PageShellActionsSlot targetRef={setActionsTarget}>
157
161
  {actions}
158
162
  </PageShellActionsSlot>
159
163
  </SurfaceHeader>
160
164
  <PageShellContext.Provider value>
161
165
  <PageNavigationTargetContext.Provider value={navigationTarget}>
162
- <PageTitleTargetContext.Provider value={titleTarget}>
163
- <PageActionsTarget target={actionsTarget}>
164
- <div data-slot="page-shell-content" className="min-h-0 flex-1">
165
- {children}
166
- </div>
167
- </PageActionsTarget>
168
- </PageTitleTargetContext.Provider>
166
+ <PageActionsTarget target={actionsTarget}>
167
+ <div data-slot="page-shell-content" className="min-h-0 flex-1">
168
+ {children}
169
+ </div>
170
+ </PageActionsTarget>
169
171
  </PageNavigationTargetContext.Provider>
170
172
  </PageShellContext.Provider>
171
173
  </div>
@@ -217,6 +219,59 @@ interface PageComposedProps extends PageBaseProps {
217
219
 
218
220
  export type PageProps = PageShorthandProps | PageComposedProps;
219
221
 
222
+ function countDirectChildrenOfType(
223
+ node: ReactElement,
224
+ type: typeof PageTitle,
225
+ ): number {
226
+ return Children.toArray(
227
+ (node.props as { children?: ReactNode }).children,
228
+ ).filter((child) => isValidElement(child) && child.type === type).length;
229
+ }
230
+
231
+ function countPageBodyHeadings(node: ReactNode): {
232
+ content: number;
233
+ state: number;
234
+ } {
235
+ return Children.toArray(node).reduce(
236
+ (total, child) => {
237
+ if (!isValidElement(child)) return total;
238
+ const props = child.props as {
239
+ children?: ReactNode;
240
+ level?: number;
241
+ status?: string;
242
+ };
243
+ if (child.type === PageState && props.status !== "ready") {
244
+ return {
245
+ content: total.content,
246
+ state: total.state + 1,
247
+ };
248
+ }
249
+ const nested = countPageBodyHeadings(props.children);
250
+ return {
251
+ content:
252
+ total.content +
253
+ (child.type === Content && props.level === 1 ? 1 : 0) +
254
+ nested.content,
255
+ state:
256
+ total.state + nested.state,
257
+ };
258
+ },
259
+ { content: 0, state: 0 },
260
+ );
261
+ }
262
+
263
+ function hasOpaquePageBodyContent(node: ReactNode): boolean {
264
+ return Children.toArray(node).some((child) => {
265
+ if (!isValidElement(child)) return false;
266
+ if (typeof child.type !== "string") {
267
+ return child.type !== Content && child.type !== PageState;
268
+ }
269
+ return hasOpaquePageBodyContent(
270
+ (child.props as { children?: ReactNode }).children,
271
+ );
272
+ });
273
+ }
274
+
220
275
  export function Page({
221
276
  title,
222
277
  actions,
@@ -240,7 +295,9 @@ export function Page({
240
295
  (node) => isValidElement(node) && node.type === PageIntro,
241
296
  );
242
297
  const activeStateCount = useRef(0);
298
+ const headingCount = useRef(0);
243
299
  const [integralState, setIntegralState] = useState(false);
300
+ const [, setHeadingRevision] = useState(0);
244
301
  const registerIntegralState = useCallback(() => {
245
302
  activeStateCount.current += 1;
246
303
  setIntegralState(true);
@@ -252,56 +309,105 @@ export function Page({
252
309
  setIntegralState(activeStateCount.current > 0);
253
310
  };
254
311
  }, []);
312
+ const registerHeading = useCallback(() => {
313
+ headingCount.current += 1;
314
+ setHeadingRevision((revision) => revision + 1);
315
+ let registered = true;
316
+ return () => {
317
+ if (!registered) return;
318
+ registered = false;
319
+ headingCount.current = Math.max(0, headingCount.current - 1);
320
+ setHeadingRevision((revision) => revision + 1);
321
+ };
322
+ }, []);
255
323
  if (
256
324
  shorthand &&
257
- nodes.some((node) => isValidElement(node) && node.type === PageHeader)
325
+ nodes.some(
326
+ (node) =>
327
+ isValidElement(node) &&
328
+ [PageHeader, PageIntro, PageBody, PageFooter].includes(
329
+ node.type as typeof PageHeader,
330
+ ),
331
+ )
258
332
  ) {
259
333
  throw new Error(
260
- "Page não permite misturar propriedades de shorthand com PageHeader explícito.",
334
+ "Page não permite misturar propriedades de shorthand com composição explícita.",
261
335
  );
262
336
  }
263
337
  if (!shorthand) {
264
- const validIntroComposition =
265
- !insideShell &&
266
- intros.length === 1 &&
267
- headers.length === 0 &&
268
- bodies.length === 1 &&
269
- footers.length <= 1 &&
270
- intros.length + bodies.length + footers.length === nodes.length;
271
- const validStandaloneComposition =
272
- !insideShell &&
273
- headers.length === 1 &&
274
- intros.length === 0 &&
275
- bodies.length === 1 &&
276
- footers.length <= 1 &&
277
- headers.length + bodies.length + footers.length === nodes.length;
278
- const validShellComposition =
279
- insideShell &&
280
- headers.length === 1 &&
281
- bodies.length === 1 &&
338
+ const validComposition =
339
+ headers.length <= 1 &&
282
340
  intros.length <= 1 &&
341
+ bodies.length === 1 &&
283
342
  footers.length <= 1 &&
284
343
  headers.length + intros.length + bodies.length + footers.length ===
285
344
  nodes.length;
286
- if (
287
- !validIntroComposition &&
288
- !validStandaloneComposition &&
289
- !validShellComposition
290
- ) {
345
+ if (!validComposition) {
291
346
  throw new Error(
292
- insideShell
293
- ? "Page explícito dentro de PageShell exige um PageHeader, um PageBody e aceita no máximo um PageIntro e um PageFooter como filhos diretos."
294
- : "Page explícito exige um PageHeader ou PageIntro, um PageBody e no máximo um PageFooter como filhos diretos.",
347
+ "Page explícito exige um PageBody e aceita no máximo um PageHeader, um PageIntro e um PageFooter como filhos diretos.",
348
+ );
349
+ }
350
+ const bodyHeadingSources = bodies.reduce<{
351
+ content: number;
352
+ state: number;
353
+ }>(
354
+ (total, body) => {
355
+ const count = countPageBodyHeadings(
356
+ (body as ReactElement<{ children?: ReactNode }>).props.children,
357
+ );
358
+ return {
359
+ content: total.content + count.content,
360
+ state: total.state + count.state,
361
+ };
362
+ },
363
+ { content: 0, state: 0 },
364
+ );
365
+ const structuralHeadingSources =
366
+ headers.reduce<number>(
367
+ (total, header) =>
368
+ total + countDirectChildrenOfType(header as ReactElement, PageTitle),
369
+ 0,
370
+ ) +
371
+ intros.reduce<number>(
372
+ (total, intro) =>
373
+ total + countDirectChildrenOfType(intro as ReactElement, PageTitle),
374
+ 0,
375
+ ) + bodyHeadingSources.content;
376
+ const headingSources =
377
+ bodyHeadingSources.state > 0
378
+ ? bodyHeadingSources.state + bodyHeadingSources.content
379
+ : structuralHeadingSources;
380
+ const hasOpaqueBodyContent = bodies.some((body) =>
381
+ hasOpaquePageBodyContent(
382
+ (body as ReactElement<{ children?: ReactNode }>).props.children,
383
+ ),
384
+ );
385
+ if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
386
+ throw new Error(
387
+ "Page explícito exige exatamente um heading principal em PageHeader, PageIntro, Content level={1} ou PageState ativo dentro de PageBody.",
295
388
  );
296
389
  }
297
390
  }
391
+ useLayoutEffect(() => {
392
+ const hasActiveIntegralState = activeStateCount.current > 0;
393
+ if (integralState !== hasActiveIntegralState) return;
394
+ if (headingCount.current !== 1) {
395
+ throw new Error(
396
+ "Page explícito exige exatamente um heading principal em PageHeader, PageIntro, Content level={1} ou PageState ativo dentro de PageBody.",
397
+ );
398
+ }
399
+ });
298
400
  const content =
299
401
  shorthand && insideShell ? (
300
402
  <>
301
- <PageHeader>
403
+ {actions !== undefined && (
404
+ <PageHeader>
405
+ <PageActions>{actions}</PageActions>
406
+ </PageHeader>
407
+ )}
408
+ <PageIntro>
302
409
  <PageTitle>{title}</PageTitle>
303
- {actions !== undefined && <PageActions>{actions}</PageActions>}
304
- </PageHeader>
410
+ </PageIntro>
305
411
  <PageBody>{children}</PageBody>
306
412
  </>
307
413
  ) : shorthand ? (
@@ -320,33 +426,35 @@ export function Page({
320
426
  <PageContext.Provider value={{}}>
321
427
  <PageIntegralStateContext.Provider value={integralState}>
322
428
  <PageStatePresenceProvider register={registerIntegralState}>
323
- <main
324
- data-slot="page"
325
- className={cn(
326
- "min-w-0 flex-1",
327
- insideShell && "min-h-full",
328
- integralState && "flex min-h-full flex-col",
329
- )}
330
- {...props}
331
- >
332
- <div
429
+ <PageHeadingPresenceProvider register={registerHeading}>
430
+ <main
431
+ data-slot="page"
333
432
  className={cn(
334
- "mx-auto max-w-7xl space-y-6 px-8 py-8",
335
- integralState &&
336
- "flex min-h-full w-full flex-1 flex-col space-y-0",
337
- className,
433
+ "min-w-0 flex-1",
434
+ insideShell && "min-h-full",
435
+ integralState && "flex min-h-full flex-col",
338
436
  )}
437
+ {...props}
339
438
  >
340
- {content}
341
- </div>
342
- </main>
439
+ <div
440
+ className={cn(
441
+ "mx-auto max-w-7xl space-y-6 px-8 py-8",
442
+ integralState &&
443
+ "flex min-h-full w-full flex-1 flex-col space-y-0",
444
+ className,
445
+ )}
446
+ >
447
+ {content}
448
+ </div>
449
+ </main>
450
+ </PageHeadingPresenceProvider>
343
451
  </PageStatePresenceProvider>
344
452
  </PageIntegralStateContext.Provider>
345
453
  </PageContext.Provider>
346
454
  );
347
455
  }
348
456
 
349
- /** Introdução opcional do conteúdo, separada da navegação persistente do shell. */
457
+ /** Título estrutural no conteúdo de PageShell ou introdução opcional de uma Page isolada. */
350
458
  export function PageIntro({
351
459
  className,
352
460
  children,
@@ -358,12 +466,16 @@ export function PageIntro({
358
466
  requireParent(page !== null, "PageIntro", "Page");
359
467
  if (integralState) {
360
468
  if (!insideShell) return <></>;
361
- const actions = Children.toArray(children).filter(
362
- (node) => isValidElement(node) && node.type === PageActions,
469
+ const persistentSlots = Children.toArray(children).filter(
470
+ (node) =>
471
+ isValidElement(node) &&
472
+ [PageBack, PageNavigation, PageActions].includes(
473
+ node.type as typeof PageBack,
474
+ ),
363
475
  );
364
476
  return (
365
477
  <PageHeaderContext.Provider value="intro">
366
- {actions}
478
+ {persistentSlots}
367
479
  </PageHeaderContext.Provider>
368
480
  );
369
481
  }
@@ -374,9 +486,11 @@ export function PageIntro({
374
486
  name="PageIntro"
375
487
  slot="page-intro"
376
488
  slots={{
489
+ leading: [PageBack, PageNavigation],
377
490
  title: PageTitle,
378
491
  actions: PageActions,
379
492
  }}
493
+ leadingPlacement="inline"
380
494
  className={className}
381
495
  {...props}
382
496
  >
@@ -386,7 +500,10 @@ export function PageIntro({
386
500
  );
387
501
  }
388
502
 
389
- /** A anatomia é a de `SurfaceHeader`, a mesma de `ContentHeader`; só os slots mudam de nome. */
503
+ /**
504
+ * Chrome da Page: navegação e ações persistentes. Fora de PageShell também pode
505
+ * carregar o título; dentro dele, os slots são projetados na barra do shell.
506
+ */
390
507
  export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {}
391
508
 
392
509
  export function PageHeader({
@@ -417,7 +534,11 @@ export function PageHeader({
417
534
  actions: PageActions,
418
535
  }}
419
536
  leadingPlacement="inline"
420
- className={className}
537
+ titleRequired={false}
538
+ className={cn(
539
+ "flex h-[3.75rem] items-center border-b p-3 [&>*]:w-full",
540
+ className,
541
+ )}
421
542
  {...props}
422
543
  >
423
544
  {children}
@@ -431,10 +552,9 @@ export function PageTitle({
431
552
  ...props
432
553
  }: HTMLAttributes<HTMLHeadingElement>): ReactElement {
433
554
  const variant = useContext(PageHeaderContext);
434
- const insideShell = useContext(PageShellContext);
435
- const target = useContext(PageTitleTargetContext);
436
555
  requireParent(variant !== null, "PageTitle", "PageHeader ou PageIntro");
437
- const title = (
556
+ usePageHeadingPresence();
557
+ return (
438
558
  <h1
439
559
  data-slot="page-title"
440
560
  className={cn(
@@ -446,10 +566,6 @@ export function PageTitle({
446
566
  {...props}
447
567
  />
448
568
  );
449
- if (insideShell && variant === "header") {
450
- return target === null ? <></> : createPortal(title, target);
451
- }
452
- return title;
453
569
  }
454
570
 
455
571
  export function PageActions({
@@ -495,11 +611,7 @@ export function PageNavigation({
495
611
  const variant = useContext(PageHeaderContext);
496
612
  const insideShell = useContext(PageShellContext);
497
613
  const target = useContext(PageNavigationTargetContext);
498
- requireParent(
499
- variant !== null,
500
- "PageNavigation",
501
- "PageHeader",
502
- );
614
+ requireParent(variant !== null, "PageNavigation", "PageHeader ou PageIntro");
503
615
  const navigation = (
504
616
  <div
505
617
  data-slot="page-navigation-content"
@@ -507,7 +619,7 @@ export function PageNavigation({
507
619
  {...props}
508
620
  />
509
621
  );
510
- if (insideShell && variant === "header") {
622
+ if (insideShell) {
511
623
  return target === null ? <></> : createPortal(navigation, target);
512
624
  }
513
625
  return navigation;
@@ -522,7 +634,7 @@ export function PageBack({
522
634
  const variant = useContext(PageHeaderContext);
523
635
  const insideShell = useContext(PageShellContext);
524
636
  const target = useContext(PageNavigationTargetContext);
525
- requireParent(variant !== null, "PageBack", "PageHeader");
637
+ requireParent(variant !== null, "PageBack", "PageHeader ou PageIntro");
526
638
  const destination =
527
639
  typeof children === "string" ? children : "a página anterior";
528
640
  const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
@@ -540,7 +652,7 @@ export function PageBack({
540
652
  </a>
541
653
  </Button>
542
654
  );
543
- if (insideShell && variant === "header") {
655
+ if (insideShell) {
544
656
  return target === null ? <></> : createPortal(back, target);
545
657
  }
546
658
  return back;
@@ -572,7 +684,7 @@ export function PageFooter({
572
684
  <div
573
685
  data-slot="page-footer"
574
686
  className={cn(
575
- "mt-auto flex shrink-0 items-center justify-end border-t bg-muted/30 px-8 py-4",
687
+ "mt-auto flex h-[3.75rem] shrink-0 items-center justify-end border-t bg-muted/30 p-3",
576
688
  className,
577
689
  )}
578
690
  {...props}