@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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { useState, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { Braces, X } from "lucide-react";
|
|
3
|
+
import type {
|
|
4
|
+
PresentationDiagnostic,
|
|
5
|
+
PresentationDefinition,
|
|
6
|
+
PresentationInvocation,
|
|
7
|
+
PresentationSurface,
|
|
8
|
+
} from "../../../core/presentation.ts";
|
|
9
|
+
import { createPresentationInspectionSnapshot } from "../../../core/presentation.ts";
|
|
10
|
+
import { cn } from "../../lib/cn.ts";
|
|
11
|
+
import { Button, buttonVariants } from "../primitives/button.tsx";
|
|
12
|
+
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
13
|
+
import { Copyable } from "../primitives/copyable.tsx";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogBody,
|
|
17
|
+
DialogClose,
|
|
18
|
+
DialogContent,
|
|
19
|
+
DialogFooter,
|
|
20
|
+
DialogHeader,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
} from "../primitives/dialog.tsx";
|
|
23
|
+
import {
|
|
24
|
+
Drawer,
|
|
25
|
+
DrawerBody,
|
|
26
|
+
DrawerClose,
|
|
27
|
+
DrawerContent,
|
|
28
|
+
DrawerFooter,
|
|
29
|
+
DrawerHeader,
|
|
30
|
+
DrawerTitle,
|
|
31
|
+
} from "../primitives/drawer.tsx";
|
|
32
|
+
import {
|
|
33
|
+
Page,
|
|
34
|
+
PageActions,
|
|
35
|
+
PageBody,
|
|
36
|
+
PageFooter,
|
|
37
|
+
PageHeader,
|
|
38
|
+
PageIntro,
|
|
39
|
+
PageNavigation,
|
|
40
|
+
PageTitle,
|
|
41
|
+
useInsidePageShell,
|
|
42
|
+
} from "./page.tsx";
|
|
43
|
+
|
|
44
|
+
interface PresentationBaseProps {
|
|
45
|
+
surface: PresentationSurface;
|
|
46
|
+
title: ReactNode;
|
|
47
|
+
navigation?: ReactNode;
|
|
48
|
+
headerActions?: ReactNode;
|
|
49
|
+
footerActions?: ReactNode;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
className?: string;
|
|
52
|
+
bodyClassName?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PresentationProps = PresentationBaseProps &
|
|
56
|
+
(
|
|
57
|
+
| { open?: undefined; onOpenChange?: (open: boolean) => void }
|
|
58
|
+
| { open: boolean; onOpenChange: (open: boolean) => void }
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const bodyClassName =
|
|
62
|
+
"h-full min-h-full [&>[data-slot=data-state]]:flex [&>[data-slot=data-state]]:min-h-full [&>[data-slot=data-state]]:items-center [&>[data-slot=data-state]]:justify-center";
|
|
63
|
+
|
|
64
|
+
function PresentationActions({
|
|
65
|
+
children,
|
|
66
|
+
equal = false,
|
|
67
|
+
}: {
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
equal?: boolean;
|
|
70
|
+
}) {
|
|
71
|
+
return (
|
|
72
|
+
<ButtonGroup mode="spaced" distribution={equal ? "equal" : "content"}>
|
|
73
|
+
{children}
|
|
74
|
+
</ButtonGroup>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function CloseAction({
|
|
79
|
+
surface,
|
|
80
|
+
}: {
|
|
81
|
+
surface: "dialog" | "drawer";
|
|
82
|
+
}): ReactElement {
|
|
83
|
+
const button = (
|
|
84
|
+
<Button size="icon-sm" variant="ghost" aria-label="Fechar">
|
|
85
|
+
<X aria-hidden />
|
|
86
|
+
</Button>
|
|
87
|
+
);
|
|
88
|
+
return surface === "dialog" ? (
|
|
89
|
+
<DialogClose asChild>{button}</DialogClose>
|
|
90
|
+
) : (
|
|
91
|
+
<DrawerClose asChild>{button}</DrawerClose>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A mesma Presentation em Page, Dialog ou Drawer; a superfície não redefine o recurso. */
|
|
96
|
+
export function Presentation({
|
|
97
|
+
surface,
|
|
98
|
+
title,
|
|
99
|
+
navigation,
|
|
100
|
+
headerActions,
|
|
101
|
+
footerActions,
|
|
102
|
+
children,
|
|
103
|
+
open,
|
|
104
|
+
onOpenChange,
|
|
105
|
+
className,
|
|
106
|
+
bodyClassName: bodyClassNameProp,
|
|
107
|
+
}: PresentationProps): ReactElement {
|
|
108
|
+
const insidePageShell = useInsidePageShell();
|
|
109
|
+
const [internalOpen, setInternalOpen] = useState(true);
|
|
110
|
+
const modalOpen = open ?? internalOpen;
|
|
111
|
+
function handleOpenChange(nextOpen: boolean): void {
|
|
112
|
+
if (open === undefined) setInternalOpen(nextOpen);
|
|
113
|
+
onOpenChange?.(nextOpen);
|
|
114
|
+
}
|
|
115
|
+
const body = (
|
|
116
|
+
<div
|
|
117
|
+
data-slot="presentation-body"
|
|
118
|
+
className={cn(bodyClassName, bodyClassNameProp)}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (surface === "page") {
|
|
125
|
+
if (insidePageShell) {
|
|
126
|
+
return (
|
|
127
|
+
<Page className={cn("flex min-h-full flex-col", className)}>
|
|
128
|
+
<PageIntro>
|
|
129
|
+
{navigation === undefined ? null : (
|
|
130
|
+
<PageNavigation>{navigation}</PageNavigation>
|
|
131
|
+
)}
|
|
132
|
+
<PageTitle>{title}</PageTitle>
|
|
133
|
+
{headerActions === undefined ? null : (
|
|
134
|
+
<PageActions>
|
|
135
|
+
<PresentationActions>{headerActions}</PresentationActions>
|
|
136
|
+
</PageActions>
|
|
137
|
+
)}
|
|
138
|
+
</PageIntro>
|
|
139
|
+
<PageBody className="min-h-0 flex-1 overflow-y-auto">{body}</PageBody>
|
|
140
|
+
{footerActions === undefined ? null : (
|
|
141
|
+
<PageFooter>
|
|
142
|
+
<PresentationActions>{footerActions}</PresentationActions>
|
|
143
|
+
</PageFooter>
|
|
144
|
+
)}
|
|
145
|
+
</Page>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
<Page
|
|
150
|
+
className={cn(
|
|
151
|
+
"flex min-h-full max-w-none flex-col space-y-0 px-0 py-0",
|
|
152
|
+
className,
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<PageHeader className="border-b px-3 py-4">
|
|
156
|
+
{navigation === undefined ? null : (
|
|
157
|
+
<PageNavigation>{navigation}</PageNavigation>
|
|
158
|
+
)}
|
|
159
|
+
<PageTitle className="text-lg leading-none">{title}</PageTitle>
|
|
160
|
+
{headerActions === undefined ? null : (
|
|
161
|
+
<PageActions>
|
|
162
|
+
<PresentationActions>{headerActions}</PresentationActions>
|
|
163
|
+
</PageActions>
|
|
164
|
+
)}
|
|
165
|
+
</PageHeader>
|
|
166
|
+
<PageBody className="min-h-0 flex-1 overflow-y-auto px-8 py-8">
|
|
167
|
+
{body}
|
|
168
|
+
</PageBody>
|
|
169
|
+
{footerActions === undefined ? null : (
|
|
170
|
+
<PageFooter>
|
|
171
|
+
<PresentationActions>{footerActions}</PresentationActions>
|
|
172
|
+
</PageFooter>
|
|
173
|
+
)}
|
|
174
|
+
</Page>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (surface === "dialog") {
|
|
179
|
+
return (
|
|
180
|
+
<Dialog open={modalOpen} onOpenChange={handleOpenChange}>
|
|
181
|
+
<DialogContent
|
|
182
|
+
className={className}
|
|
183
|
+
showCloseButton={false}
|
|
184
|
+
aria-describedby={undefined}
|
|
185
|
+
>
|
|
186
|
+
<DialogHeader>
|
|
187
|
+
<div className="flex items-center justify-between gap-4">
|
|
188
|
+
{navigation}
|
|
189
|
+
<DialogTitle className="min-w-0 flex-1 truncate">
|
|
190
|
+
{title}
|
|
191
|
+
</DialogTitle>
|
|
192
|
+
<PresentationActions>
|
|
193
|
+
{headerActions}
|
|
194
|
+
<CloseAction surface="dialog" />
|
|
195
|
+
</PresentationActions>
|
|
196
|
+
</div>
|
|
197
|
+
</DialogHeader>
|
|
198
|
+
<DialogBody className={bodyClassNameProp}>{body}</DialogBody>
|
|
199
|
+
{footerActions === undefined ? null : (
|
|
200
|
+
<DialogFooter>
|
|
201
|
+
<PresentationActions equal>{footerActions}</PresentationActions>
|
|
202
|
+
</DialogFooter>
|
|
203
|
+
)}
|
|
204
|
+
</DialogContent>
|
|
205
|
+
</Dialog>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Drawer open={modalOpen} onOpenChange={handleOpenChange}>
|
|
211
|
+
<DrawerContent
|
|
212
|
+
className={className}
|
|
213
|
+
showCloseButton={false}
|
|
214
|
+
aria-describedby={undefined}
|
|
215
|
+
>
|
|
216
|
+
<DrawerHeader>
|
|
217
|
+
<div className="flex items-center justify-between gap-4">
|
|
218
|
+
{navigation}
|
|
219
|
+
<DrawerTitle className="min-w-0 flex-1 truncate">
|
|
220
|
+
{title}
|
|
221
|
+
</DrawerTitle>
|
|
222
|
+
<PresentationActions>
|
|
223
|
+
{headerActions}
|
|
224
|
+
<CloseAction surface="drawer" />
|
|
225
|
+
</PresentationActions>
|
|
226
|
+
</div>
|
|
227
|
+
</DrawerHeader>
|
|
228
|
+
<DrawerBody className={bodyClassNameProp}>{body}</DrawerBody>
|
|
229
|
+
{footerActions === undefined ? null : (
|
|
230
|
+
<DrawerFooter>
|
|
231
|
+
<PresentationActions equal>{footerActions}</PresentationActions>
|
|
232
|
+
</DrawerFooter>
|
|
233
|
+
)}
|
|
234
|
+
</DrawerContent>
|
|
235
|
+
</Drawer>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface PresentationInspectorProps {
|
|
240
|
+
definition: PresentationDefinition | unknown;
|
|
241
|
+
invocation?: PresentationInvocation | unknown;
|
|
242
|
+
resolved?: unknown;
|
|
243
|
+
diagnostics?: readonly PresentationDiagnostic[];
|
|
244
|
+
triggerLabel?: string;
|
|
245
|
+
title?: string;
|
|
246
|
+
/** Apresenta o gatilho como FAB; o consumidor continua responsável por posicioná-lo. */
|
|
247
|
+
floating?: boolean;
|
|
248
|
+
className?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Ferramenta de desenvolvimento para conferir e copiar o artefato consumido pelo renderer. */
|
|
252
|
+
export function PresentationInspector({
|
|
253
|
+
definition,
|
|
254
|
+
invocation,
|
|
255
|
+
resolved,
|
|
256
|
+
diagnostics,
|
|
257
|
+
triggerLabel = "Ver JSON da Presentation",
|
|
258
|
+
title = "JSON da Presentation",
|
|
259
|
+
floating = false,
|
|
260
|
+
className,
|
|
261
|
+
}: PresentationInspectorProps): ReactElement {
|
|
262
|
+
const [open, setOpen] = useState(false);
|
|
263
|
+
const [json, setJson] = useState<string | null>(null);
|
|
264
|
+
|
|
265
|
+
function inspect(): void {
|
|
266
|
+
const snapshot = createPresentationInspectionSnapshot({
|
|
267
|
+
definition,
|
|
268
|
+
invocation,
|
|
269
|
+
resolved,
|
|
270
|
+
diagnostics,
|
|
271
|
+
});
|
|
272
|
+
setJson(JSON.stringify(snapshot, null, 2) ?? "null");
|
|
273
|
+
setOpen(true);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleOpenChange(nextOpen: boolean): void {
|
|
277
|
+
setOpen(nextOpen);
|
|
278
|
+
if (!nextOpen) setJson(null);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<>
|
|
283
|
+
<Button
|
|
284
|
+
size={floating ? "icon" : "icon-sm"}
|
|
285
|
+
variant={floating ? "outline" : "ghost"}
|
|
286
|
+
data-appearance={floating ? "fab" : "inline"}
|
|
287
|
+
className={cn(floating && "rounded-full shadow-md", className)}
|
|
288
|
+
aria-label={triggerLabel}
|
|
289
|
+
title={triggerLabel}
|
|
290
|
+
onClick={inspect}
|
|
291
|
+
>
|
|
292
|
+
<Braces aria-hidden />
|
|
293
|
+
</Button>
|
|
294
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
295
|
+
<DialogContent className="sm:max-w-3xl" aria-describedby={undefined}>
|
|
296
|
+
<DialogHeader>
|
|
297
|
+
<DialogTitle>{title}</DialogTitle>
|
|
298
|
+
</DialogHeader>
|
|
299
|
+
<DialogBody>
|
|
300
|
+
<pre className="max-h-[65vh] overflow-auto rounded-lg bg-muted p-4 text-xs leading-relaxed">
|
|
301
|
+
{json ?? ""}
|
|
302
|
+
</pre>
|
|
303
|
+
</DialogBody>
|
|
304
|
+
<DialogFooter>
|
|
305
|
+
<Copyable
|
|
306
|
+
value={json ?? ""}
|
|
307
|
+
className={buttonVariants({ variant: "outline" })}
|
|
308
|
+
>
|
|
309
|
+
Copiar JSON
|
|
310
|
+
</Copyable>
|
|
311
|
+
</DialogFooter>
|
|
312
|
+
</DialogContent>
|
|
313
|
+
</Dialog>
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -48,7 +48,7 @@ export function Sidebar({
|
|
|
48
48
|
className={cn(
|
|
49
49
|
"group/sidebar flex h-full min-h-0 shrink-0 flex-col transition-[width] duration-200 ease-out",
|
|
50
50
|
divider && "border-r border-border",
|
|
51
|
-
collapsed ? "w-
|
|
51
|
+
collapsed ? "w-16" : "w-64",
|
|
52
52
|
className,
|
|
53
53
|
)}
|
|
54
54
|
>
|
|
@@ -182,7 +182,7 @@ export function SidebarItem({
|
|
|
182
182
|
aria-current={active ? "page" : undefined}
|
|
183
183
|
onClick={onClick}
|
|
184
184
|
className={cn(
|
|
185
|
-
"mx-auto grid size-
|
|
185
|
+
"mx-auto grid size-10 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40",
|
|
186
186
|
"outline-none",
|
|
187
187
|
focusRing,
|
|
188
188
|
active
|
|
@@ -423,7 +423,7 @@ export function SidebarNav({
|
|
|
423
423
|
<nav
|
|
424
424
|
aria-label={navLabel}
|
|
425
425
|
data-slot="sidebar-nav"
|
|
426
|
-
className={cn("space-y-4 p-
|
|
426
|
+
className={cn("space-y-4 p-3", className)}
|
|
427
427
|
>
|
|
428
428
|
{groups.map((group, gi) =>
|
|
429
429
|
!groupFilled(group) ? null : (
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ReactNode } from 'react'
|
|
16
|
-
import { useState } from 'react'
|
|
16
|
+
import { useId, useState } from 'react'
|
|
17
17
|
import type { SimpleContract } from '../../../core/index.ts'
|
|
18
18
|
import { useTriggerAction } from '../../drivers/react.tsx'
|
|
19
19
|
import { toast } from '../primitives/sonner.tsx'
|
|
@@ -23,9 +23,9 @@ import { Button, type ButtonProps } from '../primitives/button.tsx'
|
|
|
23
23
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
24
24
|
import {
|
|
25
25
|
Dialog,
|
|
26
|
+
DialogBody,
|
|
26
27
|
DialogClose,
|
|
27
28
|
DialogContent,
|
|
28
|
-
DialogDescription,
|
|
29
29
|
DialogFooter,
|
|
30
30
|
DialogHeader,
|
|
31
31
|
DialogTitle,
|
|
@@ -59,7 +59,8 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
59
59
|
* contrato — declarativo). Sem nenhum dos dois, dispara direto. */
|
|
60
60
|
confirm?: {
|
|
61
61
|
title: string
|
|
62
|
-
|
|
62
|
+
/** Conteúdo relevante apresentado antes das ações. */
|
|
63
|
+
body?: ReactNode
|
|
63
64
|
actionLabel?: string
|
|
64
65
|
cancelLabel?: string
|
|
65
66
|
}
|
|
@@ -88,6 +89,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
88
89
|
itemLabel,
|
|
89
90
|
className,
|
|
90
91
|
}: ActionTriggerProps<TInput, TData>) {
|
|
92
|
+
const confirmBodyId = useId()
|
|
91
93
|
const [open, setOpen] = useState(false)
|
|
92
94
|
const { trigger, isLoading } = useTriggerAction(action, {
|
|
93
95
|
onSuccess: (data) => {
|
|
@@ -98,7 +100,9 @@ export function ActionTrigger<TInput, TData>({
|
|
|
98
100
|
onError: (err) => {
|
|
99
101
|
// A frase de negócio do servidor vence o rótulo do contrato (ver lib/action-errors.ts);
|
|
100
102
|
// fora da allowlist, vale o rótulo — nunca o texto técnico cru.
|
|
101
|
-
toast.error(
|
|
103
|
+
toast.error(
|
|
104
|
+
humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível concluir.')),
|
|
105
|
+
)
|
|
102
106
|
},
|
|
103
107
|
})
|
|
104
108
|
|
|
@@ -111,9 +115,13 @@ export function ActionTrigger<TInput, TData>({
|
|
|
111
115
|
(spec !== undefined
|
|
112
116
|
? {
|
|
113
117
|
title: msgText(spec.title, 'Confirmar?'),
|
|
114
|
-
...(spec.message !== undefined ? {
|
|
115
|
-
...(spec.confirmLabel !== undefined
|
|
116
|
-
|
|
118
|
+
...(spec.message !== undefined ? { body: msgText(spec.message, '') } : {}),
|
|
119
|
+
...(spec.confirmLabel !== undefined
|
|
120
|
+
? { actionLabel: msgText(spec.confirmLabel, buttonLabel) }
|
|
121
|
+
: {}),
|
|
122
|
+
...(spec.cancelLabel !== undefined
|
|
123
|
+
? { cancelLabel: msgText(spec.cancelLabel, 'Cancelar') }
|
|
124
|
+
: {}),
|
|
117
125
|
}
|
|
118
126
|
: undefined)
|
|
119
127
|
const destructive = spec?.destructive === true
|
|
@@ -126,7 +134,8 @@ export function ActionTrigger<TInput, TData>({
|
|
|
126
134
|
* botão é sempre `solid` — não herda o `variant` do gatilho, que descreve o item, não a
|
|
127
135
|
* decisão.
|
|
128
136
|
*/
|
|
129
|
-
const triggerContext =
|
|
137
|
+
const triggerContext =
|
|
138
|
+
context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
|
|
130
139
|
const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
|
|
131
140
|
const confirmContext = destructive ? 'danger' : (context ?? 'primary')
|
|
132
141
|
const confirmVariant = 'solid'
|
|
@@ -173,24 +182,36 @@ export function ActionTrigger<TInput, TData>({
|
|
|
173
182
|
{renderButton(() => setOpen(true))}
|
|
174
183
|
|
|
175
184
|
<Dialog mode="alert" open={open} onOpenChange={setOpen}>
|
|
176
|
-
<DialogContent
|
|
185
|
+
<DialogContent
|
|
186
|
+
data-action={action.name}
|
|
187
|
+
aria-describedby={
|
|
188
|
+
itemLabel !== undefined || confirm.body !== undefined ? confirmBodyId : undefined
|
|
189
|
+
}
|
|
190
|
+
>
|
|
177
191
|
<DialogHeader>
|
|
178
192
|
<DialogTitle>{confirm.title}</DialogTitle>
|
|
179
|
-
{(itemLabel !== undefined || confirm.description !== undefined) && (
|
|
180
|
-
<DialogDescription>
|
|
181
|
-
{itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
|
|
182
|
-
{itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
|
|
183
|
-
{confirm.description}
|
|
184
|
-
</DialogDescription>
|
|
185
|
-
)}
|
|
186
193
|
</DialogHeader>
|
|
194
|
+
{(itemLabel !== undefined || confirm.body !== undefined) && (
|
|
195
|
+
<DialogBody id={confirmBodyId} className="text-sm text-muted-foreground">
|
|
196
|
+
{itemLabel !== undefined && (
|
|
197
|
+
<span className="font-medium text-foreground">“{itemLabel}”</span>
|
|
198
|
+
)}
|
|
199
|
+
{itemLabel !== undefined && confirm.body !== undefined ? ' — ' : ''}
|
|
200
|
+
{confirm.body}
|
|
201
|
+
</DialogBody>
|
|
202
|
+
)}
|
|
187
203
|
<DialogFooter>
|
|
188
204
|
<DialogClose initialFocus asChild>
|
|
189
205
|
<Button context="neutral" variant="ghost" disabled={isLoading}>
|
|
190
206
|
{confirm.cancelLabel ?? 'Cancelar'}
|
|
191
207
|
</Button>
|
|
192
208
|
</DialogClose>
|
|
193
|
-
<Button
|
|
209
|
+
<Button
|
|
210
|
+
context={confirmContext}
|
|
211
|
+
variant={confirmVariant}
|
|
212
|
+
busy={isLoading}
|
|
213
|
+
onClick={fire}
|
|
214
|
+
>
|
|
194
215
|
{confirm.actionLabel ?? buttonLabel}
|
|
195
216
|
</Button>
|
|
196
217
|
</DialogFooter>
|
|
@@ -1,71 +1,80 @@
|
|
|
1
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
|
-
import { Slot } from "radix-ui"
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { Slot } from "radix-ui";
|
|
3
3
|
|
|
4
|
-
import { cn } from
|
|
5
|
-
import { Separator } from
|
|
4
|
+
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import { Separator } from "./separator.tsx";
|
|
6
6
|
|
|
7
7
|
const buttonGroupVariants = cva(
|
|
8
8
|
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
orientation: {
|
|
12
|
-
horizontal:
|
|
13
|
-
vertical:
|
|
12
|
+
horizontal: "",
|
|
13
|
+
vertical: "flex-col",
|
|
14
14
|
},
|
|
15
15
|
mode: {
|
|
16
|
-
connected:
|
|
17
|
-
spaced:
|
|
16
|
+
connected: "",
|
|
17
|
+
spaced: "gap-1",
|
|
18
18
|
},
|
|
19
19
|
shape: {
|
|
20
|
-
default:
|
|
21
|
-
pill:
|
|
20
|
+
default: "",
|
|
21
|
+
pill: "rounded-full",
|
|
22
|
+
},
|
|
23
|
+
distribution: {
|
|
24
|
+
content: "",
|
|
25
|
+
equal:
|
|
26
|
+
"w-full flex-col-reverse gap-2 sm:flex-row [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
|
|
22
27
|
},
|
|
23
28
|
},
|
|
24
29
|
compoundVariants: [
|
|
25
30
|
{
|
|
26
|
-
orientation:
|
|
27
|
-
mode:
|
|
31
|
+
orientation: "horizontal",
|
|
32
|
+
mode: "connected",
|
|
28
33
|
className:
|
|
29
|
-
|
|
34
|
+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
|
30
35
|
},
|
|
31
36
|
{
|
|
32
|
-
orientation:
|
|
33
|
-
mode:
|
|
37
|
+
orientation: "vertical",
|
|
38
|
+
mode: "connected",
|
|
34
39
|
className:
|
|
35
|
-
|
|
40
|
+
"[&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
36
41
|
},
|
|
37
42
|
{
|
|
38
|
-
orientation:
|
|
39
|
-
mode:
|
|
40
|
-
shape:
|
|
41
|
-
className:
|
|
43
|
+
orientation: "horizontal",
|
|
44
|
+
mode: "connected",
|
|
45
|
+
shape: "pill",
|
|
46
|
+
className:
|
|
47
|
+
"[&>*:first-child]:rounded-l-full [&>*:last-child]:rounded-r-full [&>[data-slot=select]:first-child_[data-slot=select-control]]:rounded-l-full [&>[data-slot=select]:last-child_[data-slot=select-control]]:rounded-r-full [&>[data-slot=select-wrapper]:first-child_[data-slot=select]]:rounded-l-full [&>[data-slot=select-wrapper]:last-child_[data-slot=select]]:rounded-r-full",
|
|
42
48
|
},
|
|
43
49
|
{
|
|
44
|
-
orientation:
|
|
45
|
-
mode:
|
|
46
|
-
shape:
|
|
47
|
-
className:
|
|
50
|
+
orientation: "vertical",
|
|
51
|
+
mode: "connected",
|
|
52
|
+
shape: "pill",
|
|
53
|
+
className:
|
|
54
|
+
"[&>*:first-child]:rounded-t-full [&>*:last-child]:rounded-b-full [&>[data-slot=select]:first-child_[data-slot=select-control]]:rounded-t-full [&>[data-slot=select]:last-child_[data-slot=select-control]]:rounded-b-full [&>[data-slot=select-wrapper]:first-child_[data-slot=select]]:rounded-t-full [&>[data-slot=select-wrapper]:last-child_[data-slot=select]]:rounded-b-full",
|
|
48
55
|
},
|
|
49
56
|
{
|
|
50
|
-
mode:
|
|
51
|
-
shape:
|
|
57
|
+
mode: "spaced",
|
|
58
|
+
shape: "pill",
|
|
52
59
|
className:
|
|
53
|
-
|
|
60
|
+
"[&>*]:rounded-full [&>[data-slot=select]_[data-slot=select-control]]:rounded-full [&>[data-slot=select-wrapper]_[data-slot=select]]:rounded-full",
|
|
54
61
|
},
|
|
55
62
|
],
|
|
56
63
|
defaultVariants: {
|
|
57
64
|
orientation: "horizontal",
|
|
58
|
-
mode:
|
|
65
|
+
mode: "connected",
|
|
59
66
|
shape: "default",
|
|
67
|
+
distribution: "content",
|
|
60
68
|
},
|
|
61
|
-
}
|
|
62
|
-
)
|
|
69
|
+
},
|
|
70
|
+
);
|
|
63
71
|
|
|
64
72
|
function ButtonGroup({
|
|
65
73
|
className,
|
|
66
74
|
orientation,
|
|
67
75
|
mode,
|
|
68
76
|
shape,
|
|
77
|
+
distribution,
|
|
69
78
|
...props
|
|
70
79
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
71
80
|
return (
|
|
@@ -75,17 +84,18 @@ function ButtonGroup({
|
|
|
75
84
|
data-orientation={orientation}
|
|
76
85
|
data-mode={mode}
|
|
77
86
|
data-shape={shape}
|
|
87
|
+
data-distribution={distribution}
|
|
78
88
|
className={cn(
|
|
79
|
-
buttonGroupVariants({ orientation, mode, shape }),
|
|
80
|
-
mode !==
|
|
81
|
-
(orientation ===
|
|
82
|
-
?
|
|
83
|
-
:
|
|
89
|
+
buttonGroupVariants({ orientation, mode, shape, distribution }),
|
|
90
|
+
mode !== "spaced" &&
|
|
91
|
+
(orientation === "vertical"
|
|
92
|
+
? "[&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:rounded-t-none [&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:border-t-0 [&>[data-slot=select]:not(:last-child)_[data-slot=select-control]]:rounded-b-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:rounded-t-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:border-t-0 [&>[data-slot=select-wrapper]:not(:last-child)_[data-slot=select]]:rounded-b-none"
|
|
93
|
+
: "[&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:rounded-l-none [&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:border-l-0 [&>[data-slot=select]:not(:last-child)_[data-slot=select-control]]:rounded-r-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:rounded-l-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:border-l-0 [&>[data-slot=select-wrapper]:not(:last-child)_[data-slot=select]]:rounded-r-none"),
|
|
84
94
|
className,
|
|
85
95
|
)}
|
|
86
96
|
{...props}
|
|
87
97
|
/>
|
|
88
|
-
)
|
|
98
|
+
);
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
function ButtonGroupText({
|
|
@@ -93,19 +103,19 @@ function ButtonGroupText({
|
|
|
93
103
|
asChild = false,
|
|
94
104
|
...props
|
|
95
105
|
}: React.ComponentProps<"div"> & {
|
|
96
|
-
asChild?: boolean
|
|
106
|
+
asChild?: boolean;
|
|
97
107
|
}) {
|
|
98
|
-
const Comp = asChild ? Slot.Root : "div"
|
|
108
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
99
109
|
|
|
100
110
|
return (
|
|
101
111
|
<Comp
|
|
102
112
|
className={cn(
|
|
103
113
|
"flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
104
|
-
className
|
|
114
|
+
className,
|
|
105
115
|
)}
|
|
106
116
|
{...props}
|
|
107
117
|
/>
|
|
108
|
-
)
|
|
118
|
+
);
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
function ButtonGroupSeparator({
|
|
@@ -119,11 +129,11 @@ function ButtonGroupSeparator({
|
|
|
119
129
|
orientation={orientation}
|
|
120
130
|
className={cn(
|
|
121
131
|
"relative m-0! self-stretch bg-input data-[orientation=vertical]:h-auto",
|
|
122
|
-
className
|
|
132
|
+
className,
|
|
123
133
|
)}
|
|
124
134
|
{...props}
|
|
125
135
|
/>
|
|
126
|
-
)
|
|
136
|
+
);
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
export {
|
|
@@ -131,4 +141,4 @@ export {
|
|
|
131
141
|
ButtonGroupSeparator,
|
|
132
142
|
ButtonGroupText,
|
|
133
143
|
buttonGroupVariants,
|
|
134
|
-
}
|
|
144
|
+
};
|