@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.
- package/CHANGELOG.md +63 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +33 -5
- package/bin/lib/cli-shared.mjs +30 -1
- package/bin/lib/copy.mjs +3 -0
- package/bin/lib/db.mjs +2 -0
- package/docs/adr/0004-page-content-state-is-composed.md +39 -5
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
- package/docs/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/src/ui/components/patterns/list.tsx +41 -26
- package/src/ui/components/patterns/page-state.tsx +48 -6
- package/src/ui/components/patterns/page.tsx +221 -55
- package/src/ui/components/patterns/state-surface.tsx +137 -23
- package/src/ui/components/patterns/surface-header.tsx +102 -17
- package/src/ui/components/patterns/trigger.tsx +7 -6
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/control.ts +12 -3
- package/src/ui/docs/content/action-list-dialog.md +1 -1
- package/src/ui/docs/content/action-list.md +34 -3
- package/src/ui/docs/content/action-trigger.md +4 -3
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/button.md +20 -5
- package/src/ui/docs/content/content.md +3 -3
- package/src/ui/docs/content/data-state.md +4 -4
- package/src/ui/docs/content/page.md +159 -44
- package/src/ui/meta.ts +6 -6
- 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
|
-
/**
|
|
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="
|
|
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
|
-
<
|
|
1066
|
-
|
|
1067
|
-
<
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
|
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
|
|
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={
|
|
114
|
+
className={stateClassName}
|
|
77
115
|
>
|
|
78
116
|
<ErrorSurface
|
|
79
|
-
|
|
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={
|
|
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(
|
|
22
|
-
const PageHeaderContext = createContext(
|
|
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
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
}:
|
|
141
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
284
|
+
export function PageActions({
|
|
190
285
|
className,
|
|
191
286
|
...props
|
|
192
|
-
}: HTMLAttributes<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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(
|
|
208
|
-
|
|
209
|
-
|
|
320
|
+
requireParent(
|
|
321
|
+
useContext(PageHeaderContext) !== null,
|
|
322
|
+
"PageNavigation",
|
|
323
|
+
"PageHeader",
|
|
324
|
+
);
|
|
325
|
+
return (
|
|
210
326
|
<div
|
|
211
|
-
data-slot="page-
|
|
212
|
-
className={cn(
|
|
327
|
+
data-slot="page-navigation-content"
|
|
328
|
+
className={cn("min-w-0", className)}
|
|
213
329
|
{...props}
|
|
214
330
|
/>
|
|
215
331
|
);
|
|
216
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
}
|