@softize/opus 14.0.0 → 15.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 (32) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +33 -5
  4. package/bin/lib/cli-shared.mjs +30 -1
  5. package/bin/lib/copy.mjs +3 -0
  6. package/bin/lib/db.mjs +2 -0
  7. package/docs/adr/0004-page-content-state-is-composed.md +39 -5
  8. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
  9. package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
  10. package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
  11. package/docs/data-layer.md +9 -0
  12. package/package.json +1 -1
  13. package/registry/skills/build-opus-ui/SKILL.md +3 -2
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
  15. package/src/ui/components/patterns/list.tsx +41 -26
  16. package/src/ui/components/patterns/page-state.tsx +48 -6
  17. package/src/ui/components/patterns/page.tsx +221 -55
  18. package/src/ui/components/patterns/state-surface.tsx +137 -23
  19. package/src/ui/components/patterns/surface-header.tsx +102 -17
  20. package/src/ui/components/patterns/trigger.tsx +7 -6
  21. package/src/ui/components/primitives/button-group.tsx +34 -8
  22. package/src/ui/components/primitives/control.ts +12 -3
  23. package/src/ui/docs/content/action-list-dialog.md +1 -1
  24. package/src/ui/docs/content/action-list.md +34 -3
  25. package/src/ui/docs/content/action-trigger.md +4 -3
  26. package/src/ui/docs/content/alert.md +16 -4
  27. package/src/ui/docs/content/button.md +20 -5
  28. package/src/ui/docs/content/content.md +3 -3
  29. package/src/ui/docs/content/data-state.md +4 -4
  30. package/src/ui/docs/content/page.md +159 -44
  31. package/src/ui/meta.ts +6 -6
  32. package/src/ui/react.tsx +8 -2
@@ -74,6 +74,7 @@ import {
74
74
  PaginationPrevious,
75
75
  } from "../primitives/pagination.tsx";
76
76
  import { Button } from "../primitives/button.tsx";
77
+ import { ButtonGroup } from "../primitives/button-group.tsx";
77
78
  import { Calendar } from "../primitives/calendar.tsx";
78
79
  import { Checkbox } from "../primitives/checkbox.tsx";
