@softize/opus 30.0.0 → 31.1.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 +123 -0
- package/bin/lib/check.mjs +109 -101
- package/bin/lib/copy.mjs +2973 -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/0060-surface-composition-separates-chrome-and-intro.md +142 -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/0066-civil-date-input-and-calendar-list.md +28 -0
- package/docs/adr/README.md +6 -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 +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -0
- package/src/ui/components/patterns/form.tsx +22 -1
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list-calendar.tsx +129 -0
- package/src/ui/components/patterns/list.tsx +612 -382
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- 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 +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/calendar.tsx +39 -18
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/date-input.tsx +276 -0
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- 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 +1 -1
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +6 -1
- package/src/ui/docs/content/action-list.md +156 -68
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/calendar-month-view.md +50 -0
- package/src/ui/docs/content/calendar.md +6 -1
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/date-input.md +67 -0
- package/src/ui/docs/content/dialog.md +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/input.md +3 -0
- package/src/ui/docs/content/page.md +204 -412
- 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 +65 -120
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/docs/registry.tsx +38 -9
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +17 -6
- package/src/ui/react.tsx +41 -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,50 +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
|
-
structuralMetaClasses,
|
|
35
|
-
validateSurfaceIntro,
|
|
36
41
|
structuralHeaderClasses,
|
|
42
|
+
structuralMetaClasses,
|
|
37
43
|
} from "./structural-header.tsx";
|
|
38
|
-
import {
|
|
39
|
-
import { FillRoot, useFillRegion } from "./fill-region.ts";
|
|
44
|
+
import { Surface } from "./surface.tsx";
|
|
40
45
|
|
|
41
46
|
import {
|
|
47
|
+
surfaceTitleClasses,
|
|
42
48
|
surfaceHeaderRowClasses,
|
|
43
49
|
surfaceRegionClasses,
|
|
44
|
-
type SurfaceLayout,
|
|
45
50
|
} from "./surface-anatomy.ts";
|
|
46
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
|
+
|
|
47
59
|
interface PageContextValue {
|
|
48
|
-
|
|
60
|
+
layout: "flow" | "fill";
|
|
49
61
|
contentClassName?: string;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
const PageContext = createContext<PageContextValue | null>(null);
|
|
53
65
|
const PageHeaderContext = createContext(false);
|
|
54
|
-
const PageIntroContext = createContext(false);
|
|
55
66
|
const PageBodyContext = createContext(false);
|
|
56
67
|
const PageIntegralStateContext = createContext(false);
|
|
57
68
|
const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
58
69
|
interface PageShellContextValue {
|
|
59
70
|
navigation?: ReactNode;
|
|
71
|
+
width?: PageWidth;
|
|
60
72
|
actions?: ReactNode;
|
|
73
|
+
footer?: ReactNode;
|
|
61
74
|
}
|
|
75
|
+
const PageConsumerContext = createContext<(() => () => void) | null>(null);
|
|
62
76
|
const PageShellContext = createContext<PageShellContextValue | null>(null);
|
|
63
77
|
|
|
64
78
|
function hasRenderableNode(node: ReactNode): boolean {
|
|
@@ -73,17 +87,10 @@ function hasRenderableNode(node: ReactNode): boolean {
|
|
|
73
87
|
return true;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
/** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
|
|
77
|
-
export function useInsidePageShell(): boolean {
|
|
78
|
-
return useContext(PageShellContext) !== null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
90
|
export interface PageShellProps extends Omit<
|
|
82
91
|
HTMLAttributes<HTMLDivElement>,
|
|
83
92
|
"children"
|
|
84
93
|
> {
|
|
85
|
-
/** Navegação contextual conhecida pelo shell, normalmente um Breadcrumb. */
|
|
86
|
-
navigation?: ReactNode;
|
|
87
94
|
/** Ações do shell ou do recurso que antecedem as ações declaradas pela Page. */
|
|
88
95
|
actions?: ReactNode;
|
|
89
96
|
/** Uma Page descendente, diretamente ou através da rota ativa. */
|
|
@@ -92,14 +99,12 @@ export interface PageShellProps extends Omit<
|
|
|
92
99
|
|
|
93
100
|
/** Moldura que coordena contexto do shell com o PageHeader da página ativa. */
|
|
94
101
|
export function PageShell({
|
|
95
|
-
navigation,
|
|
96
102
|
actions,
|
|
97
103
|
children,
|
|
98
104
|
className,
|
|
99
105
|
...props
|
|
100
106
|
}: PageShellProps): ReactElement {
|
|
101
107
|
const shell = {
|
|
102
|
-
...(hasRenderableNode(navigation) ? { navigation } : {}),
|
|
103
108
|
...(hasRenderableNode(actions) ? { actions } : {}),
|
|
104
109
|
};
|
|
105
110
|
return (
|
|
@@ -123,6 +128,60 @@ export function PageShell({
|
|
|
123
128
|
);
|
|
124
129
|
}
|
|
125
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
|
+
|
|
126
185
|
export function PageActionsTarget({
|
|
127
186
|
target,
|
|
128
187
|
children,
|
|
@@ -149,17 +208,17 @@ interface PageBaseProps extends Omit<
|
|
|
149
208
|
HTMLAttributes<HTMLElement>,
|
|
150
209
|
"children" | "title"
|
|
151
210
|
> {
|
|
152
|
-
/** Classes
|
|
211
|
+
/** Classes da coluna. O conteúdo do Body ocupa a largura disponível. */
|
|
153
212
|
className?: string;
|
|
154
|
-
/** Classes do conteúdo
|
|
213
|
+
/** Classes adicionais do conteúdo do body. */
|
|
155
214
|
contentClassName?: string;
|
|
156
215
|
/**
|
|
157
|
-
* `flow`
|
|
216
|
+
* `flow` rola título e corpo na Page; `fill` mantém o header visível e delega às regiões internas.
|
|
158
217
|
* A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Surface` (ADR 0024).
|
|
159
218
|
*/
|
|
160
219
|
layout?: "flow" | "fill";
|
|
161
|
-
/**
|
|
162
|
-
|
|
220
|
+
/** Limita a coluna inteira, mantendo header e body alinhados. */
|
|
221
|
+
width?: PageWidth;
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
interface PageShorthandProps extends PageBaseProps {
|
|
@@ -203,7 +262,6 @@ function countDirectChildrenOfType(
|
|
|
203
262
|
|
|
204
263
|
function countPageBodyHeadings(node: ReactNode): {
|
|
205
264
|
content: number;
|
|
206
|
-
intro: number;
|
|
207
265
|
state: number;
|
|
208
266
|
} {
|
|
209
267
|
return Children.toArray(node).reduce(
|
|
@@ -217,7 +275,6 @@ function countPageBodyHeadings(node: ReactNode): {
|
|
|
217
275
|
if (child.type === PageState && props.status !== "ready") {
|
|
218
276
|
return {
|
|
219
277
|
content: total.content,
|
|
220
|
-
intro: total.intro,
|
|
221
278
|
state: total.state + 1,
|
|
222
279
|
};
|
|
223
280
|
}
|
|
@@ -227,11 +284,10 @@ function countPageBodyHeadings(node: ReactNode): {
|
|
|
227
284
|
total.content +
|
|
228
285
|
(child.type === Surface && props.level === 1 ? 1 : 0) +
|
|
229
286
|
nested.content,
|
|
230
|
-
intro: total.intro + (child.type === PageIntro ? 1 : 0) + nested.intro,
|
|
231
287
|
state: total.state + nested.state,
|
|
232
288
|
};
|
|
233
289
|
},
|
|
234
|
-
{ content: 0,
|
|
290
|
+
{ content: 0, state: 0 },
|
|
235
291
|
);
|
|
236
292
|
}
|
|
237
293
|
|
|
@@ -253,13 +309,14 @@ export function Page({
|
|
|
253
309
|
meta,
|
|
254
310
|
actions,
|
|
255
311
|
layout = "flow",
|
|
256
|
-
|
|
312
|
+
width: widthProp,
|
|
257
313
|
contentClassName,
|
|
258
314
|
className,
|
|
259
315
|
children,
|
|
260
316
|
...props
|
|
261
317
|
}: PageProps): ReactElement {
|
|
262
318
|
const shell = useContext(PageShellContext);
|
|
319
|
+
const width = widthProp ?? shell?.width ?? "wide";
|
|
263
320
|
const shorthand = title !== undefined;
|
|
264
321
|
const nodes = flattenPageHeaderChildren(children);
|
|
265
322
|
const headers = nodes.filter(
|
|
@@ -271,6 +328,8 @@ export function Page({
|
|
|
271
328
|
const footers = nodes.filter(
|
|
272
329
|
(node) => isValidElement(node) && node.type === PageFooter,
|
|
273
330
|
);
|
|
331
|
+
const registerConsumer = useContext(PageConsumerContext);
|
|
332
|
+
useLayoutEffect(() => registerConsumer?.(), [registerConsumer]);
|
|
274
333
|
const activeStateCount = useRef(0);
|
|
275
334
|
const headingCount = useRef(0);
|
|
276
335
|
const [integralState, setIntegralState] = useState(false);
|
|
@@ -323,7 +382,6 @@ export function Page({
|
|
|
323
382
|
);
|
|
324
383
|
}
|
|
325
384
|
const bodyHeadingSources = bodies.reduce<{
|
|
326
|
-
intro: number;
|
|
327
385
|
content: number;
|
|
328
386
|
state: number;
|
|
329
387
|
}>(
|
|
@@ -333,20 +391,21 @@ export function Page({
|
|
|
333
391
|
);
|
|
334
392
|
return {
|
|
335
393
|
content: total.content + count.content,
|
|
336
|
-
intro: total.intro + count.intro,
|
|
337
394
|
state: total.state + count.state,
|
|
338
395
|
};
|
|
339
396
|
},
|
|
340
|
-
{ content: 0,
|
|
397
|
+
{ content: 0, state: 0 },
|
|
341
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
|
+
);
|
|
342
403
|
const structuralHeadingSources =
|
|
343
404
|
headers.reduce<number>(
|
|
344
405
|
(total, header) =>
|
|
345
406
|
total + countDirectChildrenOfType(header as ReactElement, PageTitle),
|
|
346
407
|
0,
|
|
347
|
-
) +
|
|
348
|
-
bodyHeadingSources.content +
|
|
349
|
-
bodyHeadingSources.intro;
|
|
408
|
+
) + bodyHeadingSources.content;
|
|
350
409
|
const headingSources =
|
|
351
410
|
bodyHeadingSources.state > 0
|
|
352
411
|
? bodyHeadingSources.state + bodyHeadingSources.content
|
|
@@ -358,7 +417,7 @@ export function Page({
|
|
|
358
417
|
);
|
|
359
418
|
if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
|
|
360
419
|
throw new Error(
|
|
361
|
-
"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.",
|
|
362
421
|
);
|
|
363
422
|
}
|
|
364
423
|
}
|
|
@@ -367,53 +426,64 @@ export function Page({
|
|
|
367
426
|
if (integralState !== hasActiveIntegralState) return;
|
|
368
427
|
if (headingCount.current !== 1) {
|
|
369
428
|
throw new Error(
|
|
370
|
-
"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.",
|
|
371
430
|
);
|
|
372
431
|
}
|
|
373
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
|
+
|
|
374
439
|
const content = shorthand ? (
|
|
375
440
|
<>
|
|
376
441
|
<PageHeader>
|
|
442
|
+
<PageTitle>{title}</PageTitle>
|
|
443
|
+
{description !== undefined && (
|
|
444
|
+
<PageDescription>{description}</PageDescription>
|
|
445
|
+
)}
|
|
446
|
+
{meta !== undefined && <PageMeta>{meta}</PageMeta>}
|
|
377
447
|
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
378
448
|
</PageHeader>
|
|
379
|
-
<PageBody>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<PageDescription>{description}</PageDescription>
|
|
384
|
-
)}
|
|
385
|
-
{meta !== undefined && <PageMeta>{meta}</PageMeta>}
|
|
386
|
-
</PageIntro>
|
|
387
|
-
{children}
|
|
388
|
-
</PageBody>
|
|
449
|
+
<PageBody>{children}</PageBody>
|
|
450
|
+
{shell?.footer === undefined ? null : (
|
|
451
|
+
<PageFooter>{shell.footer}</PageFooter>
|
|
452
|
+
)}
|
|
389
453
|
</>
|
|
390
454
|
) : (
|
|
391
455
|
<>
|
|
392
|
-
{headers.length === 0 &&
|
|
393
|
-
(shell?.navigation !== undefined || shell?.actions !== undefined) ? (
|
|
394
|
-
<PageHeader />
|
|
395
|
-
) : null}
|
|
396
456
|
{children}
|
|
457
|
+
{shell?.footer === undefined || footers.length > 0 ? null : (
|
|
458
|
+
<PageFooter>{shell.footer}</PageFooter>
|
|
459
|
+
)}
|
|
397
460
|
</>
|
|
398
461
|
);
|
|
399
462
|
|
|
400
463
|
return (
|
|
401
|
-
<PageContext.Provider value={{
|
|
464
|
+
<PageContext.Provider value={{ contentClassName, layout }}>
|
|
402
465
|
<PageIntegralStateContext.Provider value={integralState}>
|
|
403
466
|
<PageStatePresenceProvider register={registerIntegralState}>
|
|
404
467
|
<PageHeadingPresenceProvider register={registerHeading}>
|
|
405
468
|
<main
|
|
406
469
|
data-slot="page"
|
|
407
470
|
data-layout={layout}
|
|
408
|
-
data-
|
|
471
|
+
data-width={width}
|
|
409
472
|
className={cn(
|
|
410
|
-
"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",
|
|
411
475
|
)}
|
|
412
476
|
{...props}
|
|
413
477
|
>
|
|
414
478
|
<div
|
|
479
|
+
data-slot="page-column"
|
|
415
480
|
className={cn(
|
|
416
|
-
"
|
|
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",
|
|
417
487
|
className,
|
|
418
488
|
)}
|
|
419
489
|
>
|
|
@@ -428,7 +498,10 @@ export function Page({
|
|
|
428
498
|
}
|
|
429
499
|
|
|
430
500
|
/** Cabeçalho único da Page: navegação contextual, título e ações. */
|
|
431
|
-
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
|
+
}
|
|
432
505
|
|
|
433
506
|
export function PageHeader({
|
|
434
507
|
className,
|
|
@@ -442,39 +515,40 @@ export function PageHeader({
|
|
|
442
515
|
if (integralState) return <></>;
|
|
443
516
|
|
|
444
517
|
const nodes = flattenPageHeaderChildren(children);
|
|
445
|
-
const localNavigation = nodes.filter(
|
|
446
|
-
(node) =>
|
|
447
|
-
isValidElement(node) &&
|
|
448
|
-
node.type === PageNavigation &&
|
|
449
|
-
hasRenderableNode((node.props as { children?: ReactNode }).children),
|
|
450
|
-
);
|
|
451
518
|
const localActions = nodes.filter(
|
|
452
519
|
(node) =>
|
|
453
520
|
isValidElement(node) &&
|
|
454
521
|
node.type === PageActions &&
|
|
455
522
|
hasRenderableNode((node.props as { children?: ReactNode }).children),
|
|
456
523
|
) as ReactElement<HTMLAttributes<HTMLDivElement>>[];
|
|
457
|
-
if (shell?.navigation !== undefined && localNavigation.length > 0) {
|
|
458
|
-
throw new Error(
|
|
459
|
-
"PageHeader não combina a navegação de PageShell com PageNavigation local.",
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
524
|
if (localActions.length > 1) {
|
|
463
525
|
throw new Error("PageHeader aceita no máximo um PageActions.");
|
|
464
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
|
+
);
|
|
465
534
|
const structuralChildren = nodes.filter(
|
|
466
535
|
(node) =>
|
|
467
536
|
!(
|
|
468
537
|
isValidElement(node) &&
|
|
469
|
-
[
|
|
470
|
-
node.type as typeof PageNavigation,
|
|
471
|
-
)
|
|
538
|
+
[PageBack, PageActions].includes(node.type as typeof PageActions)
|
|
472
539
|
),
|
|
473
540
|
);
|
|
474
541
|
const pageActions = localActions[0];
|
|
475
542
|
const mergedActions =
|
|
476
|
-
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 : (
|
|
477
549
|
<PageActions {...pageActions?.props}>
|
|
550
|
+
{nodes.filter((node) => isValidElement(node) && node.type === PageBack)}
|
|
551
|
+
{backs.length === 0 ? shell?.navigation : null}
|
|
478
552
|
{shell?.actions}
|
|
479
553
|
{pageActions?.props.children}
|
|
480
554
|
</PageActions>
|
|
@@ -482,8 +556,7 @@ export function PageHeader({
|
|
|
482
556
|
if (
|
|
483
557
|
structuralChildren.length === 0 &&
|
|
484
558
|
mergedActions === null &&
|
|
485
|
-
|
|
486
|
-
shell?.navigation === undefined
|
|
559
|
+
!hasRenderableNode(props.tabs)
|
|
487
560
|
) {
|
|
488
561
|
return <></>;
|
|
489
562
|
}
|
|
@@ -493,38 +566,23 @@ export function PageHeader({
|
|
|
493
566
|
name="PageHeader"
|
|
494
567
|
slot="page"
|
|
495
568
|
slots={{
|
|
496
|
-
leading: [PageBack, PageNavigation],
|
|
497
569
|
title: PageTitle,
|
|
498
570
|
description: PageDescription,
|
|
499
571
|
meta: PageMeta,
|
|
500
572
|
actions: PageActions,
|
|
501
573
|
}}
|
|
502
|
-
leadingPlacement=
|
|
503
|
-
|
|
504
|
-
(node) => isValidElement(node) && node.type === PageTitle,
|
|
505
|
-
) ||
|
|
506
|
-
(localNavigation.length === 0 &&
|
|
507
|
-
shell?.navigation === undefined &&
|
|
508
|
-
!nodes.some(
|
|
509
|
-
(node) => isValidElement(node) && node.type === PageBack,
|
|
510
|
-
))
|
|
511
|
-
? "above"
|
|
512
|
-
: "inline"
|
|
513
|
-
}
|
|
514
|
-
titleRequired={false}
|
|
574
|
+
leadingPlacement="inline"
|
|
575
|
+
titleRequired
|
|
515
576
|
rowClassName={surfaceHeaderRowClasses("page")}
|
|
516
577
|
className={cn(
|
|
517
|
-
"shrink-0",
|
|
518
|
-
surfaceRegionClasses("page",
|
|
578
|
+
"mb-2 shrink-0",
|
|
579
|
+
surfaceRegionClasses("page", "header", {
|
|
580
|
+
bounded: false,
|
|
581
|
+
}),
|
|
519
582
|
className,
|
|
520
583
|
)}
|
|
521
584
|
{...props}
|
|
522
585
|
>
|
|
523
|
-
{shell?.navigation === undefined ? (
|
|
524
|
-
localNavigation
|
|
525
|
-
) : (
|
|
526
|
-
<PageNavigation>{shell.navigation}</PageNavigation>
|
|
527
|
-
)}
|
|
528
586
|
{structuralChildren}
|
|
529
587
|
{mergedActions}
|
|
530
588
|
</StructuralHeader>
|
|
@@ -532,47 +590,14 @@ export function PageHeader({
|
|
|
532
590
|
);
|
|
533
591
|
}
|
|
534
592
|
|
|
535
|
-
/** Primeiro bloco do corpo, separado dos controles do cabeçalho. */
|
|
536
|
-
export function PageIntro({
|
|
537
|
-
className,
|
|
538
|
-
children,
|
|
539
|
-
...props
|
|
540
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
541
|
-
requireParent(useContext(PageBodyContext), "PageIntro", "PageBody");
|
|
542
|
-
const integralState = useContext(PageIntegralStateContext);
|
|
543
|
-
if (integralState) return <></>;
|
|
544
|
-
return (
|
|
545
|
-
<PageIntroContext.Provider value>
|
|
546
|
-
<StructuralHeader
|
|
547
|
-
name="PageIntro"
|
|
548
|
-
slot="page-intro"
|
|
549
|
-
slots={{
|
|
550
|
-
title: PageTitle,
|
|
551
|
-
description: PageDescription,
|
|
552
|
-
meta: PageMeta,
|
|
553
|
-
actions: PageIntroActions,
|
|
554
|
-
}}
|
|
555
|
-
leadingPlacement="inline"
|
|
556
|
-
titleRequired
|
|
557
|
-
className={className}
|
|
558
|
-
{...props}
|
|
559
|
-
>
|
|
560
|
-
{children}
|
|
561
|
-
</StructuralHeader>
|
|
562
|
-
</PageIntroContext.Provider>
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
593
|
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
567
594
|
export function PageMeta({
|
|
568
595
|
className,
|
|
569
596
|
...props
|
|
570
597
|
}: HTMLAttributes<HTMLDivElement>) {
|
|
571
598
|
const header = useContext(PageHeaderContext);
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
if (!inside)
|
|
575
|
-
throw new Error("PageMeta deve estar dentro de PageHeader ou PageIntro.");
|
|
599
|
+
const inside = header;
|
|
600
|
+
if (!inside) throw new Error("PageMeta deve estar dentro de PageHeader.");
|
|
576
601
|
return (
|
|
577
602
|
<div
|
|
578
603
|
data-slot="page-meta"
|
|
@@ -582,37 +607,17 @@ export function PageMeta({
|
|
|
582
607
|
);
|
|
583
608
|
}
|
|
584
609
|
|
|
585
|
-
/** Ações locais do conteúdo, distintas das ações da superfície em PageActions. */
|
|
586
|
-
export function PageIntroActions({
|
|
587
|
-
className,
|
|
588
|
-
...props
|
|
589
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
590
|
-
requireParent(useContext(PageIntroContext), "PageIntroActions", "PageIntro");
|
|
591
|
-
return (
|
|
592
|
-
<div
|
|
593
|
-
data-slot="page-intro-actions"
|
|
594
|
-
className={cn(structuralHeaderClasses.page.actions, className)}
|
|
595
|
-
{...props}
|
|
596
|
-
/>
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
610
|
export function PageTitle({
|
|
601
611
|
className,
|
|
602
612
|
...props
|
|
603
613
|
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
604
614
|
const insideHeader = useContext(PageHeaderContext);
|
|
605
|
-
|
|
606
|
-
requireParent(
|
|
607
|
-
insideHeader || insideIntro,
|
|
608
|
-
"PageTitle",
|
|
609
|
-
"PageHeader ou PageIntro",
|
|
610
|
-
);
|
|
615
|
+
requireParent(insideHeader, "PageTitle", "PageHeader");
|
|
611
616
|
usePageHeadingPresence();
|
|
612
617
|
return (
|
|
613
618
|
<h1
|
|
614
619
|
data-slot="page-title"
|
|
615
|
-
className={cn(
|
|
620
|
+
className={cn(surfaceTitleClasses("page"), "text-3xl", className)}
|
|
616
621
|
{...props}
|
|
617
622
|
/>
|
|
618
623
|
);
|
|
@@ -624,8 +629,7 @@ export function PageDescription({
|
|
|
624
629
|
...props
|
|
625
630
|
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
626
631
|
const header = useContext(PageHeaderContext);
|
|
627
|
-
|
|
628
|
-
requireParent(header || intro, "PageDescription", "PageHeader ou PageIntro");
|
|
632
|
+
requireParent(header, "PageDescription", "PageHeader");
|
|
629
633
|
return (
|
|
630
634
|
<p
|
|
631
635
|
data-slot="page-description"
|
|
@@ -643,41 +647,29 @@ export function PageActions({
|
|
|
643
647
|
requireParent(insideHeader, "PageActions", "PageHeader");
|
|
644
648
|
const target = useContext(PageActionsTargetContext);
|
|
645
649
|
const actions = (
|
|
646
|
-
<
|
|
650
|
+
<ButtonGroup
|
|
651
|
+
mode="spaced"
|
|
647
652
|
data-slot="page-actions"
|
|
648
|
-
className={cn(
|
|
653
|
+
className={cn("w-auto shrink-0 items-center", className)}
|
|
649
654
|
{...props}
|
|
650
655
|
/>
|
|
651
656
|
);
|
|
652
657
|
return target === null ? actions : createPortal(actions, target);
|
|
653
658
|
}
|
|
654
659
|
|
|
655
|
-
export
|
|
656
|
-
ComponentProps<"a">,
|
|
657
|
-
"children" | "href"
|
|
658
|
-
> {
|
|
660
|
+
export type PageBackProps = {
|
|
659
661
|
/** Nome visível do destino pai e base do nome acessível. */
|
|
660
662
|
children: ReactNode;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const insideHeader = useContext(PageHeaderContext);
|
|
672
|
-
requireParent(insideHeader, "PageNavigation", "PageHeader");
|
|
673
|
-
return (
|
|
674
|
-
<div
|
|
675
|
-
data-slot="page-navigation-content"
|
|
676
|
-
className={cn("min-w-0", className)}
|
|
677
|
-
{...props}
|
|
678
|
-
/>
|
|
679
|
-
);
|
|
680
|
-
}
|
|
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
|
+
);
|
|
681
673
|
|
|
682
674
|
export function PageBack({
|
|
683
675
|
children,
|
|
@@ -690,42 +682,73 @@ export function PageBack({
|
|
|
690
682
|
const destination =
|
|
691
683
|
typeof children === "string" ? children : "a página anterior";
|
|
692
684
|
const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
<
|
|
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"
|
|
696
697
|
data-slot="page-back"
|
|
697
698
|
aria-label={accessibleLabel}
|
|
698
|
-
title={accessibleLabel}
|
|
699
699
|
className={className}
|
|
700
|
-
{
|
|
700
|
+
disabled={props.disabled}
|
|
701
|
+
onClick={props.onBack}
|
|
701
702
|
>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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>
|
|
706
722
|
);
|
|
707
|
-
return back;
|
|
708
723
|
}
|
|
709
724
|
|
|
710
725
|
export function PageBody({
|
|
726
|
+
flush = false,
|
|
711
727
|
className,
|
|
712
728
|
contentClassName,
|
|
713
729
|
children,
|
|
714
730
|
...props
|
|
715
731
|
}: HTMLAttributes<HTMLDivElement> & {
|
|
716
732
|
contentClassName?: string;
|
|
733
|
+
/** Canvas e outras regiões que governam o próprio recuo. */
|
|
734
|
+
flush?: boolean;
|
|
717
735
|
}): ReactElement {
|
|
718
736
|
const page = useContext(PageContext);
|
|
719
737
|
const fill = useFillRegion();
|
|
738
|
+
const integralState = useContext(PageIntegralStateContext);
|
|
720
739
|
requireParent(page !== null, "PageBody", "Page");
|
|
721
|
-
validateSurfaceIntro(children, PageIntro, "PageBody");
|
|
722
740
|
return (
|
|
723
741
|
<div
|
|
724
742
|
data-slot="page-body"
|
|
725
743
|
{...fill.attributes}
|
|
726
744
|
className={cn(
|
|
727
|
-
"flex
|
|
728
|
-
|
|
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
|
+
],
|
|
729
752
|
className,
|
|
730
753
|
)}
|
|
731
754
|
{...props}
|
|
@@ -735,9 +758,14 @@ export function PageBody({
|
|
|
735
758
|
data-slot="page-body-content"
|
|
736
759
|
{...fill.attributes}
|
|
737
760
|
className={cn(
|
|
738
|
-
"
|
|
739
|
-
surfaceRegionClasses("page",
|
|
740
|
-
|
|
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",
|
|
741
769
|
page!.contentClassName,
|
|
742
770
|
contentClassName,
|
|
743
771
|
)}
|
|
@@ -760,7 +788,9 @@ export function PageFooter({
|
|
|
760
788
|
data-slot="page-footer"
|
|
761
789
|
className={cn(
|
|
762
790
|
"mt-auto flex min-h-[3.75rem] shrink-0 items-center justify-end",
|
|
763
|
-
surfaceRegionClasses("page",
|
|
791
|
+
surfaceRegionClasses("page", "footer", {
|
|
792
|
+
bounded: false,
|
|
793
|
+
}),
|
|
764
794
|
className,
|
|
765
795
|
)}
|
|
766
796
|
{...props}
|