@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +51 -18
  2. package/bin/lib/check.mjs +98 -15
  3. package/bin/lib/copy.mjs +811 -314
  4. package/bin/lib/gen-manifest.mjs +24 -23
  5. package/bin/lib/gen-runner.mjs +188 -148
  6. package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
  8. package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
  9. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
  10. package/docs/code-style.md +24 -19
  11. package/package.json +5 -1
  12. package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
  13. package/src/core/presentation.ts +512 -0
  14. package/src/presentation/index.ts +1 -0
  15. package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
  16. package/src/ui/components/patterns/confirm.tsx +34 -29
  17. package/src/ui/components/patterns/form-dialog.tsx +20 -8
  18. package/src/ui/components/patterns/list.tsx +26 -9
  19. package/src/ui/components/patterns/page.tsx +99 -139
  20. package/src/ui/components/patterns/presentation.tsx +316 -0
  21. package/src/ui/components/patterns/sidebar.tsx +3 -3
  22. package/src/ui/components/patterns/trigger.tsx +38 -17
  23. package/src/ui/components/primitives/button-group.tsx +53 -43
  24. package/src/ui/components/primitives/command.tsx +30 -72
  25. package/src/ui/components/primitives/dialog.tsx +23 -89
  26. package/src/ui/components/primitives/drawer.tsx +8 -34
  27. package/src/ui/docs/content/action-form-dialog.md +12 -10
  28. package/src/ui/docs/content/action-list-dialog.md +22 -17
  29. package/src/ui/docs/content/button.md +52 -35
  30. package/src/ui/docs/content/communication.md +26 -26
  31. package/src/ui/docs/content/dialog.md +173 -154
  32. package/src/ui/docs/content/drawer.md +12 -11
  33. package/src/ui/docs/content/page.md +72 -91
  34. package/src/ui/docs/content/presentation.md +158 -0
  35. package/src/ui/docs/registry.tsx +6 -0
  36. package/src/ui/meta.ts +8 -2
  37. 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-14" : "w-64",
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-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40",
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-2", className)}
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
- description?: string
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(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível concluir.')))
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 ? { description: msgText(spec.message, '') } : {}),
115
- ...(spec.confirmLabel !== undefined ? { actionLabel: msgText(spec.confirmLabel, buttonLabel) } : {}),
116
- ...(spec.cancelLabel !== undefined ? { cancelLabel: msgText(spec.cancelLabel, 'Cancelar') } : {}),
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 = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
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 data-action={action.name}>
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 context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
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 '../../lib/cn.ts'
5
- import { Separator } from './separator.tsx'
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: 'flex-col',
12
+ horizontal: "",
13
+ vertical: "flex-col",
14
14
  },
15
15
  mode: {
16
- connected: '',
17
- spaced: 'gap-1',
16
+ connected: "",
17
+ spaced: "gap-1",
18
18
  },
19
19
  shape: {
20
- default: '',
21
- pill: 'rounded-full',
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: 'horizontal',
27
- mode: 'connected',
31
+ orientation: "horizontal",
32
+ mode: "connected",
28
33
  className:
29
- '[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
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: 'vertical',
33
- mode: 'connected',
37
+ orientation: "vertical",
38
+ mode: "connected",
34
39
  className:
35
- '[&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
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: 'horizontal',
39
- mode: 'connected',
40
- shape: 'pill',
41
- className: '[&>*: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',
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: 'vertical',
45
- mode: 'connected',
46
- shape: 'pill',
47
- className: '[&>*: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',
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: 'spaced',
51
- shape: 'pill',
57
+ mode: "spaced",
58
+ shape: "pill",
52
59
  className:
53
- '[&>*]:rounded-full [&>[data-slot=select]_[data-slot=select-control]]:rounded-full [&>[data-slot=select-wrapper]_[data-slot=select]]:rounded-full',
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: 'connected',
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 !== 'spaced' &&
81
- (orientation === 'vertical'
82
- ? '[&>[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'
83
- : '[&>[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'),
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
+ };