@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
|
@@ -2,11 +2,26 @@ import * as React from "react";
|
|
|
2
2
|
import { Dialog as DrawerPrimitive } from "radix-ui";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import {
|
|
6
|
+
SurfaceLayoutProvider,
|
|
7
|
+
surfaceActionsClasses,
|
|
8
|
+
surfaceHeaderRowClasses,
|
|
9
|
+
surfaceRegionClasses,
|
|
10
|
+
surfaceTitleClasses,
|
|
11
|
+
useSurfaceLayout,
|
|
12
|
+
type SurfaceLayout,
|
|
13
|
+
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
15
|
+
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
16
|
+
import {
|
|
17
|
+
SurfaceMedia,
|
|
18
|
+
type SurfaceMediaProps,
|
|
19
|
+
} from "../patterns/surface-media.tsx";
|
|
5
20
|
import {
|
|
6
21
|
CloseButton,
|
|
7
22
|
type CloseButtonSize,
|
|
8
23
|
} from "./close-button.tsx";
|
|
9
|
-
import { overlayClasses } from "./dialog.tsx";
|
|
24
|
+
import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
|
|
10
25
|
|
|
11
26
|
interface DrawerContentContextValue {
|
|
12
27
|
showCloseButton: boolean;
|
|
@@ -78,19 +93,29 @@ function DrawerContent({
|
|
|
78
93
|
showCloseButton = true,
|
|
79
94
|
closeDisabled = false,
|
|
80
95
|
closeSize = "sm",
|
|
96
|
+
layout = "sectioned",
|
|
81
97
|
"aria-describedby": ariaDescribedBy,
|
|
98
|
+
onEscapeKeyDown,
|
|
99
|
+
onInteractOutside,
|
|
82
100
|
...props
|
|
83
101
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
84
102
|
side?: "top" | "right" | "bottom" | "left";
|
|
85
103
|
showCloseButton?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
|
|
106
|
+
* continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
|
|
107
|
+
*/
|
|
86
108
|
closeDisabled?: boolean;
|
|
87
109
|
closeSize?: CloseButtonSize;
|
|
110
|
+
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
111
|
+
layout?: SurfaceLayout;
|
|
88
112
|
}) {
|
|
89
113
|
return (
|
|
90
114
|
<DrawerPortal>
|
|
91
115
|
<DrawerOverlay />
|
|
92
116
|
<DrawerPrimitive.Content
|
|
93
117
|
data-slot="drawer-content"
|
|
118
|
+
data-layout={layout}
|
|
94
119
|
className={cn(
|
|
95
120
|
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
96
121
|
side === "right" &&
|
|
@@ -104,13 +129,17 @@ function DrawerContent({
|
|
|
104
129
|
className,
|
|
105
130
|
)}
|
|
106
131
|
aria-describedby={ariaDescribedBy}
|
|
132
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
133
|
+
onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
|
|
107
134
|
{...props}
|
|
108
135
|
>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
<SurfaceLayoutProvider value={layout}>
|
|
137
|
+
<DrawerContentContext.Provider
|
|
138
|
+
value={{ showCloseButton, closeDisabled, closeSize }}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</DrawerContentContext.Provider>
|
|
142
|
+
</SurfaceLayoutProvider>
|
|
114
143
|
</DrawerPrimitive.Content>
|
|
115
144
|
</DrawerPortal>
|
|
116
145
|
);
|
|
@@ -121,27 +150,76 @@ function DrawerHeader({
|
|
|
121
150
|
children,
|
|
122
151
|
...props
|
|
123
152
|
}: React.ComponentProps<"div">) {
|
|
153
|
+
const layout = useSurfaceLayout("drawer");
|
|
124
154
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
125
155
|
React.useContext(DrawerContentContext);
|
|
126
156
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
<SurfaceHeader
|
|
158
|
+
name="DrawerHeader"
|
|
159
|
+
slot="drawer"
|
|
160
|
+
slots={{
|
|
161
|
+
leading: DrawerNavigation,
|
|
162
|
+
media: DrawerMedia,
|
|
163
|
+
title: DrawerTitle,
|
|
164
|
+
actions: DrawerActions,
|
|
165
|
+
}}
|
|
166
|
+
leadingPlacement="inline"
|
|
167
|
+
titleRequired={false}
|
|
168
|
+
rowClassName={surfaceHeaderRowClasses("drawer")}
|
|
169
|
+
className={cn(
|
|
170
|
+
"text-left",
|
|
171
|
+
surfaceRegionClasses("drawer", layout, "header"),
|
|
172
|
+
className,
|
|
173
|
+
)}
|
|
174
|
+
trailing={
|
|
175
|
+
showCloseButton ? (
|
|
176
|
+
<DrawerCloseButton disabled={closeDisabled} size={closeSize} />
|
|
177
|
+
) : undefined
|
|
178
|
+
}
|
|
130
179
|
{...props}
|
|
131
180
|
>
|
|
132
181
|
{children}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
182
|
+
</SurfaceHeader>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Moldura de ícone ou imagem que abre a linha do header. */
|
|
187
|
+
function DrawerMedia(props: SurfaceMediaProps) {
|
|
188
|
+
return <SurfaceMedia slot="drawer-media" {...props} />;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Região introdutória do header — o voltar de um fluxo modal encadeado. */
|
|
192
|
+
function DrawerNavigation({ className, ...props }: React.ComponentProps<"div">) {
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
data-slot="drawer-navigation-content"
|
|
196
|
+
className={cn("flex min-w-0 shrink-0 items-center", className)}
|
|
197
|
+
{...props}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Ações da tela no header, antes do botão de fechar. */
|
|
203
|
+
function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
data-slot="drawer-actions"
|
|
207
|
+
className={cn(surfaceActionsClasses, className)}
|
|
208
|
+
{...props}
|
|
209
|
+
/>
|
|
137
210
|
);
|
|
138
211
|
}
|
|
139
212
|
|
|
140
213
|
function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">) {
|
|
214
|
+
const layout = useSurfaceLayout("drawer");
|
|
141
215
|
return (
|
|
142
216
|
<div
|
|
143
217
|
data-slot="drawer-body"
|
|
144
|
-
className={cn(
|
|
218
|
+
className={cn(
|
|
219
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
220
|
+
surfaceRegionClasses("drawer", layout, "body"),
|
|
221
|
+
className,
|
|
222
|
+
)}
|
|
145
223
|
style={{ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties}
|
|
146
224
|
{...props}
|
|
147
225
|
/>
|
|
@@ -149,11 +227,16 @@ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">)
|
|
|
149
227
|
}
|
|
150
228
|
|
|
151
229
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
230
|
+
const layout = useSurfaceLayout("drawer");
|
|
152
231
|
return (
|
|
153
232
|
<div
|
|
154
233
|
data-slot="drawer-footer"
|
|
155
234
|
className={cn(
|
|
156
|
-
|
|
235
|
+
// `mt-auto` é a divergência declarada do Drawer: o conteúdo lateral quase nunca
|
|
236
|
+
// preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
|
|
237
|
+
// Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
|
|
238
|
+
"mt-auto flex shrink-0 flex-row justify-end gap-2",
|
|
239
|
+
surfaceRegionClasses("drawer", layout, "footer"),
|
|
157
240
|
className,
|
|
158
241
|
)}
|
|
159
242
|
{...props}
|
|
@@ -163,21 +246,45 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
163
246
|
|
|
164
247
|
function DrawerTitle({
|
|
165
248
|
className,
|
|
249
|
+
level,
|
|
250
|
+
children,
|
|
166
251
|
...props
|
|
167
|
-
}: React.ComponentProps<typeof DrawerPrimitive.Title>
|
|
252
|
+
}: React.ComponentProps<typeof DrawerPrimitive.Title> & {
|
|
253
|
+
/** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
|
|
254
|
+
level?: HeadingLevel;
|
|
255
|
+
}) {
|
|
256
|
+
const layout = useSurfaceLayout("drawer");
|
|
257
|
+
const classes = cn(
|
|
258
|
+
"min-w-0 truncate",
|
|
259
|
+
surfaceTitleClasses("drawer", layout),
|
|
260
|
+
className,
|
|
261
|
+
);
|
|
262
|
+
// O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
|
|
263
|
+
if (level === undefined) {
|
|
264
|
+
return (
|
|
265
|
+
<DrawerPrimitive.Title
|
|
266
|
+
data-slot="drawer-title"
|
|
267
|
+
className={classes}
|
|
268
|
+
{...props}
|
|
269
|
+
>
|
|
270
|
+
{children}
|
|
271
|
+
</DrawerPrimitive.Title>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const Heading = headingTags[level];
|
|
168
275
|
return (
|
|
169
|
-
<DrawerPrimitive.Title
|
|
170
|
-
data-slot="drawer-title"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)}
|
|
175
|
-
{...props}
|
|
176
|
-
/>
|
|
276
|
+
<DrawerPrimitive.Title asChild {...props}>
|
|
277
|
+
<Heading data-slot="drawer-title" className={classes}>
|
|
278
|
+
{children}
|
|
279
|
+
</Heading>
|
|
280
|
+
</DrawerPrimitive.Title>
|
|
177
281
|
);
|
|
178
282
|
}
|
|
179
283
|
|
|
180
284
|
export {
|
|
285
|
+
DrawerActions,
|
|
286
|
+
DrawerMedia,
|
|
287
|
+
DrawerNavigation,
|
|
181
288
|
Drawer,
|
|
182
289
|
DrawerTrigger,
|
|
183
290
|
DrawerClose,
|
|
@@ -69,7 +69,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function EmptyDescription({ className, ...props }: React.ComponentProps<"
|
|
72
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
73
|
return (
|
|
74
74
|
<div
|
|
75
75
|
data-slot="empty-description"
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
type LucideIcon,
|
|
47
47
|
} from 'lucide-react'
|
|
48
48
|
import { cn } from '../../lib/cn.ts'
|
|
49
|
-
import { focusRing } from './control.ts'
|
|
49
|
+
import { controlHeight, focusRing } from './control.ts'
|
|
50
50
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
|
|
51
51
|
import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
|
|
52
52
|
|
|
@@ -142,8 +142,12 @@ export function IconPicker({
|
|
|
142
142
|
aria-describedby={ariaDescribedBy}
|
|
143
143
|
data-slot="icon-picker"
|
|
144
144
|
className={cn(
|
|
145
|
-
|
|
145
|
+
// Mesma altura do Input e do Select no tamanho default: lado a lado num form, alinham.
|
|
146
|
+
'flex w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
|
|
147
|
+
controlHeight.default,
|
|
146
148
|
focusRing,
|
|
149
|
+
// As mesmas classes de inválido do Input e do Select: o erro aparece na moldura.
|
|
150
|
+
'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
|
|
147
151
|
className,
|
|
148
152
|
)}
|
|
149
153
|
>
|
|
@@ -41,6 +41,13 @@ export interface IconTileProps
|
|
|
41
41
|
extends React.ComponentProps<'span'>,
|
|
42
42
|
IconTileStyleOptions {}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Preenche os padrões de `context`, `variant` e `size` (`neutral`, `subtle`, `default`).
|
|
46
|
+
*
|
|
47
|
+
* @deprecated Não há decisão a tomar com os valores resolvidos fora do próprio IconTile. Para a
|
|
48
|
+
* moldura, renderize `IconTile`; para aplicá-la a outro elemento, use `iconTileVariants`, que já
|
|
49
|
+
* aplica os mesmos padrões. Sai do pacote numa próxima versão major.
|
|
50
|
+
*/
|
|
44
51
|
export function resolveIconTileStyle({
|
|
45
52
|
context,
|
|
46
53
|
variant,
|
|
@@ -37,7 +37,12 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const inputGroupAddonVariants = cva(
|
|
40
|
-
|
|
40
|
+
[
|
|
41
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-0.3125rem)] [&>svg:not([class*='size-'])]:size-4",
|
|
42
|
+
// O addon acompanha o controle desabilitado sem depender do `data-disabled` manual. Só o
|
|
43
|
+
// controle conta: um InputGroupButton desabilitado não apaga o grupo inteiro.
|
|
44
|
+
"group-has-[[data-slot=input-group-control]:disabled]/input-group:opacity-50",
|
|
45
|
+
],
|
|
41
46
|
{
|
|
42
47
|
variants: {
|
|
43
48
|
align: {
|
|
@@ -72,7 +77,9 @@ function InputGroupAddon({
|
|
|
72
77
|
if ((e.target as HTMLElement).closest("button")) {
|
|
73
78
|
return
|
|
74
79
|
}
|
|
75
|
-
e.currentTarget.parentElement
|
|
80
|
+
e.currentTarget.parentElement
|
|
81
|
+
?.querySelector<HTMLElement>("input, textarea")
|
|
82
|
+
?.focus()
|
|
76
83
|
}}
|
|
77
84
|
{...props}
|
|
78
85
|
/>
|
|
@@ -49,7 +49,7 @@ function InputOTPSlot({
|
|
|
49
49
|
data-slot="input-otp-slot"
|
|
50
50
|
data-active={isActive}
|
|
51
51
|
className={cn(
|
|
52
|
-
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
|
|
52
|
+
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input bg-muted/30 text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
|
|
53
53
|
className
|
|
54
54
|
)}
|
|
55
55
|
{...props}
|
|
@@ -15,7 +15,7 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function KbdGroup({ className, ...props }: React.ComponentProps<"
|
|
18
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
19
19
|
return (
|
|
20
20
|
<kbd
|
|
21
21
|
data-slot="kbd-group"
|
|
@@ -12,6 +12,9 @@ function Label({
|
|
|
12
12
|
data-slot="label"
|
|
13
13
|
className={cn(
|
|
14
14
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
15
|
+
// O Field declara o grupo nomeado `group/field`: sem o sufixo, o Label não enxerga o
|
|
16
|
+
// `data-disabled` dele e continua aceso ao lado de um controle desabilitado.
|
|
17
|
+
"group-data-[disabled=true]/field:pointer-events-none group-data-[disabled=true]/field:opacity-50",
|
|
15
18
|
className
|
|
16
19
|
)}
|
|
17
20
|
{...props}
|
|
@@ -12,6 +12,11 @@ function Menu({
|
|
|
12
12
|
return <MenuPrimitive.Root data-slot="menu" {...props} />
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated `MenuContent` já renderiza o painel num portal próprio, então envolvê-lo com
|
|
17
|
+
* `MenuPortal` não muda onde o painel aparece. Use `MenuContent` diretamente. Sai do pacote numa
|
|
18
|
+
* próxima versão major.
|
|
19
|
+
*/
|
|
15
20
|
function MenuPortal({
|
|
16
21
|
...props
|
|
17
22
|
}: React.ComponentProps<typeof MenuPrimitive.Portal>) {
|
|
@@ -42,7 +42,25 @@ interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'c
|
|
|
42
42
|
secondaryMetric?: MetricCardSecondaryMetric
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type MetricCardActivationProps =
|
|
46
|
+
| {
|
|
47
|
+
/** Indica se a medida controla o recorte atualmente aplicado. */
|
|
48
|
+
active: boolean
|
|
49
|
+
/** Ativa a medida como recorte ou seleção. */
|
|
50
|
+
onActivate: () => void
|
|
51
|
+
/** Nome acessível da ativação, como “Filtrar por Agendados”. */
|
|
52
|
+
activationLabel: string
|
|
53
|
+
/** Use `onActivate`; o clique genérico conflita com os controles internos do card. */
|
|
54
|
+
onClick?: never
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
active?: never
|
|
58
|
+
onActivate?: never
|
|
59
|
+
activationLabel?: never
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
export type MetricCardProps = MetricCardBaseProps &
|
|
63
|
+
MetricCardActivationProps &
|
|
46
64
|
(
|
|
47
65
|
| {
|
|
48
66
|
/** Nome curto da medida apresentada. */
|
|
@@ -75,6 +93,9 @@ export function MetricCard({
|
|
|
75
93
|
tone,
|
|
76
94
|
action,
|
|
77
95
|
secondaryMetric,
|
|
96
|
+
active,
|
|
97
|
+
onActivate,
|
|
98
|
+
activationLabel,
|
|
78
99
|
loading = false,
|
|
79
100
|
className,
|
|
80
101
|
...props
|
|
@@ -84,6 +105,7 @@ export function MetricCard({
|
|
|
84
105
|
}
|
|
85
106
|
const hasIcon = icon !== undefined && icon !== null
|
|
86
107
|
const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
|
|
108
|
+
const interactive = onActivate !== undefined
|
|
87
109
|
|
|
88
110
|
const primaryCard = (
|
|
89
111
|
<Card
|
|
@@ -92,9 +114,9 @@ export function MetricCard({
|
|
|
92
114
|
className={cn(
|
|
93
115
|
'min-w-0 p-4',
|
|
94
116
|
secondaryMetric !== undefined && 'relative z-10',
|
|
95
|
-
secondaryMetric === undefined && className,
|
|
117
|
+
secondaryMetric === undefined && !interactive && className,
|
|
96
118
|
)}
|
|
97
|
-
{...(secondaryMetric === undefined ? props : {})}
|
|
119
|
+
{...(secondaryMetric === undefined && !interactive ? props : {})}
|
|
98
120
|
>
|
|
99
121
|
{loading ? (
|
|
100
122
|
<div className="space-y-3">
|
|
@@ -106,7 +128,10 @@ export function MetricCard({
|
|
|
106
128
|
<>
|
|
107
129
|
<div className="flex items-start justify-between gap-3">
|
|
108
130
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
109
|
-
<div
|
|
131
|
+
<div
|
|
132
|
+
data-slot="metric-card-label"
|
|
133
|
+
className="min-w-0 truncate text-sm font-medium text-muted-foreground"
|
|
134
|
+
>
|
|
110
135
|
{label}
|
|
111
136
|
</div>
|
|
112
137
|
{labelAction !== undefined && (
|
|
@@ -129,7 +154,10 @@ export function MetricCard({
|
|
|
129
154
|
</span>
|
|
130
155
|
)}
|
|
131
156
|
</div>
|
|
132
|
-
<div
|
|
157
|
+
<div
|
|
158
|
+
data-slot="metric-card-value"
|
|
159
|
+
className="mt-3 text-2xl font-semibold tracking-tight tabular-nums"
|
|
160
|
+
>
|
|
133
161
|
{value}
|
|
134
162
|
</div>
|
|
135
163
|
{description !== undefined && (
|
|
@@ -147,46 +175,77 @@ export function MetricCard({
|
|
|
147
175
|
</Card>
|
|
148
176
|
)
|
|
149
177
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{primaryCard}
|
|
160
|
-
<Card
|
|
161
|
-
data-slot="metric-card-secondary"
|
|
162
|
-
className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
|
|
178
|
+
const metric =
|
|
179
|
+
secondaryMetric === undefined ? (
|
|
180
|
+
primaryCard
|
|
181
|
+
) : (
|
|
182
|
+
<div
|
|
183
|
+
{...(!interactive ? props : {})}
|
|
184
|
+
data-slot="metric-card-stack"
|
|
185
|
+
aria-busy={loading || undefined}
|
|
186
|
+
className={cn('relative isolate min-w-0', !interactive && className)}
|
|
163
187
|
>
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
</div>
|
|
174
|
-
<div className="flex min-w-0 items-center gap-1.5">
|
|
188
|
+
{primaryCard}
|
|
189
|
+
<Card
|
|
190
|
+
data-slot="metric-card-secondary"
|
|
191
|
+
className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
|
|
192
|
+
>
|
|
193
|
+
{loading ? (
|
|
194
|
+
<Skeleton className="h-5 w-full" />
|
|
195
|
+
) : (
|
|
196
|
+
<>
|
|
175
197
|
<div
|
|
176
|
-
data-slot="metric-card-secondary-
|
|
177
|
-
className="
|
|
198
|
+
data-slot="metric-card-secondary-value"
|
|
199
|
+
className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums"
|
|
178
200
|
>
|
|
179
|
-
{secondaryMetric.
|
|
201
|
+
{secondaryMetric.value}
|
|
180
202
|
</div>
|
|
181
|
-
|
|
182
|
-
<div
|
|
183
|
-
|
|
203
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
204
|
+
<div
|
|
205
|
+
data-slot="metric-card-secondary-label"
|
|
206
|
+
className="min-w-0 truncate text-xs font-medium text-muted-foreground"
|
|
207
|
+
>
|
|
208
|
+
{secondaryMetric.label}
|
|
184
209
|
</div>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
{secondaryMetric.labelAction !== undefined && (
|
|
211
|
+
<div data-slot="metric-card-secondary-label-action" className="shrink-0">
|
|
212
|
+
{secondaryMetric.labelAction}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
</Card>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if (!interactive) return metric
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
{...props}
|
|
227
|
+
data-slot="metric-card-activation"
|
|
228
|
+
data-active={active}
|
|
229
|
+
className={cn(
|
|
230
|
+
// Ativa = a borda na cor primária, e só. Anel somaria uma segunda linha de
|
|
231
|
+
// outra cor à borda e, pintado para fora, sumiria no primeiro ancestral com
|
|
232
|
+
// `overflow-hidden`.
|
|
233
|
+
'relative isolate min-w-0 data-[active=true]:[&_[data-slot=metric-card]]:border-primary',
|
|
234
|
+
className,
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
data-slot="metric-card-activation-control"
|
|
240
|
+
className="peer absolute inset-0 z-0 cursor-pointer rounded-xl outline-none"
|
|
241
|
+
aria-label={activationLabel}
|
|
242
|
+
aria-pressed={active}
|
|
243
|
+
disabled={loading}
|
|
244
|
+
onClick={onActivate}
|
|
245
|
+
/>
|
|
246
|
+
<div className="pointer-events-none relative z-10 h-full [&>[data-slot=metric-card]]:h-full [&_[data-slot=metric-card]]:transition-colors [&_[data-slot=metric-card-action]]:pointer-events-auto [&_[data-slot=metric-card-label-action]]:pointer-events-auto [&_[data-slot=metric-card-secondary-label-action]]:pointer-events-auto peer-hover:[&_[data-slot=metric-card]]:border-foreground/40 peer-focus-visible:[&_[data-slot=metric-card]]:ring-2 peer-focus-visible:[&_[data-slot=metric-card]]:ring-inset peer-focus-visible:[&_[data-slot=metric-card]]:ring-ring">
|
|
247
|
+
{metric}
|
|
248
|
+
</div>
|
|
190
249
|
</div>
|
|
191
250
|
)
|
|
192
251
|
}
|
|
@@ -59,7 +59,7 @@ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function PopoverTitle({ className, ...props }: React.ComponentProps<"
|
|
62
|
+
function PopoverTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
63
63
|
return (
|
|
64
64
|
<div
|
|
65
65
|
data-slot="popover-title"
|
|
@@ -32,13 +32,21 @@ export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimi
|
|
|
32
32
|
function Progress({
|
|
33
33
|
className,
|
|
34
34
|
value,
|
|
35
|
+
max = 100,
|
|
35
36
|
context = 'primary',
|
|
36
37
|
...props
|
|
37
38
|
}: ProgressProps) {
|
|
39
|
+
// A largura é a fração de `max`, não de 100: `value={3} max={4}` enche três quartos. Um `max`
|
|
40
|
+
// não positivo cai em 100, como o Radix faz no Root.
|
|
41
|
+
const scale = max > 0 ? max : 100
|
|
42
|
+
const percent = ((value ?? 0) / scale) * 100
|
|
38
43
|
return (
|
|
39
44
|
<ProgressPrimitive.Root
|
|
40
45
|
data-slot="progress"
|
|
41
46
|
data-context={context}
|
|
47
|
+
// O Root precisa do `value` para anunciar o andamento (aria-valuenow, data-state).
|
|
48
|
+
value={value}
|
|
49
|
+
max={max}
|
|
42
50
|
className={cn(
|
|
43
51
|
"relative h-2 w-full overflow-hidden rounded-full",
|
|
44
52
|
trackClasses[context],
|
|
@@ -49,7 +57,7 @@ function Progress({
|
|
|
49
57
|
<ProgressPrimitive.Indicator
|
|
50
58
|
data-slot="progress-indicator"
|
|
51
59
|
className={cn("h-full w-full flex-1 transition-all", indicatorClasses[context])}
|
|
52
|
-
style={{ transform: `translateX(-${100 -
|
|
60
|
+
style={{ transform: `translateX(-${100 - percent}%)` }}
|
|
53
61
|
/>
|
|
54
62
|
</ProgressPrimitive.Root>
|
|
55
63
|
)
|