@spunto/design-system 0.2.1 → 0.4.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/README.md CHANGED
@@ -38,9 +38,71 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
38
38
  radius base is `0.25rem`; `Build`/`Ship`/`Run` are semantic (`bg-build`, `text-run`…).
39
39
  - **`cn`** — `clsx` + `tailwind-merge`.
40
40
  - **`./colors`** — `cssVar`, `chartColors`, `chartRamp`, `segColors` for JS/chart contexts.
41
- - **Primitives** — `Button` (+`buttonVariants`), `Card` (+parts), `Input`, `Textarea`,
42
- `Label`, `Badge` (+`badgeVariants`), `Switch`. Dependency-light (cva + clsx +
43
- tailwind-merge), no component framework required.
41
+ - **Primitives** — form + layout building blocks styled on `@base-ui/react`:
42
+ - _Basics_ — `Button` (+`buttonVariants`), `Card` (+parts), `Badge` (+`badgeVariants`),
43
+ `Separator`, `Spinner`, `Skeleton`, `Avatar` (+`AvatarImage`/`AvatarFallback`).
44
+ - _Forms_ — `Input`, `Textarea`, `Label`, `Switch`, `Checkbox`,
45
+ `RadioGroup` (+`RadioGroupItem`), `Select` (+`SelectTrigger`/`SelectValue`/
46
+ `SelectContent`/`SelectItem`/`SelectGroup`/`SelectGroupLabel`/`SelectSeparator`).
47
+ - _Navigation & feedback_ — `Tabs` (+`TabsList`/`TabsIndicator`/`TabsTab`/`TabsPanel`),
48
+ `Alert` (+`AlertTitle`/`AlertDescription`, +`alertVariants`) — an **inline, persistent,
49
+ declarative** status banner, the counterpart to the ephemeral imperative `toast()`.
50
+ - _Overlays_ (plug into the `SpuntoProvider` umbrella) — `Dialog`, `AlertDialog`
51
+ (a confirmation variant, non-dismissible), `Tooltip`. Their portals render into
52
+ the provider's overlay container; `Tooltip`'s shared delay group is mounted by the
53
+ provider too.
54
+ - **`SpuntoProvider` + `toast()`** — the imperative toast system (see below). The
55
+ provider is also the umbrella that mounts the tooltip delay group and the overlay
56
+ portal container the dialogs/selects render into.
57
+
58
+ ## Toasts — `SpuntoProvider` + `toast()`
59
+
60
+ `SpuntoProvider` is the design system's client umbrella provider. Mount it once
61
+ around your app; it renders the toast viewport, mounts the shared `Tooltip`
62
+ delay group, and hosts the overlay portal container that `Dialog`/`AlertDialog`/
63
+ `Select` render into — the single seam every overlay feature plugs into. It also
64
+ takes `tooltipDelay` (default `600`) and `tooltipCloseDelay` (default `0`).
65
+ `toast()` is a sonner-style imperative
66
+ API callable from **anywhere** — event handlers, async code, even outside the
67
+ React tree (it's backed by Base UI's `createToastManager`).
68
+
69
+ In a Next.js App Router app, the root layout stays a Server Component — just
70
+ render the (client) provider around `{children}`:
71
+
72
+ ```tsx
73
+ // app/layout.tsx
74
+ import { SpuntoProvider } from "@spunto/design-system"
75
+ import "@spunto/design-system/styles.css"
76
+
77
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
78
+ return (
79
+ <html lang="en">
80
+ <body>
81
+ <SpuntoProvider toastPosition="top-right">{children}</SpuntoProvider>
82
+ </body>
83
+ </html>
84
+ )
85
+ }
86
+ ```
87
+
88
+ Then fire toasts from anywhere:
89
+
90
+ ```tsx
91
+ import { toast } from "@spunto/design-system"
92
+
93
+ toast.success("Worker ready", { description: "spunto-abc is up" })
94
+ toast.error("Deploy failed", { action: { label: "Retry", onClick: redeploy } })
95
+
96
+ // Persistent (no auto-dismiss) + manual control:
97
+ const id = toast.info("Deploying…", { duration: 0 })
98
+ toast.dismiss(id)
99
+ ```
100
+
101
+ `toast(message, options)` and the `toast.success/error/warning/info` helpers take
102
+ `{ title?, description?, variant?, duration?, action? }` and return the toast id;
103
+ `toast.dismiss(id?)` closes one toast (or all). `SpuntoProvider` accepts
104
+ `toastPosition` (default `"top-right"`), `toastDuration` (default `5000`, `0` =
105
+ persistent) and `toastLimit` (default `3`).
44
106
 
