@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.
- package/CHANGELOG.md +137 -0
- package/bin/lib/check.mjs +178 -94
- package/bin/lib/copy.mjs +2970 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/README.md +8 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
- package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -1
- package/src/ui/components/patterns/form.tsx +2 -0
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list.tsx +588 -365
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +238 -181
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +47 -34
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +55 -37
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +59 -89
- package/src/ui/components/primitives/drawer.tsx +52 -87
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +3 -3
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +2 -1
- package/src/ui/docs/content/action-list.md +146 -73
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/dialog.md +72 -101
- package/src/ui/docs/content/drawer.md +69 -92
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +207 -386
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +94 -116
- package/src/ui/docs/content/table.md +7 -0
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +38 -8
- 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.
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
211
|
+
/** Classes da coluna. O conteúdo do Body ocupa a largura disponível. */
|
|
152
212
|
className?: string;
|
|
153
|
-
/** Classes do conteúdo
|
|
213
|
+
/** Classes adicionais do conteúdo do body. */
|
|
154
214
|
contentClassName?: string;
|
|
155
215
|
/**
|
|
156
|
-
* `flow`
|
|
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
|
-
/**
|
|
161
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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={{
|
|
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-
|
|
471
|
+
data-width={width}
|
|
403
472
|
className={cn(
|
|
404
|
-
"flex h-full min-h-0 min-w-[64rem] flex-1 flex-col
|
|
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
|
-
"
|
|
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
|
-
[
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
/**
|
|
529
|
-
export function
|
|
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>)
|
|
563
|
-
|
|
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-
|
|
567
|
-
className={cn(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
<
|
|
650
|
+
<ButtonGroup
|
|
651
|
+
mode="spaced"
|
|
620
652
|
data-slot="page-actions"
|
|
621
|
-
className={cn(
|
|
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
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
667
|
-
|
|
668
|
-
<
|
|
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
|
-
{
|
|
700
|
+
disabled={props.disabled}
|
|
701
|
+
onClick={props.onBack}
|
|
674
702
|
>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
|
701
|
-
|
|
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
|
-
"
|
|
712
|
-
surfaceRegionClasses("page",
|
|
713
|
-
|
|
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",
|
|
791
|
+
surfaceRegionClasses("page", "footer", {
|
|
792
|
+
bounded: false,
|
|
793
|
+
}),
|
|
737
794
|
className,
|
|
738
795
|
)}
|
|
739
796
|
{...props}
|