@softize/opus 12.9.0 → 12.11.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 +44 -0
- package/bin/lib/check.mjs +1103 -310
- package/bin/lib/copy.mjs +74 -6
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/code-style.md +6 -2
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +34 -19
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +26 -3
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +1096 -777
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +354 -80
- package/src/ui/components/patterns/trigger.tsx +13 -9
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert-dialog.tsx +7 -5
- package/src/ui/components/primitives/alert.tsx +298 -80
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +33 -20
- package/src/ui/components/primitives/field.tsx +4 -4
- package/src/ui/components/primitives/item.tsx +137 -81
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +3 -3
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/alert-dialog.md +8 -8
- package/src/ui/docs/content/alert.md +54 -23
- package/src/ui/docs/content/badge.md +18 -19
- package/src/ui/docs/content/button.md +12 -9
- package/src/ui/docs/content/card.md +5 -5
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/customization.md +2 -2
- package/src/ui/docs/content/detail.md +5 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +11 -10
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +6 -3
- package/src/ui/docs/content/field.md +1 -1
- package/src/ui/docs/content/input-group.md +3 -2
- package/src/ui/docs/content/item.md +47 -21
- package/src/ui/docs/content/menu.md +5 -4
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/page-state.md +45 -0
- package/src/ui/docs/content/page.md +48 -10
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/sidebar.md +4 -4
- package/src/ui/docs/content/skeleton.md +2 -2
- package/src/ui/docs/content/table.md +3 -3
- package/src/ui/docs/content/tokens.md +28 -0
- package/src/ui/docs/content/tooltip.md +15 -1
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +596 -228
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +292 -270
- package/src/ui/react.tsx +378 -111
- package/src/ui/theme.css +66 -0
|
@@ -1,38 +1,91 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
3
4
|
import { cn } from '../../lib/cn.ts'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
6
|
+
export type DotContext = UiContext
|
|
7
|
+
export type DotVariant = 'solid' | 'outline'
|
|
8
|
+
export type LegacyDotVariant =
|
|
9
|
+
| 'default'
|
|
10
|
+
| 'secondary'
|
|
11
|
+
| 'destructive'
|
|
12
|
+
| 'success'
|
|
13
|
+
| 'warning'
|
|
14
|
+
| 'info'
|
|
15
|
+
|
|
16
|
+
interface DotStyleOptions {
|
|
17
|
+
context?: DotContext | null
|
|
18
|
+
variant?: DotVariant | LegacyDotVariant | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const solidClasses: Record<DotContext, string> = {
|
|
22
|
+
neutral: 'bg-context-neutral-emphasis',
|
|
23
|
+
primary: 'bg-context-primary',
|
|
24
|
+
info: 'bg-context-info',
|
|
25
|
+
success: 'bg-context-success',
|
|
26
|
+
warning: 'bg-context-warning',
|
|
27
|
+
danger: 'bg-context-danger',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const outlineClasses: Record<DotContext, string> = {
|
|
31
|
+
neutral: 'border border-context-neutral-border bg-transparent',
|
|
32
|
+
primary: 'border border-context-primary bg-transparent',
|
|
33
|
+
info: 'border border-context-info bg-transparent',
|
|
34
|
+
success: 'border border-context-success bg-transparent',
|
|
35
|
+
warning: 'border border-context-warning bg-transparent',
|
|
36
|
+
danger: 'border border-context-danger bg-transparent',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveDotStyle({ context, variant }: DotStyleOptions = {}): {
|
|
40
|
+
context: DotContext
|
|
41
|
+
variant: DotVariant
|
|
42
|
+
} {
|
|
43
|
+
const legacy =
|
|
44
|
+
variant === 'default' ||
|
|
45
|
+
variant === 'secondary' ||
|
|
46
|
+
variant === 'destructive' ||
|
|
47
|
+
variant === 'success' ||
|
|
48
|
+
variant === 'warning' ||
|
|
49
|
+
variant === 'info'
|
|
50
|
+
if (context != null && legacy) {
|
|
51
|
+
throw new Error('Dot não permite combinar context com uma variante semântica legada.')
|
|
52
|
+
}
|
|
53
|
+
if (variant === 'default') return { context: 'primary', variant: 'solid' }
|
|
54
|
+
if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
|
|
55
|
+
if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
|
|
56
|
+
if (variant === 'success' || variant === 'warning' || variant === 'info') {
|
|
57
|
+
return { context: variant, variant: 'solid' }
|
|
58
|
+
}
|
|
59
|
+
return { context: context ?? 'neutral', variant: variant ?? 'solid' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function dotVariants(options: DotStyleOptions = {}): string {
|
|
63
|
+
const resolved = resolveDotStyle(options)
|
|
64
|
+
return cn(
|
|
65
|
+
'inline-block size-1.5 shrink-0 rounded-full',
|
|
66
|
+
resolved.variant === 'outline'
|
|
67
|
+
? outlineClasses[resolved.context]
|
|
68
|
+
: solidClasses[resolved.context],
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DotProps extends React.ComponentProps<'span'>, DotStyleOptions {
|
|
23
73
|
/** Nome acessível quando a cor comunica estado; sem label, o ponto é decorativo. */
|
|
24
74
|
label?: string
|
|
25
75
|
}
|
|
26
76
|
|
|
27
77
|
/** Indicador compacto de estado; use Badge quando o texto precisar permanecer visível. */
|
|
28
|
-
export function Dot({ className, variant, label, ...props }: DotProps): React.ReactElement {
|
|
78
|
+
export function Dot({ className, context, variant, label, ...props }: DotProps): React.ReactElement {
|
|
79
|
+
const resolved = resolveDotStyle({ context, variant })
|
|
29
80
|
return (
|
|
30
81
|
<span
|
|
31
82
|
data-slot="dot"
|
|
83
|
+
data-context={resolved.context}
|
|
84
|
+
data-variant={resolved.variant}
|
|
32
85
|
role={label === undefined ? undefined : 'img'}
|
|
33
86
|
aria-label={label}
|
|
34
87
|
aria-hidden={label === undefined ? true : undefined}
|
|
35
|
-
className={cn(dotVariants(
|
|
88
|
+
className={cn(dotVariants(resolved), className)}
|
|
36
89
|
{...props}
|
|
37
90
|
/>
|
|
38
91
|
)
|
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { XIcon } from "lucide-react"
|
|
3
|
-
import { Dialog as DrawerPrimitive } from "radix-ui"
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { XIcon } from "lucide-react";
|
|
3
|
+
import { Dialog as DrawerPrimitive } from "radix-ui";
|
|
4
4
|
|
|
5
|
-
import { cn } from
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
6
|
|
|
7
|
-
function Drawer({
|
|
8
|
-
|
|
7
|
+
function Drawer({
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
|
10
|
+
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function DrawerTrigger({
|
|
12
14
|
...props
|
|
13
15
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
|
14
|
-
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props}
|
|
16
|
+
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function DrawerClose({
|
|
18
20
|
...props
|
|
19
21
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
|
20
|
-
return <DrawerPrimitive.Close data-slot="drawer-close" {...props}
|
|
22
|
+
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function DrawerPortal({
|
|
24
26
|
...props
|
|
25
27
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
|
26
|
-
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props}
|
|
28
|
+
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
function DrawerOverlay({
|
|
@@ -35,11 +37,11 @@ function DrawerOverlay({
|
|
|
35
37
|
data-slot="drawer-overlay"
|
|
36
38
|
className={cn(
|
|
37
39
|
"fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
38
|
-
className
|
|
40
|
+
className,
|
|
39
41
|
)}
|
|
40
42
|
{...props}
|
|
41
43
|
/>
|
|
42
|
-
)
|
|
44
|
+
);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function DrawerContent({
|
|
@@ -49,8 +51,8 @@ function DrawerContent({
|
|
|
49
51
|
showCloseButton = true,
|
|
50
52
|
...props
|
|
51
53
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
52
|
-
side?: "top" | "right" | "bottom" | "left"
|
|
53
|
-
showCloseButton?: boolean
|
|
54
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
55
|
+
showCloseButton?: boolean;
|
|
54
56
|
}) {
|
|
55
57
|
return (
|
|
56
58
|
<DrawerPortal>
|
|
@@ -67,7 +69,7 @@ function DrawerContent({
|
|
|
67
69
|
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
68
70
|
side === "bottom" &&
|
|
69
71
|
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
70
|
-
className
|
|
72
|
+
className,
|
|
71
73
|
)}
|
|
72
74
|
{...props}
|
|
73
75
|
>
|
|
@@ -80,7 +82,7 @@ function DrawerContent({
|
|
|
80
82
|
)}
|
|
81
83
|
</DrawerPrimitive.Content>
|
|
82
84
|
</DrawerPortal>
|
|
83
|
-
)
|
|
85
|
+
);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
@@ -90,7 +92,17 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
90
92
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
91
93
|
{...props}
|
|
92
94
|
/>
|
|
93
|
-
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function DrawerBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
data-slot="drawer-body"
|
|
102
|
+
className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
@@ -100,7 +112,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
100
112
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
101
113
|
{...props}
|
|
102
114
|
/>
|
|
103
|
-
)
|
|
115
|
+
);
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
function DrawerTitle({
|
|
@@ -113,7 +125,7 @@ function DrawerTitle({
|
|
|
113
125
|
className={cn("font-semibold text-foreground", className)}
|
|
114
126
|
{...props}
|
|
115
127
|
/>
|
|
116
|
-
)
|
|
128
|
+
);
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
function DrawerDescription({
|
|
@@ -126,7 +138,7 @@ function DrawerDescription({
|
|
|
126
138
|
className={cn("text-sm text-muted-foreground", className)}
|
|
127
139
|
{...props}
|
|
128
140
|
/>
|
|
129
|
-
)
|
|
141
|
+
);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
export {
|
|
@@ -135,7 +147,8 @@ export {
|
|
|
135
147
|
DrawerClose,
|
|
136
148
|
DrawerContent,
|
|
137
149
|
DrawerHeader,
|
|
150
|
+
DrawerBody,
|
|
138
151
|
DrawerFooter,
|
|
139
152
|
DrawerTitle,
|
|
140
153
|
DrawerDescription,
|
|
141
|
-
}
|
|
154
|
+
};
|
|
@@ -57,14 +57,14 @@ const fieldVariants = cva(
|
|
|
57
57
|
{
|
|
58
58
|
variants: {
|
|
59
59
|
orientation: {
|
|
60
|
-
vertical: ["flex-col gap-
|
|
60
|
+
vertical: ["flex-col gap-1.5 [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
61
61
|
horizontal: [
|
|
62
62
|
"flex-row items-center gap-3",
|
|
63
63
|
"[&>[data-slot=field-label]]:flex-auto",
|
|
64
64
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
65
65
|
],
|
|
66
66
|
responsive: [
|
|
67
|
-
"flex-col gap-
|
|
67
|
+
"flex-col gap-1.5 @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
68
68
|
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
69
69
|
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
70
70
|
],
|
|
@@ -113,9 +113,9 @@ function FieldLabel({
|
|
|
113
113
|
<Label
|
|
114
114
|
data-slot="field-label"
|
|
115
115
|
className={cn(
|
|
116
|
-
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
116
|
+
"group/field-label peer/field-label flex w-fit gap-2 text-xs leading-snug font-medium text-muted-foreground group-data-[invalid=true]/field:text-destructive group-data-[disabled=true]/field:opacity-50",
|
|
117
117
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
118
|
-
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
|
118
|
+
"has-[>[data-slot=field]]:has-data-[state=checked]:border-primary has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/5 dark:has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/10",
|
|
119
119
|
className
|
|
120
120
|
)}
|
|
121
121
|
{...props}
|
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { Slot } from "radix-ui"
|
|
4
|
-
|
|
5
|
-
import { cn } from
|
|
6
|
-
import { Separator } from
|
|
7
|
-
|
|
8
|
-
const itemGroupVariants = cva(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
|
+
import { Separator } from "./separator.tsx";
|
|
7
|
+
|
|
8
|
+
const itemGroupVariants = cva(
|
|
9
|
+
"group/item-group @container/item-group flex flex-col",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
plain: "",
|
|
14
|
+
framed:
|
|
15
|
+
"overflow-hidden rounded-xl border border-border bg-card text-card-foreground [&>[data-slot=item]]:rounded-none [&>[data-slot=item-container]>[data-slot=item]]:rounded-none",
|
|
16
|
+
},
|
|
14
17
|
},
|
|
18
|
+
defaultVariants: { variant: "plain" },
|
|
15
19
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const ItemGroupContext = React.createContext(false);
|
|
23
|
+
const ItemContext = React.createContext(false);
|
|
24
|
+
const ItemHeaderContext = React.createContext(false);
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
function requireParent(
|
|
27
|
+
inside: boolean,
|
|
28
|
+
component: string,
|
|
29
|
+
parent: string,
|
|
30
|
+
): void {
|
|
31
|
+
if (!inside) throw new Error(`${component} deve estar dentro de ${parent}.`);
|
|
32
|
+
}
|
|
22
33
|
|
|
23
34
|
function ItemGroup({
|
|
24
35
|
className,
|
|
@@ -35,7 +46,7 @@ function ItemGroup({
|
|
|
35
46
|
{...props}
|
|
36
47
|
/>
|
|
37
48
|
</ItemGroupContext.Provider>
|
|
38
|
-
)
|
|
49
|
+
);
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
function ItemSeparator({
|
|
@@ -49,7 +60,7 @@ function ItemSeparator({
|
|
|
49
60
|
className={cn("my-0", className)}
|
|
50
61
|
{...props}
|
|
51
62
|
/>
|
|
52
|
-
)
|
|
63
|
+
);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
const itemVariants = cva(
|
|
@@ -66,12 +77,9 @@ const itemVariants = cva(
|
|
|
66
77
|
sm: "gap-2.5 px-4 py-3",
|
|
67
78
|
},
|
|
68
79
|
},
|
|
69
|
-
defaultVariants: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
)
|
|
80
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
81
|
+
},
|
|
82
|
+
);
|
|
75
83
|
|
|
76
84
|
function Item({
|
|
77
85
|
className,
|
|
@@ -82,9 +90,8 @@ function Item({
|
|
|
82
90
|
...props
|
|
83
91
|
}: React.ComponentProps<"div"> &
|
|
84
92
|
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
|
85
|
-
const grouped = React.useContext(ItemGroupContext)
|
|
86
|
-
const Comp = asChild ? Slot.Root : "div"
|
|
87
|
-
|
|
93
|
+
const grouped = React.useContext(ItemGroupContext);
|
|
94
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
88
95
|
const content = (
|
|
89
96
|
<Comp
|
|
90
97
|
role={role ?? (!asChild && grouped ? "listitem" : undefined)}
|
|
@@ -94,43 +101,41 @@ function Item({
|
|
|
94
101
|
className={cn(itemVariants({ variant, size, className }))}
|
|
95
102
|
{...props}
|
|
96
103
|
/>
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
if (asChild && grouped) {
|
|
100
|
-
return (
|
|
101
|
-
<div role="listitem" data-slot="item-container">
|
|
102
|
-
{content}
|
|
103
|
-
</div>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
104
|
+
);
|
|
107
105
|
return (
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
<ItemContext.Provider value>
|
|
107
|
+
{asChild && grouped ? (
|
|
108
|
+
<div role="listitem" data-slot="item-container">
|
|
109
|
+
{content}
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
content
|
|
113
|
+
)}
|
|
114
|
+
</ItemContext.Provider>
|
|
115
|
+
);
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
const itemMediaVariants = cva(
|
|
113
|
-
"flex shrink-0 items-center justify-center gap-2
|
|
119
|
+
"flex shrink-0 self-stretch items-center justify-center gap-2 [&_svg]:pointer-events-none",
|
|
114
120
|
{
|
|
115
121
|
variants: {
|
|
116
122
|
variant: {
|
|
117
123
|
default: "bg-transparent",
|
|
118
|
-
icon: "
|
|
124
|
+
icon: "min-h-8 w-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
119
125
|
image:
|
|
120
|
-
"
|
|
126
|
+
"min-h-10 w-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
|
|
121
127
|
},
|
|
122
128
|
},
|
|
123
|
-
defaultVariants: {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
)
|
|
129
|
+
defaultVariants: { variant: "default" },
|
|
130
|
+
},
|
|
131
|
+
);
|
|
128
132
|
|
|
129
133
|
function ItemMedia({
|
|
130
134
|
className,
|
|
131
135
|
variant = "default",
|
|
132
136
|
...props
|
|
133
137
|
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
138
|
+
requireParent(React.useContext(ItemContext), "ItemMedia", "Item");
|
|
134
139
|
return (
|
|
135
140
|
<div
|
|
136
141
|
data-slot="item-media"
|
|
@@ -138,94 +143,145 @@ function ItemMedia({
|
|
|
138
143
|
className={cn(itemMediaVariants({ variant, className }))}
|
|
139
144
|
{...props}
|
|
140
145
|
/>
|
|
141
|
-
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ItemHeader({
|
|
150
|
+
className,
|
|
151
|
+
children,
|
|
152
|
+
...props
|
|
153
|
+
}: React.ComponentProps<"div">) {
|
|
154
|
+
requireParent(React.useContext(ItemContext), "ItemHeader", "Item");
|
|
155
|
+
const nodes = React.Children.toArray(children);
|
|
156
|
+
const titles = nodes.filter(
|
|
157
|
+
(node) => React.isValidElement(node) && node.type === ItemTitle,
|
|
158
|
+
);
|
|
159
|
+
const descriptions = nodes.filter(
|
|
160
|
+
(node) => React.isValidElement(node) && node.type === ItemDescription,
|
|
161
|
+
);
|
|
162
|
+
const canonical =
|
|
163
|
+
titles.length === 1 &&
|
|
164
|
+
descriptions.length <= 1 &&
|
|
165
|
+
titles.length + descriptions.length === nodes.length;
|
|
166
|
+
return (
|
|
167
|
+
<ItemHeaderContext.Provider value>
|
|
168
|
+
<div
|
|
169
|
+
data-slot="item-header"
|
|
170
|
+
className={cn(
|
|
171
|
+
canonical
|
|
172
|
+
? "flex min-w-0 flex-1 flex-col justify-center gap-1"
|
|
173
|
+
: "flex basis-full items-center justify-between gap-2",
|
|
174
|
+
className,
|
|
175
|
+
)}
|
|
176
|
+
{...props}
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</div>
|
|
180
|
+
</ItemHeaderContext.Provider>
|
|
181
|
+
);
|
|
142
182
|
}
|
|
143
183
|
|
|
144
|
-
function
|
|
184
|
+
function ItemBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
185
|
+
requireParent(React.useContext(ItemContext), "ItemBody", "Item");
|
|
145
186
|
return (
|
|
146
187
|
<div
|
|
147
|
-
data-slot="item-
|
|
148
|
-
className={cn(
|
|
149
|
-
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
|
150
|
-
className
|
|
151
|
-
)}
|
|
188
|
+
data-slot="item-body"
|
|
189
|
+
className={cn("flex min-w-0 flex-none flex-col gap-1", className)}
|
|
152
190
|
{...props}
|
|
153
191
|
/>
|
|
154
|
-
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @deprecated Use ItemHeader para título/descrição e ItemBody para o conteúdo restante. */
|
|
196
|
+
function ItemContent({
|
|
197
|
+
className,
|
|
198
|
+
children,
|
|
199
|
+
...props
|
|
200
|
+
}: React.ComponentProps<"div">) {
|
|
201
|
+
requireParent(React.useContext(ItemContext), "ItemContent", "Item");
|
|
202
|
+
return (
|
|
203
|
+
<ItemHeaderContext.Provider value>
|
|
204
|
+
<div
|
|
205
|
+
data-slot="item-content"
|
|
206
|
+
className={cn(
|
|
207
|
+
"flex min-w-0 flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
{...props}
|
|
211
|
+
>
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
214
|
+
</ItemHeaderContext.Provider>
|
|
215
|
+
);
|
|
155
216
|
}
|
|
156
217
|
|
|
157
218
|
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
219
|
+
requireParent(React.useContext(ItemHeaderContext), "ItemTitle", "ItemHeader");
|
|
158
220
|
return (
|
|
159
221
|
<div
|
|
160
222
|
data-slot="item-title"
|
|
161
223
|
className={cn(
|
|
162
224
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
|
163
|
-
className
|
|
225
|
+
className,
|
|
164
226
|
)}
|
|
165
227
|
{...props}
|
|
166
228
|
/>
|
|
167
|
-
)
|
|
229
|
+
);
|
|
168
230
|
}
|
|
169
231
|
|
|
170
232
|
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
233
|
+
requireParent(
|
|
234
|
+
React.useContext(ItemHeaderContext),
|
|
235
|
+
"ItemDescription",
|
|
236
|
+
"ItemHeader",
|
|
237
|
+
);
|
|
171
238
|
return (
|
|
172
239
|
<p
|
|
173
240
|
data-slot="item-description"
|
|
174
241
|
className={cn(
|
|
175
|
-
"line-clamp-2 text-sm leading-normal font-normal text-balance text-muted-foreground",
|
|
176
|
-
|
|
177
|
-
className
|
|
242
|
+
"line-clamp-2 text-sm leading-normal font-normal text-balance text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
243
|
+
className,
|
|
178
244
|
)}
|
|
179
245
|
{...props}
|
|
180
246
|
/>
|
|
181
|
-
)
|
|
247
|
+
);
|
|
182
248
|
}
|
|
183
249
|
|
|
184
250
|
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
251
|
+
requireParent(React.useContext(ItemContext), "ItemActions", "Item");
|
|
185
252
|
return (
|
|
186
253
|
<div
|
|
187
254
|
data-slot="item-actions"
|
|
188
255
|
className={cn("flex items-center gap-2", className)}
|
|
189
256
|
{...props}
|
|
190
257
|
/>
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
195
|
-
return (
|
|
196
|
-
<div
|
|
197
|
-
data-slot="item-header"
|
|
198
|
-
className={cn(
|
|
199
|
-
"flex basis-full items-center justify-between gap-2",
|
|
200
|
-
className
|
|
201
|
-
)}
|
|
202
|
-
{...props}
|
|
203
|
-
/>
|
|
204
|
-
)
|
|
258
|
+
);
|
|
205
259
|
}
|
|
206
260
|
|
|
207
261
|
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
262
|
+
requireParent(React.useContext(ItemContext), "ItemFooter", "Item");
|
|
208
263
|
return (
|
|
209
264
|
<div
|
|
210
265
|
data-slot="item-footer"
|
|
211
266
|
className={cn(
|
|
212
267
|
"flex basis-full items-center justify-between gap-2",
|
|
213
|
-
className
|
|
268
|
+
className,
|
|
214
269
|
)}
|
|
215
270
|
{...props}
|
|
216
271
|
/>
|
|
217
|
-
)
|
|
272
|
+
);
|
|
218
273
|
}
|
|
219
274
|
|
|
220
275
|
export {
|
|
221
276
|
Item,
|
|
222
277
|
ItemMedia,
|
|
278
|
+
ItemHeader,
|
|
279
|
+
ItemBody,
|
|
223
280
|
ItemContent,
|
|
224
281
|
ItemActions,
|
|
225
282
|
ItemGroup,
|
|
226
283
|
ItemSeparator,
|
|
227
284
|
ItemTitle,
|
|
228
285
|
ItemDescription,
|
|
229
|
-
ItemHeader,
|
|
230
286
|
ItemFooter,
|
|
231
|
-
}
|
|
287
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
2
3
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
3
4
|
import { DropdownMenu as MenuPrimitive } from "radix-ui"
|
|
4
5
|
|
|
@@ -65,19 +66,26 @@ function MenuGroup({
|
|
|
65
66
|
function MenuItem({
|
|
66
67
|
className,
|
|
67
68
|
inset,
|
|
68
|
-
|
|
69
|
+
context,
|
|
70
|
+
variant,
|
|
69
71
|
...props
|
|
70
72
|
}: React.ComponentProps<typeof MenuPrimitive.Item> & {
|
|
71
73
|
inset?: boolean
|
|
74
|
+
context?: Extract<UiContext, 'neutral' | 'danger'>
|
|
75
|
+
/** @deprecated Use `context="danger"`. */
|
|
72
76
|
variant?: "default" | "destructive"
|
|
73
77
|
}) {
|
|
78
|
+
if (context !== undefined && variant !== undefined) {
|
|
79
|
+
throw new Error('MenuItem não permite combinar context com uma variante semântica legada.')
|
|
80
|
+
}
|
|
81
|
+
const resolvedContext = context ?? (variant === 'destructive' ? 'danger' : 'neutral')
|
|
74
82
|
return (
|
|
75
83
|
<MenuPrimitive.Item
|
|
76
84
|
data-slot="menu-item"
|
|
77
85
|
data-inset={inset}
|
|
78
|
-
data-
|
|
86
|
+
data-context={resolvedContext}
|
|
79
87
|
className={cn(
|
|
80
|
-
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[
|
|
88
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[context=danger]:focus:bg-context-danger-subtle data-[context=danger]:focus:text-context-danger-emphasis [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[context=danger]:focus:*:[svg]:text-context-danger-emphasis!",
|
|
81
89
|
className
|
|
82
90
|
)}
|
|
83
91
|
{...props}
|