@softize/opus 17.2.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 +55 -1
- package/bin/lib/check.mjs +212 -45
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +4 -0
- 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 +30 -20
- package/registry/skills/build-opus-ui/references/evaluations.md +9 -3
- package/registry/skills/build-opus-ui/references/ui-patterns.md +29 -17
- 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 +164 -50
- package/src/ui/components/patterns/presentation.tsx +140 -84
- 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 +43 -50
- 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
|
|
|
@@ -36,8 +42,8 @@ const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
|
36
42
|
const PageNavigationTargetContext = createContext<HTMLElement | null>(null);
|
|
37
43
|
const PageShellContext = createContext(false);
|
|
38
44
|
const pageBarClassName =
|
|
39
|
-
"flex h-
|
|
40
|
-
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";
|
|
41
47
|
|
|
42
48
|
/** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
|
|
43
49
|
export function useInsidePageShell(): boolean {
|
|
@@ -213,6 +219,59 @@ interface PageComposedProps extends PageBaseProps {
|
|
|
213
219
|
|
|
214
220
|
export type PageProps = PageShorthandProps | PageComposedProps;
|
|
215
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
|
+
|
|
216
275
|
export function Page({
|
|
217
276
|
title,
|
|
218
277
|
actions,
|
|
@@ -236,7 +295,9 @@ export function Page({
|
|
|
236
295
|
(node) => isValidElement(node) && node.type === PageIntro,
|
|
237
296
|
);
|
|
238
297
|
const activeStateCount = useRef(0);
|
|
298
|
+
const headingCount = useRef(0);
|
|
239
299
|
const [integralState, setIntegralState] = useState(false);
|
|
300
|
+
const [, setHeadingRevision] = useState(0);
|
|
240
301
|
const registerIntegralState = useCallback(() => {
|
|
241
302
|
activeStateCount.current += 1;
|
|
242
303
|
setIntegralState(true);
|
|
@@ -248,6 +309,17 @@ export function Page({
|
|
|
248
309
|
setIntegralState(activeStateCount.current > 0);
|
|
249
310
|
};
|
|
250
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
|
+
}, []);
|
|
251
323
|
if (
|
|
252
324
|
shorthand &&
|
|
253
325
|
nodes.some(
|
|
@@ -263,46 +335,78 @@ export function Page({
|
|
|
263
335
|
);
|
|
264
336
|
}
|
|
265
337
|
if (!shorthand) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
intros.length
|
|
269
|
-
headers.length === 0 &&
|
|
270
|
-
bodies.length === 1 &&
|
|
271
|
-
footers.length <= 1 &&
|
|
272
|
-
intros.length + bodies.length + footers.length === nodes.length;
|
|
273
|
-
const validStandaloneComposition =
|
|
274
|
-
!insideShell &&
|
|
275
|
-
headers.length === 1 &&
|
|
276
|
-
intros.length === 0 &&
|
|
277
|
-
bodies.length === 1 &&
|
|
278
|
-
footers.length <= 1 &&
|
|
279
|
-
headers.length + bodies.length + footers.length === nodes.length;
|
|
280
|
-
const validShellComposition =
|
|
281
|
-
insideShell &&
|
|
282
|
-
headers.length === 0 &&
|
|
338
|
+
const validComposition =
|
|
339
|
+
headers.length <= 1 &&
|
|
340
|
+
intros.length <= 1 &&
|
|
283
341
|
bodies.length === 1 &&
|
|
284
|
-
intros.length === 1 &&
|
|
285
342
|
footers.length <= 1 &&
|
|
286
343
|
headers.length + intros.length + bodies.length + footers.length ===
|
|
287
344
|
nodes.length;
|
|
288
|
-
if (
|
|
289
|
-
!validIntroComposition &&
|
|
290
|
-
!validStandaloneComposition &&
|
|
291
|
-
!validShellComposition
|
|
292
|
-
) {
|
|
345
|
+
if (!validComposition) {
|
|
293
346
|
throw new Error(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.",
|
|
297
388
|
);
|
|
298
389
|
}
|
|
299
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
|
+
});
|
|
300
400
|
const content =
|
|
301
401
|
shorthand && insideShell ? (
|
|
302
402
|
<>
|
|
403
|
+
{actions !== undefined && (
|
|
404
|
+
<PageHeader>
|
|
405
|
+
<PageActions>{actions}</PageActions>
|
|
406
|
+
</PageHeader>
|
|
407
|
+
)}
|
|
303
408
|
<PageIntro>
|
|
304
409
|
<PageTitle>{title}</PageTitle>
|
|
305
|
-
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
306
410
|
</PageIntro>
|
|
307
411
|
<PageBody>{children}</PageBody>
|
|
308
412
|
</>
|
|
@@ -322,26 +426,28 @@ export function Page({
|
|
|
322
426
|
<PageContext.Provider value={{}}>
|
|
323
427
|
<PageIntegralStateContext.Provider value={integralState}>
|
|
324
428
|
<PageStatePresenceProvider register={registerIntegralState}>
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"min-w-0 flex-1",
|
|
329
|
-
insideShell && "min-h-full",
|
|
330
|
-
integralState && "flex min-h-full flex-col",
|
|
331
|
-
)}
|
|
332
|
-
{...props}
|
|
333
|
-
>
|
|
334
|
-
<div
|
|
429
|
+
<PageHeadingPresenceProvider register={registerHeading}>
|
|
430
|
+
<main
|
|
431
|
+
data-slot="page"
|
|
335
432
|
className={cn(
|
|
336
|
-
"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
className,
|
|
433
|
+
"min-w-0 flex-1",
|
|
434
|
+
insideShell && "min-h-full",
|
|
435
|
+
integralState && "flex min-h-full flex-col",
|
|
340
436
|
)}
|
|
437
|
+
{...props}
|
|
341
438
|
>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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>
|
|
345
451
|
</PageStatePresenceProvider>
|
|
346
452
|
</PageIntegralStateContext.Provider>
|
|
347
453
|
</PageContext.Provider>
|
|
@@ -394,7 +500,10 @@ export function PageIntro({
|
|
|
394
500
|
);
|
|
395
501
|
}
|
|
396
502
|
|
|
397
|
-
/**
|
|
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
|
+
*/
|
|
398
507
|
export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {}
|
|
399
508
|
|
|
400
509
|
export function PageHeader({
|
|
@@ -425,7 +534,11 @@ export function PageHeader({
|
|
|
425
534
|
actions: PageActions,
|
|
426
535
|
}}
|
|
427
536
|
leadingPlacement="inline"
|
|
428
|
-
|
|
537
|
+
titleRequired={false}
|
|
538
|
+
className={cn(
|
|
539
|
+
"flex h-[3.75rem] items-center border-b p-3 [&>*]:w-full",
|
|
540
|
+
className,
|
|
541
|
+
)}
|
|
429
542
|
{...props}
|
|
430
543
|
>
|
|
431
544
|
{children}
|
|
@@ -440,6 +553,7 @@ export function PageTitle({
|
|
|
440
553
|
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
441
554
|
const variant = useContext(PageHeaderContext);
|
|
442
555
|
requireParent(variant !== null, "PageTitle", "PageHeader ou PageIntro");
|
|
556
|
+
usePageHeadingPresence();
|
|
443
557
|
return (
|
|
444
558
|
<h1
|
|
445
559
|
data-slot="page-title"
|
|
@@ -570,7 +684,7 @@ export function PageFooter({
|
|
|
570
684
|
<div
|
|
571
685
|
data-slot="page-footer"
|
|
572
686
|
className={cn(
|
|
573
|
-
"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",
|
|
574
688
|
className,
|
|
575
689
|
)}
|
|
576
690
|
{...props}
|