@softize/opus 12.10.0 → 13.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 +56 -0
- package/bin/lib/check.mjs +1098 -310
- package/bin/lib/copy.mjs +12 -5
- 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 +180 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/radius-scale.md +1 -1
- 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/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- 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 +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +163 -40
- 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/list.tsx +1097 -783
- 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 +357 -83
- package/src/ui/components/patterns/trigger.tsx +37 -30
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert.tsx +298 -110
- 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/dialog.tsx +196 -39
- 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 +40 -24
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +135 -79
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +104 -25
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +28 -34
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +64 -39
- package/src/ui/docs/content/action-trigger.md +21 -14
- package/src/ui/docs/content/action-view.md +8 -8
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +54 -28
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +21 -22
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +93 -15
- package/src/ui/docs/content/calendar.md +5 -5
- package/src/ui/docs/content/card.md +6 -6
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +3 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +5 -5
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +2 -2
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/copyable.md +4 -3
- package/src/ui/docs/content/customization.md +7 -7
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +8 -5
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +19 -18
- package/src/ui/docs/content/dock.md +3 -3
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +32 -16
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +1 -1
- package/src/ui/docs/content/icon-picker.md +8 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +64 -24
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +5 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +36 -17
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +93 -10
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +16 -8
- package/src/ui/docs/content/progress.md +7 -5
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +42 -29
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +325 -56
- package/src/ui/docs/content/skeleton.md +5 -4
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +8 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +16 -6
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +31 -3
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +7 -8
- package/src/ui/docs/content/ui.md +10 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +580 -229
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +278 -286
- package/src/ui/react.tsx +377 -111
- package/src/ui/theme.css +116 -0
- package/src/ui/components/primitives/alert-dialog.tsx +0 -190
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -78
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -81,10 +81,10 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
|
81
81
|
)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
84
|
+
function EmptyActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
85
85
|
return (
|
|
86
86
|
<div
|
|
87
|
-
data-slot="empty-
|
|
87
|
+
data-slot="empty-actions"
|
|
88
88
|
className={cn(
|
|
89
89
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
|
90
90
|
className
|
|
@@ -99,6 +99,6 @@ export {
|
|
|
99
99
|
EmptyHeader,
|
|
100
100
|
EmptyTitle,
|
|
101
101
|
EmptyDescription,
|
|
102
|
-
|
|
102
|
+
EmptyActions,
|
|
103
103
|
EmptyMedia,
|
|
104
104
|
}
|
|
@@ -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,23 +101,22 @@ 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-start items-center justify-center gap-2 [&_svg]:pointer-events-none",
|
|
114
120
|
{
|
|
115
121
|
variants: {
|
|
116
122
|
variant: {
|
|
@@ -120,17 +126,16 @@ const itemMediaVariants = cva(
|
|
|
120
126
|
"size-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}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
3
|
+
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
import { Card } from './card.tsx'
|
|
5
|
+
import { Skeleton } from './skeleton.tsx'
|
|
6
|
+
|
|
7
|
+
/** @deprecated Use `MetricCardContext`. */
|
|
8
|
+
export type MetricCardTone = 'default' | 'warning'
|
|
9
|
+
export type MetricCardContext = Exclude<UiContext, 'primary'>
|
|
10
|
+
|
|
11
|
+
const iconContextClasses: Record<MetricCardContext, string> = {
|
|
12
|
+
neutral: 'bg-context-neutral-subtle text-context-neutral-emphasis',
|
|
13
|
+
info: 'bg-context-info-subtle text-context-info-emphasis',
|
|
14
|
+
success: 'bg-context-success-subtle text-context-success-emphasis',
|
|
15
|
+
warning: 'bg-context-warning-subtle text-context-warning-emphasis',
|
|
16
|
+
danger: 'bg-context-danger-subtle text-context-danger-emphasis',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children'> {
|
|
20
|
+
/** Controle complementar exibido ao lado do rótulo, como uma explicação em tooltip. */
|
|
21
|
+
labelAction?: React.ReactNode
|
|
22
|
+
/** Contexto que explica recorte, proporção ou significado do valor. */
|
|
23
|
+
description?: React.ReactNode
|
|
24
|
+
/** Ícone decorativo que identifica a natureza da medida. */
|
|
25
|
+
icon?: React.ReactNode
|
|
26
|
+
/** Contexto semântico do ícone; não altera a superfície do card. */
|
|
27
|
+
context?: MetricCardContext
|
|
28
|
+
/** @deprecated Use `context`. Compatibilidade temporária da ADR 0006. */
|
|
29
|
+
tone?: MetricCardTone
|
|
30
|
+
/** Ação relacionada diretamente à medida. */
|
|
31
|
+
action?: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type MetricCardProps = MetricCardBaseProps &
|
|
35
|
+
(
|
|
36
|
+
| {
|
|
37
|
+
/** Nome curto da medida apresentada. */
|
|
38
|
+
label: React.ReactNode
|
|
39
|
+
/** Valor já formatado pelo consumidor. */
|
|
40
|
+
value: React.ReactNode
|
|
41
|
+
loading?: false
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
/** O rótulo pode ser omitido porque o placeholder ocupa toda a composição. */
|
|
45
|
+
label?: React.ReactNode
|
|
46
|
+
/** O valor ainda não está disponível durante o carregamento. */
|
|
47
|
+
value?: React.ReactNode
|
|
48
|
+
/** Substitui o conteúdo por placeholders preservando a geometria do card. */
|
|
49
|
+
loading: true
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* MetricCard apresenta uma medida resumida. Formatação, cálculo e carregamento dos dados
|
|
55
|
+
* continuam sob responsabilidade do consumidor; o componente padroniza apenas a composição.
|
|
56
|
+
*/
|
|
57
|
+
export function MetricCard({
|
|
58
|
+
label,
|
|
59
|
+
labelAction,
|
|
60
|
+
value,
|
|
61
|
+
description,
|
|
62
|
+
icon,
|
|
63
|
+
context,
|
|
64
|
+
tone,
|
|
65
|
+
action,
|
|
66
|
+
loading = false,
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: MetricCardProps): React.ReactElement {
|
|
70
|
+
if (context !== undefined && tone !== undefined) {
|
|
71
|
+
throw new Error('MetricCard não permite combinar context com tone legado.')
|
|
72
|
+
}
|
|
73
|
+
const hasIcon = icon !== undefined && icon !== null
|
|
74
|
+
const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Card
|
|
78
|
+
data-slot="metric-card"
|
|
79
|
+
aria-busy={loading || undefined}
|
|
80
|
+
className={cn('min-w-0 p-4', className)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{loading ? (
|
|
84
|
+
<div className="space-y-3">
|
|
85
|
+
<Skeleton className="h-4 w-28" />
|
|
86
|
+
<Skeleton className="h-8 w-20" />
|
|
87
|
+
<Skeleton className="h-3 w-full" />
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<>
|
|
91
|
+
<div className="flex items-start justify-between gap-3">
|
|
92
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
93
|
+
<div data-slot="metric-card-label" className="min-w-0 truncate text-sm font-medium text-muted-foreground">
|
|
94
|
+
{label}
|
|
95
|
+
</div>
|
|
96
|
+
{labelAction !== undefined && (
|
|
97
|
+
<div data-slot="metric-card-label-action" className="shrink-0">
|
|
98
|
+
{labelAction}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{hasIcon && (
|
|
103
|
+
<span
|
|
104
|
+
data-slot="metric-card-icon"
|
|
105
|
+
data-context={resolvedContext}
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
className={cn(
|
|
108
|
+
'flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4',
|
|
109
|
+
iconContextClasses[resolvedContext],
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{icon}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
<div data-slot="metric-card-value" className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">
|
|
117
|
+
{value}
|
|
118
|
+
</div>
|
|
119
|
+
{description !== undefined && (
|
|
120
|
+
<div data-slot="metric-card-description" className="mt-1 text-xs text-muted-foreground">
|
|
121
|
+
{description}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
{action !== undefined && (
|
|
125
|
+
<div data-slot="metric-card-action" className="mt-3">
|
|
126
|
+
{action}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</Card>
|
|
132
|
+
)
|
|
133
|
+
}
|