@softize/opus 15.2.2 → 16.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 +51 -18
- package/bin/lib/check.mjs +98 -15
- package/bin/lib/copy.mjs +811 -314
- package/bin/lib/gen-manifest.mjs +24 -23
- package/bin/lib/gen-runner.mjs +188 -148
- package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
- package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
- package/docs/code-style.md +24 -19
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
- package/src/core/presentation.ts +512 -0
- package/src/presentation/index.ts +1 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
- package/src/ui/components/patterns/confirm.tsx +34 -29
- package/src/ui/components/patterns/form-dialog.tsx +20 -8
- package/src/ui/components/patterns/list.tsx +26 -9
- package/src/ui/components/patterns/page.tsx +99 -139
- package/src/ui/components/patterns/presentation.tsx +316 -0
- package/src/ui/components/patterns/sidebar.tsx +3 -3
- package/src/ui/components/patterns/trigger.tsx +38 -17
- package/src/ui/components/primitives/button-group.tsx +53 -43
- package/src/ui/components/primitives/command.tsx +30 -72
- package/src/ui/components/primitives/dialog.tsx +23 -89
- package/src/ui/components/primitives/drawer.tsx +8 -34
- package/src/ui/docs/content/action-form-dialog.md +12 -10
- package/src/ui/docs/content/action-list-dialog.md +22 -17
- package/src/ui/docs/content/button.md +52 -35
- package/src/ui/docs/content/communication.md +26 -26
- package/src/ui/docs/content/dialog.md +173 -154
- package/src/ui/docs/content/drawer.md +12 -11
- package/src/ui/docs/content/page.md +72 -91
- package/src/ui/docs/content/presentation.md +158 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +8 -2
- package/src/ui/react.tsx +10 -3
|
@@ -22,24 +22,13 @@ import { createPortal } from "react-dom";
|
|
|
22
22
|
import { ArrowLeft } from "lucide-react";
|
|
23
23
|
import { cn } from "../../lib/cn.ts";
|
|
24
24
|
import { Button } from "../primitives/button.tsx";
|
|
25
|
-
import {
|
|
26
|
-
Tooltip,
|
|
27
|
-
TooltipContent,
|
|
28
|
-
TooltipProvider,
|
|
29
|
-
TooltipTrigger,
|
|
30
|
-
} from "../primitives/tooltip.tsx";
|
|
31
25
|
import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
|
|
32
26
|
import { PageStatePresenceProvider } from "./page-state.tsx";
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
interface PageContextValue {
|
|
37
|
-
headerVariant: PageHeaderVariant;
|
|
38
|
-
containerClassName?: string;
|
|
39
|
-
}
|
|
28
|
+
interface PageContextValue {}
|
|
40
29
|
|
|
41
30
|
const PageContext = createContext<PageContextValue | null>(null);
|
|
42
|
-
type PageHeadingRegion =
|
|
31
|
+
type PageHeadingRegion = "header" | "intro";
|
|
43
32
|
|
|
44
33
|
const PageHeaderContext = createContext<PageHeadingRegion | null>(null);
|
|
45
34
|
const PageIntegralStateContext = createContext(false);
|
|
@@ -49,6 +38,11 @@ const pageBarClassName =
|
|
|
49
38
|
"flex h-12 shrink-0 items-center border-b border-border bg-background text-foreground";
|
|
50
39
|
const pageBarContentClassName = "w-full py-2";
|
|
51
40
|
|
|
41
|
+
/** Uso interno de patterns que precisam adaptar a Page ao shell sem duplicar sua anatomia. */
|
|
42
|
+
export function useInsidePageShell(): boolean {
|
|
43
|
+
return useContext(PageShellContext);
|
|
44
|
+
}
|
|
45
|
+
|
|
52
46
|
function PageShellNavigationSlot({
|
|
53
47
|
className,
|
|
54
48
|
...props
|
|
@@ -102,7 +96,9 @@ export function PageShell({
|
|
|
102
96
|
headerClassName,
|
|
103
97
|
...props
|
|
104
98
|
}: PageShellProps): ReactElement {
|
|
105
|
-
const [actionsTarget, setActionsTarget] = useState<HTMLDivElement | null>(
|
|
99
|
+
const [actionsTarget, setActionsTarget] = useState<HTMLDivElement | null>(
|
|
100
|
+
null,
|
|
101
|
+
);
|
|
106
102
|
|
|
107
103
|
return (
|
|
108
104
|
<div
|
|
@@ -124,11 +120,7 @@ export function PageShell({
|
|
|
124
120
|
}}
|
|
125
121
|
leadingPlacement="inline"
|
|
126
122
|
titleRequired={false}
|
|
127
|
-
contentClassName={cn(
|
|
128
|
-
pageBarContentClassName,
|
|
129
|
-
"px-3",
|
|
130
|
-
headerClassName,
|
|
131
|
-
)}
|
|
123
|
+
contentClassName={cn(pageBarContentClassName, "px-3", headerClassName)}
|
|
132
124
|
className={pageBarClassName}
|
|
133
125
|
data-variant="bar"
|
|
134
126
|
>
|
|
@@ -211,6 +203,9 @@ export function Page({
|
|
|
211
203
|
const bodies = nodes.filter(
|
|
212
204
|
(node) => isValidElement(node) && node.type === PageBody,
|
|
213
205
|
);
|
|
206
|
+
const footers = nodes.filter(
|
|
207
|
+
(node) => isValidElement(node) && node.type === PageFooter,
|
|
208
|
+
);
|
|
214
209
|
const intros = nodes.filter(
|
|
215
210
|
(node) => isValidElement(node) && node.type === PageIntro,
|
|
216
211
|
);
|
|
@@ -240,58 +235,52 @@ export function Page({
|
|
|
240
235
|
intros.length === 1 &&
|
|
241
236
|
headers.length === 0 &&
|
|
242
237
|
bodies.length === 1 &&
|
|
243
|
-
|
|
238
|
+
footers.length <= 1 &&
|
|
239
|
+
intros.length + bodies.length + footers.length === nodes.length;
|
|
244
240
|
const validStandaloneComposition =
|
|
245
241
|
!insideShell &&
|
|
246
242
|
headers.length === 1 &&
|
|
247
243
|
intros.length === 0 &&
|
|
248
244
|
bodies.length === 1 &&
|
|
249
|
-
|
|
245
|
+
footers.length <= 1 &&
|
|
246
|
+
headers.length + bodies.length + footers.length === nodes.length;
|
|
250
247
|
if (!validIntroComposition && !validStandaloneComposition) {
|
|
251
248
|
throw new Error(
|
|
252
249
|
insideShell
|
|
253
|
-
? "Page explícito dentro de PageShell exige
|
|
254
|
-
: "Page explícito exige
|
|
250
|
+
? "Page explícito dentro de PageShell exige um PageIntro, um PageBody e no máximo um PageFooter como filhos diretos."
|
|
251
|
+
: "Page explícito exige um PageHeader ou PageIntro, um PageBody e no máximo um PageFooter como filhos diretos.",
|
|
255
252
|
);
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
|
-
const
|
|
259
|
-
?
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)}
|
|
285
|
-
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
286
|
-
</PageHeader>
|
|
287
|
-
<PageBody>{children}</PageBody>
|
|
288
|
-
</>
|
|
289
|
-
) : (
|
|
290
|
-
children
|
|
291
|
-
);
|
|
255
|
+
const content =
|
|
256
|
+
shorthand && insideShell ? (
|
|
257
|
+
<>
|
|
258
|
+
<PageIntro>
|
|
259
|
+
<PageTitle>{title}</PageTitle>
|
|
260
|
+
{description !== undefined && (
|
|
261
|
+
<PageDescription>{description}</PageDescription>
|
|
262
|
+
)}
|
|
263
|
+
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
264
|
+
</PageIntro>
|
|
265
|
+
<PageBody>{children}</PageBody>
|
|
266
|
+
</>
|
|
267
|
+
) : shorthand ? (
|
|
268
|
+
<>
|
|
269
|
+
<PageHeader>
|
|
270
|
+
<PageTitle>{title}</PageTitle>
|
|
271
|
+
{description !== undefined && (
|
|
272
|
+
<PageDescription>{description}</PageDescription>
|
|
273
|
+
)}
|
|
274
|
+
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
275
|
+
</PageHeader>
|
|
276
|
+
<PageBody>{children}</PageBody>
|
|
277
|
+
</>
|
|
278
|
+
) : (
|
|
279
|
+
children
|
|
280
|
+
);
|
|
292
281
|
|
|
293
282
|
return (
|
|
294
|
-
<PageContext.Provider value={
|
|
283
|
+
<PageContext.Provider value={{}}>
|
|
295
284
|
<PageIntegralStateContext.Provider value={integralState}>
|
|
296
285
|
<PageStatePresenceProvider register={registerIntegralState}>
|
|
297
286
|
<main
|
|
@@ -299,28 +288,20 @@ export function Page({
|
|
|
299
288
|
className={cn(
|
|
300
289
|
"min-w-0 flex-1",
|
|
301
290
|
insideShell && "min-h-full",
|
|
302
|
-
|
|
303
|
-
// A barra contém a rolagem (`min-h-0`); o estado integral ocupa a altura
|
|
304
|
-
// disponível (`min-h-full`). Emitir os dois juntos deixava o resultado por
|
|
305
|
-
// conta da ordem do CSS gerado — no estado integral, quem manda é ele.
|
|
306
|
-
integralState ? "min-h-full" : headerVariant === "bar" && "min-h-0",
|
|
291
|
+
integralState && "flex min-h-full flex-col",
|
|
307
292
|
)}
|
|
308
293
|
{...props}
|
|
309
294
|
>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
>
|
|
321
|
-
{content}
|
|
322
|
-
</div>
|
|
323
|
-
)}
|
|
295
|
+
<div
|
|
296
|
+
className={cn(
|
|
297
|
+
"mx-auto max-w-7xl space-y-6 px-8 py-8",
|
|
298
|
+
integralState &&
|
|
299
|
+
"flex min-h-full w-full flex-1 flex-col space-y-0",
|
|
300
|
+
className,
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{content}
|
|
304
|
+
</div>
|
|
324
305
|
</main>
|
|
325
306
|
</PageStatePresenceProvider>
|
|
326
307
|
</PageIntegralStateContext.Provider>
|
|
@@ -356,10 +337,12 @@ export function PageIntro({
|
|
|
356
337
|
name="PageIntro"
|
|
357
338
|
slot="page-intro"
|
|
358
339
|
slots={{
|
|
340
|
+
leading: [PageBack, PageNavigation],
|
|
359
341
|
title: PageTitle,
|
|
360
342
|
description: PageDescription,
|
|
361
343
|
actions: PageActions,
|
|
362
344
|
}}
|
|
345
|
+
leadingPlacement="above"
|
|
363
346
|
className={className}
|
|
364
347
|
{...props}
|
|
365
348
|
>
|
|
@@ -370,13 +353,9 @@ export function PageIntro({
|
|
|
370
353
|
}
|
|
371
354
|
|
|
372
355
|
/** A anatomia é a de `SurfaceHeader`, a mesma de `ContentHeader`; só os slots mudam de nome. */
|
|
373
|
-
export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
374
|
-
/** `default` fica no container; `bar` cria uma faixa compacta no topo da Page. */
|
|
375
|
-
variant?: PageHeaderVariant;
|
|
376
|
-
}
|
|
356
|
+
export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {}
|
|
377
357
|
|
|
378
358
|
export function PageHeader({
|
|
379
|
-
variant = "default",
|
|
380
359
|
className,
|
|
381
360
|
children,
|
|
382
361
|
...props
|
|
@@ -386,7 +365,7 @@ export function PageHeader({
|
|
|
386
365
|
requireParent(page !== null, "PageHeader", "Page");
|
|
387
366
|
if (integralState) return <></>;
|
|
388
367
|
return (
|
|
389
|
-
<PageHeaderContext.Provider value=
|
|
368
|
+
<PageHeaderContext.Provider value="header">
|
|
390
369
|
<SurfaceHeader
|
|
391
370
|
name="PageHeader"
|
|
392
371
|
slot="page"
|
|
@@ -396,20 +375,8 @@ export function PageHeader({
|
|
|
396
375
|
description: PageDescription,
|
|
397
376
|
actions: PageActions,
|
|
398
377
|
}}
|
|
399
|
-
leadingPlacement=
|
|
400
|
-
|
|
401
|
-
variant === "bar"
|
|
402
|
-
? cn(
|
|
403
|
-
pageBarContentClassName,
|
|
404
|
-
"mx-auto max-w-7xl px-8",
|
|
405
|
-
page?.containerClassName,
|
|
406
|
-
)
|
|
407
|
-
: undefined
|
|
408
|
-
}
|
|
409
|
-
className={cn(
|
|
410
|
-
variant === "bar" && pageBarClassName,
|
|
411
|
-
className,
|
|
412
|
-
)}
|
|
378
|
+
leadingPlacement="inline"
|
|
379
|
+
className={className}
|
|
413
380
|
{...props}
|
|
414
381
|
>
|
|
415
382
|
{children}
|
|
@@ -428,11 +395,9 @@ export function PageTitle({
|
|
|
428
395
|
<h1
|
|
429
396
|
data-slot="page-title"
|
|
430
397
|
className={cn(
|
|
431
|
-
variant === "
|
|
432
|
-
? "
|
|
433
|
-
:
|
|
434
|
-
? "text-3xl font-semibold tracking-tight"
|
|
435
|
-
: surfaceHeaderClasses.page.title,
|
|
398
|
+
variant === "intro"
|
|
399
|
+
? "text-3xl font-semibold tracking-tight"
|
|
400
|
+
: surfaceHeaderClasses.page.title,
|
|
436
401
|
className,
|
|
437
402
|
)}
|
|
438
403
|
{...props}
|
|
@@ -445,20 +410,11 @@ export function PageDescription({
|
|
|
445
410
|
...props
|
|
446
411
|
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
447
412
|
const variant = useContext(PageHeaderContext);
|
|
448
|
-
requireParent(
|
|
449
|
-
variant !== null,
|
|
450
|
-
"PageDescription",
|
|
451
|
-
"PageHeader ou PageIntro",
|
|
452
|
-
);
|
|
413
|
+
requireParent(variant !== null, "PageDescription", "PageHeader ou PageIntro");
|
|
453
414
|
return (
|
|
454
415
|
<p
|
|
455
416
|
data-slot="page-description"
|
|
456
|
-
className={cn(
|
|
457
|
-
variant === "bar"
|
|
458
|
-
? "mt-0.5 truncate text-xs text-muted-foreground"
|
|
459
|
-
: surfaceHeaderClasses.page.description,
|
|
460
|
-
className,
|
|
461
|
-
)}
|
|
417
|
+
className={cn(surfaceHeaderClasses.page.description, className)}
|
|
462
418
|
{...props}
|
|
463
419
|
/>
|
|
464
420
|
);
|
|
@@ -476,7 +432,7 @@ export function PageActions({
|
|
|
476
432
|
<div
|
|
477
433
|
data-slot="page-actions"
|
|
478
434
|
className={cn(
|
|
479
|
-
variant === "
|
|
435
|
+
insideShell || variant === "header"
|
|
480
436
|
? "flex shrink-0 items-center gap-2"
|
|
481
437
|
: surfaceHeaderClasses.page.actions,
|
|
482
438
|
className,
|
|
@@ -488,12 +444,14 @@ export function PageActions({
|
|
|
488
444
|
return target === null ? actions : createPortal(actions, target);
|
|
489
445
|
}
|
|
490
446
|
|
|
491
|
-
export interface PageBackProps extends Omit<
|
|
492
|
-
|
|
447
|
+
export interface PageBackProps extends Omit<
|
|
448
|
+
ComponentProps<"a">,
|
|
449
|
+
"children" | "href"
|
|
450
|
+
> {
|
|
451
|
+
/** Nome visível do destino pai e base do nome acessível. */
|
|
493
452
|
children: ReactNode;
|
|
494
|
-
/** Destino explícito. Obrigatório: sem `href` o elemento não é tabulável
|
|
495
|
-
* `role="link"
|
|
496
|
-
* visível, o retorno simplesmente desapareceria para tecnologia assistiva. */
|
|
453
|
+
/** Destino explícito. Obrigatório: sem `href` o elemento não é tabulável nem expõe
|
|
454
|
+
* `role="link"`. */
|
|
497
455
|
href: string;
|
|
498
456
|
}
|
|
499
457
|
|
|
@@ -505,7 +463,7 @@ export function PageNavigation({
|
|
|
505
463
|
requireParent(
|
|
506
464
|
useContext(PageHeaderContext) !== null,
|
|
507
465
|
"PageNavigation",
|
|
508
|
-
"PageHeader",
|
|
466
|
+
"PageHeader ou PageIntro",
|
|
509
467
|
);
|
|
510
468
|
return (
|
|
511
469
|
<div
|
|
@@ -523,34 +481,24 @@ export function PageBack({
|
|
|
523
481
|
...props
|
|
524
482
|
}: PageBackProps): ReactElement {
|
|
525
483
|
const variant = useContext(PageHeaderContext);
|
|
526
|
-
requireParent(variant !== null, "PageBack", "PageHeader");
|
|
484
|
+
requireParent(variant !== null, "PageBack", "PageHeader ou PageIntro");
|
|
527
485
|
const destination =
|
|
528
486
|
typeof children === "string" ? children : "a página anterior";
|
|
529
487
|
const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
|
|
530
|
-
|
|
531
|
-
<Button asChild variant="ghost" size=
|
|
488
|
+
return (
|
|
489
|
+
<Button asChild variant="ghost" size="icon-sm">
|
|
532
490
|
<a
|
|
533
491
|
data-slot="page-back"
|
|
534
492
|
aria-label={accessibleLabel}
|
|
493
|
+
title={accessibleLabel}
|
|
535
494
|
className={className}
|
|
536
495
|
{...props}
|
|
537
496
|
>
|
|
538
497
|
<ArrowLeft aria-hidden="true" />
|
|
539
|
-
|
|
498
|
+
<span className="sr-only">{children}</span>
|
|
540
499
|
</a>
|
|
541
500
|
</Button>
|
|
542
501
|
);
|
|
543
|
-
|
|
544
|
-
return variant === "bar" ? (
|
|
545
|
-
<TooltipProvider delayDuration={200}>
|
|
546
|
-
<Tooltip>
|
|
547
|
-
<TooltipTrigger asChild>{link}</TooltipTrigger>
|
|
548
|
-
<TooltipContent>{accessibleLabel}</TooltipContent>
|
|
549
|
-
</Tooltip>
|
|
550
|
-
</TooltipProvider>
|
|
551
|
-
) : (
|
|
552
|
-
link
|
|
553
|
-
);
|
|
554
502
|
}
|
|
555
503
|
|
|
556
504
|
export function PageBody({
|
|
@@ -563,11 +511,23 @@ export function PageBody({
|
|
|
563
511
|
return (
|
|
564
512
|
<div
|
|
565
513
|
data-slot="page-body"
|
|
514
|
+
className={cn(integralState && "flex min-h-0 flex-1 flex-col", className)}
|
|
515
|
+
{...props}
|
|
516
|
+
/>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function PageFooter({
|
|
521
|
+
className,
|
|
522
|
+
...props
|
|
523
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
524
|
+
const page = useContext(PageContext);
|
|
525
|
+
requireParent(page !== null, "PageFooter", "Page");
|
|
526
|
+
return (
|
|
527
|
+
<div
|
|
528
|
+
data-slot="page-footer"
|
|
566
529
|
className={cn(
|
|
567
|
-
|
|
568
|
-
"mx-auto w-full max-w-7xl flex-1 px-8 py-8",
|
|
569
|
-
page?.headerVariant === "bar" && page.containerClassName,
|
|
570
|
-
integralState && "flex min-h-0 flex-1 flex-col",
|
|
530
|
+
"mt-auto flex shrink-0 items-center justify-end border-t bg-muted/30 px-8 py-4",
|
|
571
531
|
className,
|
|
572
532
|
)}
|
|
573
533
|
{...props}
|