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