45
107
  ## Peer requirements
46
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/design-system",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,93 @@
1
+ "use client"
2
+
3
+ import type { ComponentProps } from "react"
4
+ import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
5
+
6
+ import { cn } from "../utils"
7
+ import { useOverlayContainer } from "./spunto-provider"
8
+
9
+ /**
10
+ * Confirmation dialog. Like `Dialog` but modal-by-default and *not* dismissible
11
+ * by an outside press or Escape — the user must pick an explicit action, so wire
12
+ * a cancel and a confirm `AlertDialogClose` in the footer.
13
+ */
14
+ function AlertDialog(props: AlertDialogPrimitive.Root.Props) {
15
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
16
+ }
17
+
18
+ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
19
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ }
21
+
22
+ /** Closes the alert dialog. Wrap a `Button` via `render` for cancel/confirm. */
23
+ function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
24
+ return <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
25
+ }
26
+
27
+ function AlertDialogContent({ className, children, ...props }: AlertDialogPrimitive.Popup.Props) {
28
+ const container = useOverlayContainer()
29
+ return (
30
+ <AlertDialogPrimitive.Portal container={container ?? undefined}>
31
+ <AlertDialogPrimitive.Backdrop
32
+ data-slot="alert-dialog-backdrop"
33
+ className="fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px] transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-black/60"
34
+ />
35
+ <AlertDialogPrimitive.Popup
36
+ data-slot="alert-dialog-content"
37
+ className={cn(
38
+ "fixed top-1/2 left-1/2 z-50 grid w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-popover p-6 text-popover-foreground shadow-xl shadow-black/[0.08] outline-none",
39
+ "transition-all duration-200 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ {children}
45
+ </AlertDialogPrimitive.Popup>
46
+ </AlertDialogPrimitive.Portal>
47
+ )
48
+ }
49
+
50
+ function AlertDialogHeader({ className, ...props }: ComponentProps<"div">) {
51
+ return <div data-slot="alert-dialog-header" className={cn("flex flex-col gap-1.5", className)} {...props} />
52
+ }
53
+
54
+ function AlertDialogFooter({ className, ...props }: ComponentProps<"div">) {
55
+ return (
56
+ <div
57
+ data-slot="alert-dialog-footer"
58
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) {
65
+ return (
66
+ <AlertDialogPrimitive.Title
67
+ data-slot="alert-dialog-title"
68
+ className={cn("text-base leading-none font-semibold text-foreground", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) {
75
+ return (
76
+ <AlertDialogPrimitive.Description
77
+ data-slot="alert-dialog-description"
78
+ className={cn("text-sm text-muted-foreground", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ AlertDialog,
86
+ AlertDialogTrigger,
87
+ AlertDialogClose,
88
+ AlertDialogContent,
89
+ AlertDialogHeader,
90
+ AlertDialogFooter,
91
+ AlertDialogTitle,
92
+ AlertDialogDescription,
93
+ }
@@ -0,0 +1,104 @@
1
+ import type { ComponentProps, ReactNode } from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../utils"
5
+
6
+ // Persistent, declarative status banner — the counterpart to the ephemeral,
7
+ // imperative `toast()`. Same four semantic variants and status tokens for a
8
+ // coherent visual language, but this one lives in the page flow and stays put.
9
+ const alertVariants = cva(
10
+ "relative grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border border-l-4 px-4 py-3 text-sm [&>svg]:mt-0.5 [&>svg]:size-[18px] [&>svg]:shrink-0",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ success: "border-border border-l-success bg-success/8 text-foreground [&>svg]:text-success",
15
+ error: "border-border border-l-destructive bg-destructive/8 text-foreground [&>svg]:text-destructive",
16
+ warning: "border-border border-l-warning bg-warning/10 text-foreground [&>svg]:text-warning",
17
+ info: "border-border border-l-info bg-info/8 text-foreground [&>svg]:text-info",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "info",
22
+ },
23
+ }
24
+ )
25
+
26
+ export type AlertVariant = NonNullable<VariantProps<typeof alertVariants>["variant"]>
27
+
28
+ function AlertIcon({ variant }: { variant: AlertVariant }) {
29
+ const common = { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true } as const
30
+ switch (variant) {
31
+ case "success":
32
+ return (
33
+ <svg {...common}>
34
+ <circle cx="12" cy="12" r="9" />
35
+ <path d="m8.5 12 2.5 2.5 4.5-5" />
36
+ </svg>
37
+ )
38
+ case "error":
39
+ return (
40
+ <svg {...common}>
41
+ <circle cx="12" cy="12" r="9" />
42
+ <path d="M15 9l-6 6M9 9l6 6" />
43
+ </svg>
44
+ )
45
+ case "warning":
46
+ return (
47
+ <svg {...common}>
48
+ <path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
49
+ <path d="M12 9v4M12 17h.01" />
50
+ </svg>
51
+ )
52
+ case "info":
53
+ return (
54
+ <svg {...common}>
55
+ <circle cx="12" cy="12" r="9" />
56
+ <path d="M12 11v5M12 8h.01" />
57
+ </svg>
58
+ )
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Inline status banner rendered in the page flow — persistent and declarative,
64
+ * unlike the transient `toast()`. Four semantic variants sharing the status
65
+ * tokens. Pass `icon={false}` to drop the leading icon, or a node to override it.
66
+ *
67
+ * ```tsx
68
+ * <Alert variant="warning">
69
+ * <AlertTitle>Quota bientôt atteint</AlertTitle>
70
+ * <AlertDescription>80 % du CPU alloué est utilisé.</AlertDescription>
71
+ * </Alert>
72
+ * ```
73
+ */
74
+ function Alert({
75
+ className,
76
+ variant = "info",
77
+ icon,
78
+ children,
79
+ ...props
80
+ }: ComponentProps<"div"> & VariantProps<typeof alertVariants> & { icon?: ReactNode | false }) {
81
+ const resolved = variant ?? "info"
82
+ return (
83
+ <div data-slot="alert" role="alert" className={cn(alertVariants({ variant: resolved }), className)} {...props}>
84
+ {icon === false ? null : icon ?? <AlertIcon variant={resolved} />}
85
+ <div className={cn("grid gap-1", icon === false && "col-span-2")}>{children}</div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ function AlertTitle({ className, ...props }: ComponentProps<"div">) {
91
+ return <div data-slot="alert-title" className={cn("font-semibold text-foreground", className)} {...props} />
92
+ }
93
+
94
+ function AlertDescription({ className, ...props }: ComponentProps<"div">) {
95
+ return (
96
+ <div
97
+ data-slot="alert-description"
98
+ className={cn("text-sm text-muted-foreground [&_p]:leading-relaxed", className)}
99
+ {...props}
100
+ />
101
+ )
102
+ }
103
+
104
+ export { Alert, AlertTitle, AlertDescription, alertVariants }
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
4
+
5
+ import { cn } from "../utils"
6
+
7
+ /** Circular avatar container. Compose `AvatarImage` + `AvatarFallback` inside. */
8
+ function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
9
+ return (
10
+ <AvatarPrimitive.Root
11
+ data-slot="avatar"
12
+ className={cn(
13
+ "relative flex size-9 shrink-0 overflow-hidden rounded-full bg-muted select-none",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ /** The avatar image. Hides itself while loading or on error (fallback shows). */
22
+ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
23
+ return <AvatarPrimitive.Image data-slot="avatar-image" className={cn("size-full object-cover", className)} {...props} />
24
+ }
25
+
26
+ /** Shown when the image is missing or still loading — usually initials. */
27
+ function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
28
+ return (
29
+ <AvatarPrimitive.Fallback
30
+ data-slot="avatar-fallback"
31
+ className={cn(
32
+ "flex size-full items-center justify-center bg-muted text-sm font-medium text-muted-foreground",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
4
+
5
+ import { cn } from "../utils"
6
+
7
+ /**
8
+ * A checkbox, styled to match `Switch`. Controlled via `checked`/`onCheckedChange`
9
+ * or uncontrolled via `defaultChecked`; supports `indeterminate`.
10
+ */
11
+ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
12
+ return (
13
+ <CheckboxPrimitive.Root
14
+ data-slot="checkbox"
15
+ className={cn(
16
+ "peer flex size-4 shrink-0 items-center justify-center rounded-[min(var(--radius-md),5px)] border border-input bg-transparent text-primary-foreground shadow-sm transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary dark:bg-input/30 dark:data-checked:bg-primary",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-current transition-none data-unchecked:hidden">
22
+ {props.indeterminate ? (
23
+ <svg width={12} height={12} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" aria-hidden>
24
+ <path d="M5 12h14" />
25
+ </svg>
26
+ ) : (
27
+ <svg width={12} height={12} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
28
+ <path d="M20 6 9 17l-5-5" />
29
+ </svg>
30
+ )}
31
+ </CheckboxPrimitive.Indicator>
32
+ </CheckboxPrimitive.Root>
33
+ )
34
+ }
35
+
36
+ export { Checkbox }
@@ -0,0 +1,112 @@
1
+ "use client"
2
+
3
+ import type { ComponentProps } from "react"
4
+ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
5
+
6
+ import { cn } from "../utils"
7
+ import { useOverlayContainer } from "./spunto-provider"
8
+
9
+ /** Groups the dialog parts. Controlled via `open`/`onOpenChange`, or uncontrolled. */
10
+ function Dialog(props: DialogPrimitive.Root.Props) {
11
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
12
+ }
13
+
14
+ /** Element that opens the dialog. Pass `render` to reuse an existing button. */
15
+ function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
16
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
17
+ }
18
+
19
+ /** Imperatively closes the dialog. Wrap a `Button` via `render` for a footer action. */
20
+ function DialogClose(props: DialogPrimitive.Close.Props) {
21
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
22
+ }
23
+
24
+ /**
25
+ * Portal + dimmed backdrop + centered popup, in one styled wrapper. Renders into
26
+ * the `SpuntoProvider` overlay container (falls back to `document.body`). Compose
27
+ * `DialogHeader`/`DialogFooter`/`DialogTitle`/`DialogDescription` inside it.
28
+ */
29
+ function DialogContent({
30
+ className,
31
+ children,
32
+ showClose = true,
33
+ ...props
34
+ }: DialogPrimitive.Popup.Props & { showClose?: boolean }) {
35
+ const container = useOverlayContainer()
36
+ return (
37
+ <DialogPrimitive.Portal container={container ?? undefined}>
38
+ <DialogPrimitive.Backdrop
39
+ data-slot="dialog-backdrop"
40
+ className="fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px] transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-black/60"
41
+ />
42
+ <DialogPrimitive.Popup
43
+ data-slot="dialog-content"
44
+ className={cn(
45
+ "fixed top-1/2 left-1/2 z-50 grid w-[calc(100vw-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-popover p-6 text-popover-foreground shadow-xl shadow-black/[0.08] outline-none",
46
+ "transition-all duration-200 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ {children}
52
+ {showClose && (
53
+ <DialogPrimitive.Close
54
+ aria-label="Fermer"
55
+ className="absolute top-4 right-4 flex size-7 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 outline-none"
56
+ >
57
+ <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
58
+ <path d="M18 6 6 18M6 6l12 12" />
59
+ </svg>
60
+ </DialogPrimitive.Close>
61
+ )}
62
+ </DialogPrimitive.Popup>
63
+ </DialogPrimitive.Portal>
64
+ )
65
+ }
66
+
67
+ /** Title + description stack at the top of the dialog. */
68
+ function DialogHeader({ className, ...props }: ComponentProps<"div">) {
69
+ return <div data-slot="dialog-header" className={cn("flex flex-col gap-1.5 pr-6", className)} {...props} />
70
+ }
71
+
72
+ /** Right-aligned action row (buttons) at the bottom of the dialog. */
73
+ function DialogFooter({ className, ...props }: ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="dialog-footer"
77
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+
83
+ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
84
+ return (
85
+ <DialogPrimitive.Title
86
+ data-slot="dialog-title"
87
+ className={cn("text-base leading-none font-semibold text-foreground", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
94
+ return (
95
+ <DialogPrimitive.Description
96
+ data-slot="dialog-description"
97
+ className={cn("text-sm text-muted-foreground", className)}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ export {
104
+ Dialog,
105
+ DialogTrigger,
106
+ DialogClose,
107
+ DialogContent,
108
+ DialogHeader,
109
+ DialogFooter,
110
+ DialogTitle,
111
+ DialogDescription,
112
+ }
@@ -0,0 +1,34 @@
1
+ "use client"
2
+
3
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio"
4
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
5
+
6
+ import { cn } from "../utils"
7
+
8
+ /**
9
+ * A set of mutually-exclusive radio buttons. Controlled via `value`/`onValueChange`
10
+ * or uncontrolled via `defaultValue`. Compose `RadioGroupItem`s inside it.
11
+ */
12
+ function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
13
+ return <RadioGroupPrimitive data-slot="radio-group" className={cn("grid gap-2.5", className)} {...props} />
14
+ }
15
+
16
+ /** A single radio button. `value` is what the group commits when selected. */
17
+ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
18
+ return (
19
+ <RadioPrimitive.Root
20
+ data-slot="radio-group-item"
21
+ className={cn(
22
+ "flex size-4 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-sm transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-checked:border-primary data-checked:bg-primary dark:bg-input/30 dark:data-checked:bg-primary",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ <RadioPrimitive.Indicator className="flex items-center justify-center data-unchecked:hidden">
28
+ <span className="size-1.5 rounded-full bg-primary-foreground" />
29
+ </RadioPrimitive.Indicator>
30
+ </RadioPrimitive.Root>
31
+ )
32
+ }
33
+
34
+ export { RadioGroup, RadioGroupItem }
@@ -0,0 +1,120 @@
1
+ "use client"
2
+
3
+ import type { ComponentProps } from "react"
4
+ import { Select as SelectPrimitive } from "@base-ui/react/select"
5
+
6
+ import { cn } from "../utils"
7
+ import { useOverlayContainer } from "./spunto-provider"
8
+
9
+ /** Groups the select parts. Controlled via `value`/`onValueChange`, or uncontrolled. */
10
+ function Select<Value>(props: SelectPrimitive.Root.Props<Value>) {
11
+ return <SelectPrimitive.Root data-slot="select" {...props} />
12
+ }
13
+
14
+ /** The button that opens the listbox. Put a `SelectValue` inside it. */
15
+ function SelectTrigger({ className, children, ...props }: SelectPrimitive.Trigger.Props) {
16
+ return (
17
+ <SelectPrimitive.Trigger
18
+ data-slot="select-trigger"
19
+ className={cn(
20
+ "flex h-8 w-full items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm whitespace-nowrap transition-colors outline-none select-none data-[popup-open]:border-ring focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground dark:bg-input/30 [&>span]:truncate",
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon className="text-muted-foreground">
27
+ <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
28
+ <path d="m6 9 6 6 6-6" />
29
+ </svg>
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ )
33
+ }
34
+
35
+ /** Displays the selected item's label (or `placeholder` when empty). */
36
+ function SelectValue(props: SelectPrimitive.Value.Props) {
37
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
38
+ }
39
+
40
+ /** Portal + positioned listbox popup. Wraps `SelectItem`/`SelectGroup` children. */
41
+ function SelectContent({ className, children, ...props }: SelectPrimitive.Popup.Props) {
42
+ const container = useOverlayContainer()
43
+ return (
44
+ <SelectPrimitive.Portal container={container ?? undefined}>
45
+ <SelectPrimitive.Positioner
46
+ data-slot="select-positioner"
47
+ className="z-50 outline-none"
48
+ side="bottom"
49
+ align="start"
50
+ sideOffset={4}
51
+ >
52
+ <SelectPrimitive.Popup
53
+ data-slot="select-content"
54
+ className={cn(
55
+ "max-h-[var(--available-height)] min-w-[max(8rem,var(--anchor-width))] overflow-y-auto overscroll-contain rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/[0.06] outline-none",
56
+ "origin-[var(--transform-origin)] transition-[transform,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ </SelectPrimitive.Popup>
63
+ </SelectPrimitive.Positioner>
64
+ </SelectPrimitive.Portal>
65
+ )
66
+ }
67
+
68
+ /** A selectable option. `value` is what gets committed; children is the label. */
69
+ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
70
+ return (
71
+ <SelectPrimitive.Item
72
+ data-slot="select-item"
73
+ className={cn(
74
+ "relative flex cursor-default items-center gap-2 rounded-md py-1.5 pr-2 pl-7 text-sm outline-none select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
75
+ className
76
+ )}
77
+ {...props}
78
+ >
79
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
80
+ <SelectPrimitive.ItemIndicator>
81
+ <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
82
+ <path d="M20 6 9 17l-5-5" />
83
+ </svg>
84
+ </SelectPrimitive.ItemIndicator>
85
+ </span>
86
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
87
+ </SelectPrimitive.Item>
88
+ )
89
+ }
90
+
91
+ /** Groups related items under a `SelectGroupLabel`. */
92
+ function SelectGroup(props: SelectPrimitive.Group.Props) {
93
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />
94
+ }
95
+
96
+ function SelectGroupLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
97
+ return (
98
+ <SelectPrimitive.GroupLabel
99
+ data-slot="select-group-label"
100
+ className={cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", className)}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ /** A thin rule between item groups. */
107
+ function SelectSeparator({ className, ...props }: ComponentProps<"div">) {
108
+ return <div data-slot="select-separator" role="separator" className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
109
+ }
110
+
111
+ export {
112
+ Select,
113
+ SelectTrigger,
114
+ SelectValue,
115
+ SelectContent,
116
+ SelectItem,
117
+ SelectGroup,
118
+ SelectGroupLabel,
119
+ SelectSeparator,
120
+ }
@@ -0,0 +1,23 @@
1
+ "use client"
2
+
3
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
4
+
5
+ import { cn } from "../utils"
6
+
7
+ /** A thin rule that separates content, horizontal or vertical. */
8
+ function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
9
+ return (
10
+ <SeparatorPrimitive
11
+ data-slot="separator"
12
+ orientation={orientation}
13
+ className={cn(
14
+ "shrink-0 bg-border",
15
+ orientation === "vertical" ? "h-full w-px" : "h-px w-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ export { Separator }
@@ -0,0 +1,13 @@
1
+ import type { ComponentProps } from "react"
2
+
3
+ import { cn } from "../utils"
4
+
5
+ /**
6
+ * A pulsing placeholder for content that hasn't loaded yet. Size it with
7
+ * utilities (`className="h-4 w-32"`); use `rounded-full` for circular avatars.
8
+ */
9
+ function Skeleton({ className, ...props }: ComponentProps<"div">) {
10
+ return <div data-slot="skeleton" className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
11
+ }
12
+
13
+ export { Skeleton }