79
80
  import {
@@ -262,7 +263,9 @@ export interface ActionFilterBarProps {
262
263
  filterOptions?: Record<string, SelectOption[]>;
263
264
  onRefresh?: () => Promise<void> | void;
264
265
  refreshing?: boolean;
265
- /** Ações de apresentação do consumidor, renderizadas depois do refresh. */
266
+ /** Controles auxiliares, agrupados com o refresh em um ButtonGroup espaçado. */
267
+ controls?: ReactNode;
268
+ /** Ações do consumidor, separadas do grupo de controles. */
266
269
  actions?: ReactNode;
267
270
  }
268
271
 
@@ -293,6 +296,8 @@ export interface ActionListProps<TInput, TItem> {
293
296
  action: ListActionLike;
294
297
  /** Input BASE (escopo fixo, ex.: { workspaceId }) — a toolbar soma por cima. */
295
298
  input: TInput;
299
+ /** Ações do consumidor no fim da barra de filtros, depois dos controles de exibição. */
300
+ toolbarActions?: ReactNode;
296
301
  /** Tabela explícita — sobrepõe as colunas do contrato. */
297
302
  columns?: ActionListColumn<TItem>[];
298
303
  /** Células custom POR CIMA das colunas do contrato (chave = column.key). */
@@ -904,6 +909,7 @@ export function ActionFilterBar({
904
909
  filterOptions,
905
910
  onRefresh,
906
911
  refreshing = false,
912
+ controls,
907
913
  actions,
908
914
  }: ActionFilterBarProps): React.ReactElement {
909
915
  const dicts = useDicts();
@@ -1047,7 +1053,7 @@ export function ActionFilterBar({
1047
1053
  <Tooltip>
1048
1054
  <TooltipTrigger asChild>
1049
1055
  <Button
1050
- variant="outline"
1056
+ variant="ghost"
1051
1057
  size="icon"
1052
1058
  className="shrink-0"
1053
1059
  aria-label="Filtros"
@@ -1061,27 +1067,32 @@ export function ActionFilterBar({
1061
1067
  )}
1062
1068
  <div className="ml-auto flex min-w-0 items-center gap-2">
1063
1069
  {!searchFirst && searchBox}
1064
- {onRefresh !== undefined && (
1065
- <Tooltip>
1066
- <TooltipTrigger asChild>
1067
- <Button
1068
- variant="outline"
1069
- size="icon"
1070
- className="shrink-0"
1071
- aria-label="Recarregar"
1072
- onClick={() => void onRefresh()}
1073
- disabled={refreshing}
1074
- >
1075
- <RefreshCw
1076
- className={cn(
1077
- "h-3.5 w-3.5",
1078
- refreshing && "animate-spin",
1079
- )}
1080
- />
1081
- </Button>
1082
- </TooltipTrigger>
1083
- <TooltipContent>Recarregar</TooltipContent>
1084
- </Tooltip>
1070
+ {(onRefresh !== undefined || controls !== undefined) && (
1071
+ <ButtonGroup mode="spaced">
1072
+ {onRefresh !== undefined && (
1073
+ <Tooltip>
1074
+ <TooltipTrigger asChild>
1075
+ <Button
1076
+ variant="ghost"
1077
+ size="icon"
1078
+ className="shrink-0"
1079
+ aria-label="Recarregar"
1080
+ onClick={() => void onRefresh()}
1081
+ disabled={refreshing}
1082
+ >
1083
+ <RefreshCw
1084
+ className={cn(
1085
+ "h-3.5 w-3.5",
1086
+ refreshing && "animate-spin",
1087
+ )}
1088
+ />
1089
+ </Button>
1090
+ </TooltipTrigger>
1091
+ <TooltipContent>Recarregar</TooltipContent>
1092
+ </Tooltip>
1093
+ )}
1094
+ {controls}
1095
+ </ButtonGroup>
1085
1096
  )}
1086
1097
  {actions}
1087
1098
  </div>
@@ -1134,6 +1145,7 @@ export function ActionFilterBar({
1134
1145
  export function ActionList<TInput, TItem>({
1135
1146
  action,
1136
1147
  input,
1148
+ toolbarActions,
1137
1149
  columns,
1138
1150
  cells,
1139
1151
  filterOptions,
@@ -1394,7 +1406,7 @@ export function ActionList<TInput, TItem>({
1394
1406
  filterOptions={filterOptions}
1395
1407
  onRefresh={() => refetch()}
1396
1408
  refreshing={isLoading}
1397
- actions={
1409
+ controls={
1398
1410
  <>
1399
1411
  {hasSegment && (
1400
1412
  <ToggleGroup
@@ -1433,7 +1445,7 @@ export function ActionList<TInput, TItem>({
1433
1445
  <TooltipTrigger asChild>
1434
1446
  <PopoverTrigger asChild>
1435
1447
  <Button
1436
- variant="outline"
1448
+ variant="ghost"
1437
1449
  size="icon"
1438
1450
  className="shrink-0"
1439
1451
  aria-label="Exibição"
@@ -1517,6 +1529,7 @@ export function ActionList<TInput, TItem>({
1517
1529
  </Popover>
1518
1530
  </>
1519
1531
  }
1532
+ actions={toolbarActions}
1520
1533
  />
1521
1534
  </div>
1522
1535
  ) : null;
@@ -1621,7 +1634,9 @@ export function ActionList<TInput, TItem>({
1621
1634
  className="w-px whitespace-nowrap text-right"
1622
1635
  onClick={(e) => e.stopPropagation()}
1623
1636
  >
1624
- {rowActions(item)}
1637
+ <ButtonGroup mode="spaced" className="ml-auto">
1638
+ {rowActions(item)}
1639
+ </ButtonGroup>
1625
1640
  </TableCell>
1626
1641
  )}
1627
1642
  </TableRow>
@@ -7,7 +7,12 @@
7
7
  * as superfícies são as mesmas (state-surface.tsx), aqui na escala da página.
8
8
  */
9
9
 
10
- import type { ReactNode } from "react";
10
+ import {
11
+ createContext,
12
+ useContext,
13
+ useLayoutEffect,
14
+ type ReactNode,
15
+ } from "react";
11
16
  import { cn } from "../../lib/cn.ts";
12
17
  import {
13
18
  DEFAULT_RETRY_LABEL,
@@ -18,6 +23,23 @@ import {
18
23
 
19
24
  export type PageStateStatus = "loading" | "error" | "empty" | "ready";
20
25
 
26
+ const PageStatePresenceContext = createContext<(() => () => void) | null>(null);
27
+
28
+ /** Integração interna: permite que Page reconheça um PageState mesmo dentro de outro componente. */
29
+ export function PageStatePresenceProvider({
30
+ register,
31
+ children,
32
+ }: {
33
+ register: () => () => void;
34
+ children: ReactNode;
35
+ }): React.ReactElement {
36
+ return (
37
+ <PageStatePresenceContext.Provider value={register}>
38
+ {children}
39
+ </PageStatePresenceContext.Provider>
40
+ );
41
+ }
42
+
21
43
  export interface PageStateProps {
22
44
  status: PageStateStatus;
23
45
  /** Título específico do estado. Defaults seguros existem para loading, error e empty. */
@@ -54,15 +76,31 @@ export function PageState({
54
76
  children,
55
77
  className,
56
78
  }: PageStateProps): React.ReactElement {
79
+ const registerPresence = useContext(PageStatePresenceContext);
80
+ useLayoutEffect(() => {
81
+ if (status === "ready" || registerPresence === null) return;
82
+ return registerPresence();
83
+ }, [registerPresence, status]);
84
+
57
85
  if (status === "ready") return <>{children}</>;
58
86
 
87
+ const stateClassName = cn(
88
+ "flex min-h-64 flex-1 items-center justify-center",
89
+ className,
90
+ );
91
+
59
92
  if (status === "loading") {
60
93
  return (
61
- <div data-slot="page-state" data-status="loading" className={className}>
94
+ <div
95
+ data-slot="page-state"
96
+ data-status="loading"
97
+ className={stateClassName}
98
+ >
62
99
  <LoadingSurface
63
- className="min-h-64"
100
+ className="w-full min-h-64"
64
101
  title={title ?? "Carregando…"}
65
102
  description={description}
103
+ titleLevel={1}
66
104
  />
67
105
  </div>
68
106
  );
@@ -73,10 +111,12 @@ export function PageState({
73
111
  <div
74
112
  data-slot="page-state"
75
113
  data-status="error"
76
- className={cn("flex min-h-64 items-center justify-center", className)}
114
+ className={stateClassName}
77
115
  >
78
116
  <ErrorSurface
79
- className="max-w-xl"
117
+ presentation="state"
118
+ className="w-full max-w-xl"
119
+ titleLevel={1}
80
120
  icon={icon}
81
121
  title={title ?? errorMessage}
82
122
  description={description}
@@ -90,9 +130,11 @@ export function PageState({
90
130
  }
91
131
 
92
132
  return (
93
- <div data-slot="page-state" data-status="empty" className={className}>
133
+ <div data-slot="page-state" data-status="empty" className={stateClassName}>
94
134
  <EmptySurface
135
+ frame="bare"
95
136
  className="min-h-64"
137
+ titleLevel={1}
96
138
  icon={icon}
97
139
  title={title ?? emptyMessage}
98
140
  description={description}
@@ -9,17 +9,38 @@ import {
9
9
  Children,
10
10
  createContext,
11
11
  isValidElement,
12
+ useCallback,
12
13
  useContext,
14
+ useRef,
15
+ useState,
13
16
  type HTMLAttributes,
17
+ type ComponentProps,
14
18
  type ReactElement,
15
19
  type ReactNode,
16
20
  } from "react";
17
21
  import { createPortal } from "react-dom";
22
+ import { ArrowLeft } from "lucide-react";
18
23
  import { cn } from "../../lib/cn.ts";
24
+ import { Button } from "../primitives/button.tsx";
25
+ import {
26
+ Tooltip,
27
+ TooltipContent,
28
+ TooltipProvider,
29
+ TooltipTrigger,
30
+ } from "../primitives/tooltip.tsx";
19
31
  import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
32
+ import { PageStatePresenceProvider } from "./page-state.tsx";
33
+
34
+ export type PageHeaderVariant = "default" | "bar";
35
+
36
+ interface PageContextValue {
37
+ headerVariant: PageHeaderVariant;
38
+ containerClassName?: string;
39
+ }
20
40
 
21
- const PageContext = createContext(false);
22
- const PageHeaderContext = createContext(false);
41
+ const PageContext = createContext<PageContextValue | null>(null);
42
+ const PageHeaderContext = createContext<PageHeaderVariant | null>(null);
43
+ const PageIntegralStateContext = createContext(false);
23
44
  const PageActionsTargetContext = createContext<HTMLElement | null>(null);
24
45
 
25
46
  export function PageActionsTarget({
@@ -54,8 +75,6 @@ interface PageBaseProps extends Omit<
54
75
 
55
76
  interface PageShorthandProps extends PageBaseProps {
56
77
  title: ReactNode;
57
- /** Total de itens ao lado do título. */
58
- count?: number;
59
78
  description?: ReactNode;
60
79
  actions?: ReactNode;
61
80
  children: ReactNode;
@@ -63,7 +82,6 @@ interface PageShorthandProps extends PageBaseProps {
63
82
 
64
83
  interface PageComposedProps extends PageBaseProps {
65
84
  title?: never;
66
- count?: never;
67
85
  description?: never;
68
86
  actions?: never;
69
87
  children: ReactNode;
@@ -73,7 +91,6 @@ export type PageProps = PageShorthandProps | PageComposedProps;
73
91
 
74
92
  export function Page({
75
93
  title,
76
- count,
77
94
  description,
78
95
  actions,
79
96
  className,
@@ -82,6 +99,25 @@ export function Page({
82
99
  }: PageProps): ReactElement {
83
100
  const shorthand = title !== undefined;
84
101
  const nodes = Children.toArray(children);
102
+ const headers = nodes.filter(
103
+ (node) => isValidElement(node) && node.type === PageHeader,
104
+ );
105
+ const bodies = nodes.filter(
106
+ (node) => isValidElement(node) && node.type === PageBody,
107
+ );
108
+ const activeStateCount = useRef(0);
109
+ const [integralState, setIntegralState] = useState(false);
110
+ const registerIntegralState = useCallback(() => {
111
+ activeStateCount.current += 1;
112
+ setIntegralState(true);
113
+ let registered = true;
114
+ return () => {
115
+ if (!registered) return;
116
+ registered = false;
117
+ activeStateCount.current = Math.max(0, activeStateCount.current - 1);
118
+ setIntegralState(activeStateCount.current > 0);
119
+ };
120
+ }, []);
85
121
  if (
86
122
  shorthand &&
87
123
  nodes.some((node) => isValidElement(node) && node.type === PageHeader)
@@ -91,12 +127,6 @@ export function Page({
91
127
  );
92
128
  }
93
129
  if (!shorthand) {
94
- const headers = nodes.filter(
95
- (node) => isValidElement(node) && node.type === PageHeader,
96
- );
97
- const bodies = nodes.filter(
98
- (node) => isValidElement(node) && node.type === PageBody,
99
- );
100
130
  if (
101
131
  headers.length !== 1 ||
102
132
  bodies.length !== 1 ||
@@ -107,49 +137,102 @@ export function Page({
107
137
  );
108
138
  }
109
139
  }
140
+ const headerVariant: PageHeaderVariant = shorthand
141
+ ? "default"
142
+ : ((headers[0] as ReactElement<PageHeaderProps> | undefined)?.props
143
+ .variant ?? "default");
144
+ const pageContext: PageContextValue = {
145
+ headerVariant,
146
+ containerClassName: className,
147
+ };
148
+
149
+ const content = shorthand ? (
150
+ <>
151
+ <PageHeader>
152
+ <PageTitle>{title}</PageTitle>
153
+ {description !== undefined && (
154
+ <PageDescription>{description}</PageDescription>
155
+ )}
156
+ {actions !== undefined && <PageActions>{actions}</PageActions>}
157
+ </PageHeader>
158
+ <PageBody>{children}</PageBody>
159
+ </>
160
+ ) : (
161
+ children
162
+ );
110
163
 
111
164
  return (
112
- <PageContext.Provider value>
113
- <main data-slot="page" className="min-w-0 flex-1" {...props}>
114
- <div className={cn("mx-auto max-w-7xl space-y-6 px-8 py-8", className)}>
115
- {shorthand ? (
116
- <>
117
- <PageHeader>
118
- <PageTitle>{title}</PageTitle>
119
- {count !== undefined && <PageMeta>{count}</PageMeta>}
120
- {description !== undefined && (
121
- <PageDescription>{description}</PageDescription>
165
+ <PageContext.Provider value={pageContext}>
166
+ <PageIntegralStateContext.Provider value={integralState}>
167
+ <PageStatePresenceProvider register={registerIntegralState}>
168
+ <main
169
+ data-slot="page"
170
+ className={cn(
171
+ "min-w-0 flex-1",
172
+ headerVariant === "bar" && "flex min-h-0 flex-col",
173
+ integralState && "flex min-h-full flex-col",
174
+ )}
175
+ {...props}
176
+ >
177
+ {headerVariant === "bar" ? (
178
+ content
179
+ ) : (
180
+ <div
181
+ className={cn(
182
+ "mx-auto max-w-7xl space-y-6 px-8 py-8",
183
+ integralState &&
184
+ "flex min-h-full w-full flex-1 flex-col space-y-0",
185
+ className,
122
186
  )}
123
- {actions !== undefined && <PageActions>{actions}</PageActions>}
124
- </PageHeader>
125
- <PageBody>{children}</PageBody>
126
- </>
127
- ) : (
128
- children
129
- )}
130
- </div>
131
- </main>
187
+ >
188
+ {content}
189
+ </div>
190
+ )}
191
+ </main>
192
+ </PageStatePresenceProvider>
193
+ </PageIntegralStateContext.Provider>
132
194
  </PageContext.Provider>
133
195
  );
134
196
  }
135
197
 
136
198
  /** A anatomia é a de `SurfaceHeader`, a mesma de `ContentHeader`; só os slots mudam de nome. */
199
+ export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {
200
+ /** `default` fica no container; `bar` cria uma faixa compacta no topo da Page. */
201
+ variant?: PageHeaderVariant;
202
+ }
203
+
137
204
  export function PageHeader({
205
+ variant = "default",
206
+ className,
138
207
  children,
139
208
  ...props
140
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
141
- requireParent(useContext(PageContext), "PageHeader", "Page");
209
+ }: PageHeaderProps): ReactElement {
210
+ const page = useContext(PageContext);
211
+ const integralState = useContext(PageIntegralStateContext);
212
+ requireParent(page !== null, "PageHeader", "Page");
213
+ if (integralState) return <></>;
142
214
  return (
143
- <PageHeaderContext.Provider value>
215
+ <PageHeaderContext.Provider value={variant}>
144
216
  <SurfaceHeader
145
217
  name="PageHeader"
146
218
  slot="page"
147
219
  slots={{
220
+ leading: [PageBack, PageNavigation],
148
221
  title: PageTitle,
149
- count: PageMeta,
150
222
  description: PageDescription,
151
223
  actions: PageActions,
152
224
  }}
225
+ leadingPlacement={variant === "bar" ? "inline" : "above"}
226
+ contentClassName={
227
+ variant === "bar"
228
+ ? cn("mx-auto w-full max-w-7xl px-8 py-2", page?.containerClassName)
229
+ : undefined
230
+ }
231
+ className={cn(
232
+ variant === "bar" &&
233
+ "shrink-0 border-b border-border bg-background text-foreground",
234
+ className,
235
+ )}
153
236
  {...props}
154
237
  >
155
238
  {children}
@@ -162,11 +245,17 @@ export function PageTitle({
162
245
  className,
163
246
  ...props
164
247
  }: HTMLAttributes<HTMLHeadingElement>): ReactElement {
165
- requireParent(useContext(PageHeaderContext), "PageTitle", "PageHeader");
248
+ const variant = useContext(PageHeaderContext);
249
+ requireParent(variant !== null, "PageTitle", "PageHeader");
166
250
  return (
167
251
  <h1
168
252
  data-slot="page-title"
169
- className={cn(surfaceHeaderClasses.page.title, className)}
253
+ className={cn(
254
+ variant === "bar"
255
+ ? "truncate text-sm font-semibold"
256
+ : surfaceHeaderClasses.page.title,
257
+ className,
258
+ )}
170
259
  {...props}
171
260
  />
172
261
  );
@@ -176,50 +265,127 @@ export function PageDescription({
176
265
  className,
177
266
  ...props
178
267
  }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
179
- requireParent(useContext(PageHeaderContext), "PageDescription", "PageHeader");
268
+ const variant = useContext(PageHeaderContext);
269
+ requireParent(variant !== null, "PageDescription", "PageHeader");
180
270
  return (
181
271
  <p
182
272
  data-slot="page-description"
183
- className={cn(surfaceHeaderClasses.page.description, className)}
273
+ className={cn(
274
+ variant === "bar"
275
+ ? "mt-0.5 truncate text-xs text-muted-foreground"
276
+ : surfaceHeaderClasses.page.description,
277
+ className,
278
+ )}
184
279
  {...props}
185
280
  />
186
281
  );
187
282
  }
188
283
 
189
- export function PageMeta({
284
+ export function PageActions({
190
285
  className,
191
286
  ...props
192
- }: HTMLAttributes<HTMLSpanElement>): ReactElement {
193
- requireParent(useContext(PageHeaderContext), "PageMeta", "PageHeader");
194
- return (
195
- <span
196
- data-slot="page-meta"
197
- className={cn(surfaceHeaderClasses.page.count, className)}
287
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
288
+ const variant = useContext(PageHeaderContext);
289
+ requireParent(variant !== null, "PageActions", "PageHeader");
290
+ const target = useContext(PageActionsTargetContext);
291
+ const actions = (
292
+ <div
293
+ data-slot="page-actions"
294
+ className={cn(
295
+ variant === "bar"
296
+ ? "flex shrink-0 items-center gap-2"
297
+ : surfaceHeaderClasses.page.actions,
298
+ className,
299
+ )}
198
300
  {...props}
199
301
  />
200
302
  );
303
+ return target === null ? actions : createPortal(actions, target);
201
304
  }
202
305
 
203
- export function PageActions({
306
+ export interface PageBackProps extends Omit<ComponentProps<"a">, "children" | "href"> {
307
+ /** Nome do destino pai. Na barra, também alimenta tooltip e nome acessível. */
308
+ children: ReactNode;
309
+ /** Destino explícito. Obrigatório: sem `href` o elemento não é tabulável, não expõe
310
+ * `role="link"` e o navegador descarta o nome acessível — na barra, onde não há texto
311
+ * visível, o retorno simplesmente desapareceria para tecnologia assistiva. */
312
+ href: string;
313
+ }
314
+
315
+ /** Região introdutória para uma trilha estrutural com mais de um ancestral relevante. */
316
+ export function PageNavigation({
204
317
  className,
205
318
  ...props
206
319
  }: HTMLAttributes<HTMLDivElement>): ReactElement {
207
- requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
208
- const target = useContext(PageActionsTargetContext);
209
- const actions = (
320
+ requireParent(
321
+ useContext(PageHeaderContext) !== null,
322
+ "PageNavigation",
323
+ "PageHeader",
324
+ );
325
+ return (
210
326
  <div
211
- data-slot="page-actions"
212
- className={cn(surfaceHeaderClasses.page.actions, className)}
327
+ data-slot="page-navigation-content"
328
+ className={cn("min-w-0", className)}
213
329
  {...props}
214
330
  />
215
331
  );
216
- return target === null ? actions : createPortal(actions, target);
332
+ }
333
+
334
+ export function PageBack({
335
+ children,
336
+ className,
337
+ "aria-label": ariaLabel,
338
+ ...props
339
+ }: PageBackProps): ReactElement {
340
+ const variant = useContext(PageHeaderContext);
341
+ requireParent(variant !== null, "PageBack", "PageHeader");
342
+ const destination =
343
+ typeof children === "string" ? children : "a página anterior";
344
+ const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
345
+ const link = (
346
+ <Button asChild variant="ghost" size={variant === "bar" ? "icon-sm" : "sm"}>
347
+ <a
348
+ data-slot="page-back"
349
+ aria-label={accessibleLabel}
350
+ className={className}
351
+ {...props}
352
+ >
353
+ <ArrowLeft aria-hidden="true" />
354
+ {variant === "default" && <span>{children}</span>}
355
+ </a>
356
+ </Button>
357
+ );
358
+
359
+ return variant === "bar" ? (
360
+ <TooltipProvider delayDuration={200}>
361
+ <Tooltip>
362
+ <TooltipTrigger asChild>{link}</TooltipTrigger>
363
+ <TooltipContent>{accessibleLabel}</TooltipContent>
364
+ </Tooltip>
365
+ </TooltipProvider>
366
+ ) : (
367
+ link
368
+ );
217
369
  }
218
370
 
219
371
  export function PageBody({
220
372
  className,
221
373
  ...props
222
374
  }: HTMLAttributes<HTMLDivElement>): ReactElement {
223
- requireParent(useContext(PageContext), "PageBody", "Page");
224
- return <div data-slot="page-body" className={className} {...props} />;
375
+ const page = useContext(PageContext);
376
+ const integralState = useContext(PageIntegralStateContext);
377
+ requireParent(page !== null, "PageBody", "Page");
378
+ return (
379
+ <div
380
+ data-slot="page-body"
381
+ className={cn(
382
+ page?.headerVariant === "bar" &&
383
+ "mx-auto w-full max-w-7xl flex-1 px-8 py-8",
384
+ page?.headerVariant === "bar" && page.containerClassName,
385
+ integralState && "flex min-h-0 flex-1 flex-col",
386
+ className,
387
+ )}
388
+ {...props}
389
+ />
390
+ );
225
391
  }