@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 +65 -3
- package/package.json +1 -1
- package/src/components/alert-dialog.tsx +93 -0
- package/src/components/alert.tsx +104 -0
- package/src/components/avatar.tsx +40 -0
- package/src/components/checkbox.tsx +36 -0
- package/src/components/dialog.tsx +112 -0
- package/src/components/radio-group.tsx +34 -0
- package/src/components/select.tsx +120 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/spinner.tsx +45 -0
- package/src/components/spunto-provider.tsx +121 -0
- package/src/components/tabs.tsx +68 -0
- package/src/components/toast.tsx +199 -0
- package/src/components/tooltip.tsx +90 -0
- package/src/index.ts +59 -0
- package/styles.css +151 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ComponentProps } from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../utils"
|
|
5
|
+
|
|
6
|
+
const spinnerVariants = cva("animate-spin text-current", {
|
|
7
|
+
variants: {
|
|
8
|
+
size: {
|
|
9
|
+
xs: "size-3",
|
|
10
|
+
sm: "size-4",
|
|
11
|
+
default: "size-5",
|
|
12
|
+
lg: "size-6",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An indeterminate loading spinner. Inherits the current text color, so drop it
|
|
22
|
+
* inside a `Button` or set `text-*` to tint it.
|
|
23
|
+
*/
|
|
24
|
+
function Spinner({
|
|
25
|
+
className,
|
|
26
|
+
size,
|
|
27
|
+
...props
|
|
28
|
+
}: ComponentProps<"svg"> & VariantProps<typeof spinnerVariants>) {
|
|
29
|
+
return (
|
|
30
|
+
<svg
|
|
31
|
+
data-slot="spinner"
|
|
32
|
+
role="status"
|
|
33
|
+
aria-label="Chargement"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
className={cn(spinnerVariants({ size }), className)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth={2.5} className="opacity-20" />
|
|
40
|
+
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { Spinner, spinnerVariants }
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, type ReactNode } from "react"
|
|
4
|
+
import { Toast as ToastPrimitive } from "@base-ui/react/toast"
|
|
5
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
6
|
+
|
|
7
|
+
import { ToastList, toastManager, type ToastSwipeDirection } from "./toast"
|
|
8
|
+
|
|
9
|
+
export type ToastPosition =
|
|
10
|
+
| "top-left"
|
|
11
|
+
| "top-center"
|
|
12
|
+
| "top-right"
|
|
13
|
+
| "bottom-left"
|
|
14
|
+
| "bottom-center"
|
|
15
|
+
| "bottom-right"
|
|
16
|
+
|
|
17
|
+
export interface SpuntoProviderProps {
|
|
18
|
+
/** App tree. */
|
|
19
|
+
children?: ReactNode
|
|
20
|
+
/**
|
|
21
|
+
* Where the toast stack docks.
|
|
22
|
+
* @default "top-right"
|
|
23
|
+
*/
|
|
24
|
+
toastPosition?: ToastPosition
|
|
25
|
+
/**
|
|
26
|
+
* Default auto-dismiss delay (ms) for toasts that don't set their own.
|
|
27
|
+
* `0` keeps toasts until dismissed.
|
|
28
|
+
* @default 5000
|
|
29
|
+
*/
|
|
30
|
+
toastDuration?: number
|
|
31
|
+
/**
|
|
32
|
+
* Maximum number of toasts stacked at once. Older toasts collapse behind the
|
|
33
|
+
* newest and animate out past this count.
|
|
34
|
+
* @default 3
|
|
35
|
+
*/
|
|
36
|
+
toastLimit?: number
|
|
37
|
+
/**
|
|
38
|
+
* Delay (ms) before a hovered `Tooltip` opens. Shared across every tooltip via
|
|
39
|
+
* Base UI's delay group, so once one is showing its neighbours open instantly.
|
|
40
|
+
* @default 600
|
|
41
|
+
*/
|
|
42
|
+
tooltipDelay?: number
|
|
43
|
+
/**
|
|
44
|
+
* Delay (ms) before a tooltip closes once the pointer leaves.
|
|
45
|
+
* @default 0
|
|
46
|
+
*/
|
|
47
|
+
tooltipCloseDelay?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Toasts swipe toward the nearest screen edge for the current position.
|
|
51
|
+
const SWIPE_DIRECTIONS: Record<ToastPosition, ToastSwipeDirection[]> = {
|
|
52
|
+
"top-left": ["up", "left"],
|
|
53
|
+
"top-center": ["up"],
|
|
54
|
+
"top-right": ["up", "right"],
|
|
55
|
+
"bottom-left": ["down", "left"],
|
|
56
|
+
"bottom-center": ["down"],
|
|
57
|
+
"bottom-right": ["down", "right"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shared DOM node the overlay components (`Dialog`, `AlertDialog`, `Select`,
|
|
62
|
+
* `Tooltip`) portal into. `SpuntoProvider` renders it as the last child of its
|
|
63
|
+
* subtree so every popup lands *inside* the themed tree — the `.dark` class and
|
|
64
|
+
* the token CSS vars cascade to portalled content for free. When no provider is
|
|
65
|
+
* mounted the value is `null`, and the primitives fall back to Base UI's default
|
|
66
|
+
* (`document.body`), so the components keep working standalone.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
const OverlayContainerContext = createContext<HTMLElement | null>(null)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Container element the design-system overlays portal into, provided by the
|
|
73
|
+
* nearest `SpuntoProvider` (or `null` → Base UI's `document.body` default).
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export function useOverlayContainer(): HTMLElement | null {
|
|
77
|
+
return useContext(OverlayContainerContext)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Client-side umbrella provider for the Spunto design system. Wrap your app once
|
|
82
|
+
* (in a Next.js `app/layout.tsx`, it's a client component — render it around
|
|
83
|
+
* `{children}` inside the server layout, no `"use client"` needed on the layout
|
|
84
|
+
* itself). It mounts the toast viewport, the shared tooltip delay group, and the
|
|
85
|
+
* overlay portal container that dialogs/selects render into — the single seam
|
|
86
|
+
* every overlay feature plugs into without a breaking change.
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <SpuntoProvider toastPosition="bottom-right">
|
|
90
|
+
* {children}
|
|
91
|
+
* </SpuntoProvider>
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function SpuntoProvider({
|
|
95
|
+
children,
|
|
96
|
+
toastPosition = "top-right",
|
|
97
|
+
toastDuration = 5000,
|
|
98
|
+
toastLimit = 3,
|
|
99
|
+
tooltipDelay = 600,
|
|
100
|
+
tooltipCloseDelay = 0,
|
|
101
|
+
}: SpuntoProviderProps) {
|
|
102
|
+
const [overlayContainer, setOverlayContainer] = useState<HTMLElement | null>(null)
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<ToastPrimitive.Provider toastManager={toastManager} timeout={toastDuration} limit={toastLimit}>
|
|
106
|
+
<TooltipPrimitive.Provider delay={tooltipDelay} closeDelay={tooltipCloseDelay}>
|
|
107
|
+
<OverlayContainerContext.Provider value={overlayContainer}>
|
|
108
|
+
{children}
|
|
109
|
+
<ToastPrimitive.Portal>
|
|
110
|
+
<ToastPrimitive.Viewport data-slot="toast-viewport" data-position={toastPosition}>
|
|
111
|
+
<ToastList swipeDirection={SWIPE_DIRECTIONS[toastPosition]} />
|
|
112
|
+
</ToastPrimitive.Viewport>
|
|
113
|
+
</ToastPrimitive.Portal>
|
|
114
|
+
{/* Overlays (Dialog/Select/Tooltip) portal into this node — kept last so
|
|
115
|
+
popups stack above the app, and inside the provider so tokens cascade. */}
|
|
116
|
+
<div ref={setOverlayContainer} data-slot="spunto-overlay-root" />
|
|
117
|
+
</OverlayContainerContext.Provider>
|
|
118
|
+
</TooltipPrimitive.Provider>
|
|
119
|
+
</ToastPrimitive.Provider>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../utils"
|
|
6
|
+
|
|
7
|
+
/** Groups the tab parts. Controlled via `value`/`onValueChange`, or uncontrolled. */
|
|
8
|
+
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
|
|
9
|
+
return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-3", className)} {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The row of tabs. Contains `TabsTab`s and an optional `TabsIndicator`. Styled as
|
|
14
|
+
* a segmented control with a muted track.
|
|
15
|
+
*/
|
|
16
|
+
function TabsList({ className, ...props }: TabsPrimitive.List.Props) {
|
|
17
|
+
return (
|
|
18
|
+
<TabsPrimitive.List
|
|
19
|
+
data-slot="tabs-list"
|
|
20
|
+
className={cn(
|
|
21
|
+
"relative isolate flex h-9 w-fit items-center gap-1 rounded-lg bg-muted p-1 text-muted-foreground",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The sliding highlight behind the active tab. Optional; place inside `TabsList`. */
|
|
30
|
+
function TabsIndicator({ className, ...props }: TabsPrimitive.Indicator.Props) {
|
|
31
|
+
return (
|
|
32
|
+
<TabsPrimitive.Indicator
|
|
33
|
+
data-slot="tabs-indicator"
|
|
34
|
+
className={cn(
|
|
35
|
+
"absolute top-1/2 left-0 z-[-1] h-[calc(100%-0.5rem)] w-[var(--active-tab-width)] -translate-y-1/2 translate-x-[var(--active-tab-left)] rounded-md bg-background shadow-sm transition-[width,transform] duration-200 ease-out",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A single tab button. `value` ties it to the matching `TabsPanel`. */
|
|
44
|
+
function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
45
|
+
return (
|
|
46
|
+
<TabsPrimitive.Tab
|
|
47
|
+
data-slot="tabs-tab"
|
|
48
|
+
className={cn(
|
|
49
|
+
"inline-flex h-full cursor-default items-center justify-center rounded-md px-3 text-sm font-medium whitespace-nowrap transition-colors outline-none select-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 data-disabled:pointer-events-none data-disabled:opacity-50 data-selected:text-foreground",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The content shown for the tab with the matching `value`. */
|
|
58
|
+
function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
59
|
+
return (
|
|
60
|
+
<TabsPrimitive.Panel
|
|
61
|
+
data-slot="tabs-panel"
|
|
62
|
+
className={cn("outline-none focus-visible:ring-2 focus-visible:ring-ring/50", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Tabs, TabsList, TabsIndicator, TabsTab, TabsPanel }
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { Toast as ToastPrimitive } from "@base-ui/react/toast"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../utils"
|
|
7
|
+
|
|
8
|
+
export type ToastVariant = "success" | "error" | "warning" | "info"
|
|
9
|
+
|
|
10
|
+
/** Optional call-to-action rendered as a button inside the toast. */
|
|
11
|
+
export interface ToastAction {
|
|
12
|
+
label: string
|
|
13
|
+
onClick: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ToastOptions {
|
|
17
|
+
/** Overrides the first positional argument as the toast's heading. */
|
|
18
|
+
title?: ReactNode
|
|
19
|
+
/** Secondary line under the title. */
|
|
20
|
+
description?: ReactNode
|
|
21
|
+
/** Colour + icon. Defaults to a neutral toast when omitted. */
|
|
22
|
+
variant?: ToastVariant
|
|
23
|
+
/**
|
|
24
|
+
* Auto-dismiss delay in ms. `0` keeps the toast until dismissed.
|
|
25
|
+
* Falls back to the `SpuntoProvider` `toastDuration` when omitted.
|
|
26
|
+
*/
|
|
27
|
+
duration?: number
|
|
28
|
+
/** Optional action button. Clicking it also dismisses the toast. */
|
|
29
|
+
action?: ToastAction
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Payload we stash on each Base UI toast so the renderer can style it. */
|
|
33
|
+
export interface SpuntoToastData {
|
|
34
|
+
variant?: ToastVariant
|
|
35
|
+
action?: ToastAction
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Singleton manager shared by the imperative `toast()` API and the
|
|
40
|
+
* `SpuntoProvider` that renders it. Created with Base UI's `createToastManager`
|
|
41
|
+
* so `toast()` is callable from anywhere — event handlers, async code, even
|
|
42
|
+
* outside the React tree.
|
|
43
|
+
*/
|
|
44
|
+
export const toastManager = ToastPrimitive.createToastManager<SpuntoToastData>()
|
|
45
|
+
|
|
46
|
+
function emit(message: ReactNode, options: ToastOptions = {}, variant?: ToastVariant): string {
|
|
47
|
+
const resolved = variant ?? options.variant
|
|
48
|
+
return toastManager.add({
|
|
49
|
+
title: options.title ?? message,
|
|
50
|
+
description: options.description,
|
|
51
|
+
// `undefined` lets the provider's default timeout apply.
|
|
52
|
+
timeout: options.duration,
|
|
53
|
+
type: resolved,
|
|
54
|
+
data: { variant: resolved, action: options.action },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ToastFn {
|
|
59
|
+
(message: ReactNode, options?: ToastOptions): string
|
|
60
|
+
success: (message: ReactNode, options?: ToastOptions) => string
|
|
61
|
+
error: (message: ReactNode, options?: ToastOptions) => string
|
|
62
|
+
warning: (message: ReactNode, options?: ToastOptions) => string
|
|
63
|
+
info: (message: ReactNode, options?: ToastOptions) => string
|
|
64
|
+
/** Dismiss a specific toast by id, or all toasts when called without one. */
|
|
65
|
+
dismiss: (id?: string) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Imperative, sonner-style toast API. Import and call from anywhere.
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* toast.success("Worker ready", { description: "spunto-abc is up" })
|
|
73
|
+
* const id = toast("Deploying…", { duration: 0 })
|
|
74
|
+
* toast.dismiss(id)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const toast: ToastFn = Object.assign(
|
|
78
|
+
(message: ReactNode, options?: ToastOptions) => emit(message, options),
|
|
79
|
+
{
|
|
80
|
+
success: (message: ReactNode, options?: ToastOptions) => emit(message, options, "success"),
|
|
81
|
+
error: (message: ReactNode, options?: ToastOptions) => emit(message, options, "error"),
|
|
82
|
+
warning: (message: ReactNode, options?: ToastOptions) => emit(message, options, "warning"),
|
|
83
|
+
info: (message: ReactNode, options?: ToastOptions) => emit(message, options, "info"),
|
|
84
|
+
dismiss: (id?: string) => toastManager.close(id),
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
/** Per-variant accent (icon + left border). Derived from the status tokens. */
|
|
89
|
+
const VARIANT_ACCENT: Record<ToastVariant, string> = {
|
|
90
|
+
success: "text-success border-l-success",
|
|
91
|
+
error: "text-destructive border-l-destructive",
|
|
92
|
+
warning: "text-warning border-l-warning",
|
|
93
|
+
info: "text-info border-l-info",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function VariantIcon({ variant }: { variant?: ToastVariant }) {
|
|
97
|
+
const common = { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true } as const
|
|
98
|
+
switch (variant) {
|
|
99
|
+
case "success":
|
|
100
|
+
return (
|
|
101
|
+
<svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
102
|
+
<circle cx="12" cy="12" r="9" />
|
|
103
|
+
<path d="m8.5 12 2.5 2.5 4.5-5" />
|
|
104
|
+
</svg>
|
|
105
|
+
)
|
|
106
|
+
case "error":
|
|
107
|
+
return (
|
|
108
|
+
<svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
109
|
+
<circle cx="12" cy="12" r="9" />
|
|
110
|
+
<path d="M15 9l-6 6M9 9l6 6" />
|
|
111
|
+
</svg>
|
|
112
|
+
)
|
|
113
|
+
case "warning":
|
|
114
|
+
return (
|
|
115
|
+
<svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
116
|
+
<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" />
|
|
117
|
+
<path d="M12 9v4M12 17h.01" />
|
|
118
|
+
</svg>
|
|
119
|
+
)
|
|
120
|
+
case "info":
|
|
121
|
+
return (
|
|
122
|
+
<svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
123
|
+
<circle cx="12" cy="12" r="9" />
|
|
124
|
+
<path d="M12 11v5M12 8h.01" />
|
|
125
|
+
</svg>
|
|
126
|
+
)
|
|
127
|
+
default:
|
|
128
|
+
return (
|
|
129
|
+
<svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
130
|
+
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
|
131
|
+
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
|
132
|
+
</svg>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function CloseIcon() {
|
|
138
|
+
return (
|
|
139
|
+
<svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
140
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
141
|
+
</svg>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Renders the live toast stack. Consumes the manager via `useToastManager`, so
|
|
147
|
+
* it must live inside a `Toast.Provider` — `SpuntoProvider` handles that.
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export type ToastSwipeDirection = "up" | "down" | "left" | "right"
|
|
151
|
+
|
|
152
|
+
export function ToastList({ swipeDirection }: { swipeDirection?: ToastSwipeDirection[] }) {
|
|
153
|
+
const { toasts } = ToastPrimitive.useToastManager<SpuntoToastData>()
|
|
154
|
+
|
|
155
|
+
return toasts.map((item) => {
|
|
156
|
+
const variant = item.data?.variant
|
|
157
|
+
const action = item.data?.action
|
|
158
|
+
return (
|
|
159
|
+
<ToastPrimitive.Root
|
|
160
|
+
key={item.id}
|
|
161
|
+
toast={item}
|
|
162
|
+
swipeDirection={swipeDirection}
|
|
163
|
+
data-slot="toast"
|
|
164
|
+
className={cn(
|
|
165
|
+
"pointer-events-auto rounded-lg border border-border border-l-4 bg-popover text-popover-foreground shadow-lg shadow-black/[0.06] outline-none",
|
|
166
|
+
"focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
167
|
+
variant ? VARIANT_ACCENT[variant] : "border-l-muted-foreground/40"
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
<ToastPrimitive.Content className="flex h-full items-start gap-3 p-3.5 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100">
|
|
171
|
+
<span className={cn("mt-px shrink-0", variant ? VARIANT_ACCENT[variant] : "text-muted-foreground")}>
|
|
172
|
+
<VariantIcon variant={variant} />
|
|
173
|
+
</span>
|
|
174
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
175
|
+
<ToastPrimitive.Title className="text-sm leading-snug font-semibold text-foreground" />
|
|
176
|
+
<ToastPrimitive.Description className="text-sm leading-snug text-muted-foreground" />
|
|
177
|
+
{action && (
|
|
178
|
+
<ToastPrimitive.Action
|
|
179
|
+
className="mt-1.5 inline-flex h-7 w-fit items-center rounded-md border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50 outline-none"
|
|
180
|
+
onClick={() => {
|
|
181
|
+
action.onClick()
|
|
182
|
+
toastManager.close(item.id)
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{action.label}
|
|
186
|
+
</ToastPrimitive.Action>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
<ToastPrimitive.Close
|
|
190
|
+
aria-label="Fermer"
|
|
191
|
+
className="-mt-1 -mr-1 flex size-6 shrink-0 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"
|
|
192
|
+
>
|
|
193
|
+
<CloseIcon />
|
|
194
|
+
</ToastPrimitive.Close>
|
|
195
|
+
</ToastPrimitive.Content>
|
|
196
|
+
</ToastPrimitive.Root>
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { ReactElement, ReactNode } from "react"
|
|
4
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../utils"
|
|
7
|
+
import { useOverlayContainer } from "./spunto-provider"
|
|
8
|
+
|
|
9
|
+
export interface TooltipProps {
|
|
10
|
+
/** The tooltip bubble content. When omitted, the child renders with no tooltip. */
|
|
11
|
+
content?: ReactNode
|
|
12
|
+
/** The element the tooltip is anchored to (used as the trigger). */
|
|
13
|
+
children: ReactElement<Record<string, unknown>>
|
|
14
|
+
/**
|
|
15
|
+
* Preferred side of the trigger to place the bubble.
|
|
16
|
+
* @default "top"
|
|
17
|
+
*/
|
|
18
|
+
side?: TooltipPrimitive.Positioner.Props["side"]
|
|
19
|
+
/**
|
|
20
|
+
* Alignment along the chosen side.
|
|
21
|
+
* @default "center"
|
|
22
|
+
*/
|
|
23
|
+
align?: TooltipPrimitive.Positioner.Props["align"]
|
|
24
|
+
/** Gap (px) between the trigger and the bubble. @default 6 */
|
|
25
|
+
sideOffset?: number
|
|
26
|
+
/** Show the little pointer triangle. @default true */
|
|
27
|
+
arrow?: boolean
|
|
28
|
+
/** Controlled open state. */
|
|
29
|
+
open?: boolean
|
|
30
|
+
/** Open-state change handler (controlled mode). */
|
|
31
|
+
onOpenChange?: TooltipPrimitive.Root.Props["onOpenChange"]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hover/focus tooltip built on Base UI. Wrap any focusable element:
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Tooltip content="Copier l'ID">
|
|
39
|
+
* <Button size="icon" variant="ghost"><CopyIcon /></Button>
|
|
40
|
+
* </Tooltip>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The shared open delay + "instant on neighbour" grouping comes from the
|
|
44
|
+
* `Tooltip.Provider` that `SpuntoProvider` mounts; it also works standalone.
|
|
45
|
+
*/
|
|
46
|
+
function Tooltip({
|
|
47
|
+
content,
|
|
48
|
+
children,
|
|
49
|
+
side = "top",
|
|
50
|
+
align = "center",
|
|
51
|
+
sideOffset = 6,
|
|
52
|
+
arrow = true,
|
|
53
|
+
open,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
}: TooltipProps) {
|
|
56
|
+
const container = useOverlayContainer()
|
|
57
|
+
|
|
58
|
+
if (content == null || content === false) return children
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<TooltipPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
62
|
+
<TooltipPrimitive.Trigger data-slot="tooltip-trigger" render={children} />
|
|
63
|
+
<TooltipPrimitive.Portal container={container ?? undefined}>
|
|
64
|
+
<TooltipPrimitive.Positioner data-slot="tooltip-positioner" side={side} align={align} sideOffset={sideOffset}>
|
|
65
|
+
<TooltipPrimitive.Popup
|
|
66
|
+
data-slot="tooltip"
|
|
67
|
+
className={cn(
|
|
68
|
+
"z-50 max-w-xs rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md shadow-black/10",
|
|
69
|
+
"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"
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{arrow && (
|
|
73
|
+
<TooltipPrimitive.Arrow
|
|
74
|
+
data-slot="tooltip-arrow"
|
|
75
|
+
className="data-[side=bottom]:top-[-6px] data-[side=left]:right-[-6px] data-[side=right]:left-[-6px] data-[side=top]:bottom-[-6px] data-[side=left]:rotate-90 data-[side=right]:-rotate-90 data-[side=bottom]:rotate-180"
|
|
76
|
+
>
|
|
77
|
+
<svg width={12} height={6} viewBox="0 0 12 6" fill="none" aria-hidden>
|
|
78
|
+
<path d="M0 0L6 6L12 0" className="fill-foreground" />
|
|
79
|
+
</svg>
|
|
80
|
+
</TooltipPrimitive.Arrow>
|
|
81
|
+
)}
|
|
82
|
+
{content}
|
|
83
|
+
</TooltipPrimitive.Popup>
|
|
84
|
+
</TooltipPrimitive.Positioner>
|
|
85
|
+
</TooltipPrimitive.Portal>
|
|
86
|
+
</TooltipPrimitive.Root>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { Tooltip }
|
package/src/index.ts
CHANGED
|
@@ -14,3 +14,62 @@ export { Textarea } from "./components/textarea"
|
|
|
14
14
|
export { Label } from "./components/label"
|
|
15
15
|
export { Badge, badgeVariants } from "./components/badge"
|
|
16
16
|
export { Switch } from "./components/switch"
|
|
17
|
+
export { Checkbox } from "./components/checkbox"
|
|
18
|
+
export { RadioGroup, RadioGroupItem } from "./components/radio-group"
|
|
19
|
+
export { Separator } from "./components/separator"
|
|
20
|
+
export { Skeleton } from "./components/skeleton"
|
|
21
|
+
export { Spinner, spinnerVariants } from "./components/spinner"
|
|
22
|
+
export { Avatar, AvatarImage, AvatarFallback } from "./components/avatar"
|
|
23
|
+
|
|
24
|
+
// Inline, declarative status banner (persistent) — the counterpart to `toast()`.
|
|
25
|
+
// `alertVariants` ships from this directive-free module so it stays RSC-callable.
|
|
26
|
+
export { Alert, AlertTitle, AlertDescription, alertVariants } from "./components/alert"
|
|
27
|
+
export type { AlertVariant } from "./components/alert"
|
|
28
|
+
|
|
29
|
+
// Tabs — list + sliding indicator + tab + panel, on Base UI.
|
|
30
|
+
export { Tabs, TabsList, TabsIndicator, TabsTab, TabsPanel } from "./components/tabs"
|
|
31
|
+
|
|
32
|
+
// Select — dropdown listbox with items, groups, and separators, on Base UI.
|
|
33
|
+
export {
|
|
34
|
+
Select,
|
|
35
|
+
SelectTrigger,
|
|
36
|
+
SelectValue,
|
|
37
|
+
SelectContent,
|
|
38
|
+
SelectItem,
|
|
39
|
+
SelectGroup,
|
|
40
|
+
SelectGroupLabel,
|
|
41
|
+
SelectSeparator,
|
|
42
|
+
} from "./components/select"
|
|
43
|
+
|
|
44
|
+
// Overlays that plug into the `SpuntoProvider` umbrella (portal container +
|
|
45
|
+
// tooltip delay group).
|
|
46
|
+
export { Tooltip } from "./components/tooltip"
|
|
47
|
+
export type { TooltipProps } from "./components/tooltip"
|
|
48
|
+
export {
|
|
49
|
+
Dialog,
|
|
50
|
+
DialogTrigger,
|
|
51
|
+
DialogClose,
|
|
52
|
+
DialogContent,
|
|
53
|
+
DialogHeader,
|
|
54
|
+
DialogFooter,
|
|
55
|
+
DialogTitle,
|
|
56
|
+
DialogDescription,
|
|
57
|
+
} from "./components/dialog"
|
|
58
|
+
export {
|
|
59
|
+
AlertDialog,
|
|
60
|
+
AlertDialogTrigger,
|
|
61
|
+
AlertDialogClose,
|
|
62
|
+
AlertDialogContent,
|
|
63
|
+
AlertDialogHeader,
|
|
64
|
+
AlertDialogFooter,
|
|
65
|
+
AlertDialogTitle,
|
|
66
|
+
AlertDialogDescription,
|
|
67
|
+
} from "./components/alert-dialog"
|
|
68
|
+
|
|
69
|
+
// Toasts — imperative `toast()` API + the umbrella client provider that renders
|
|
70
|
+
// them. `SpuntoProvider` mounts the toast viewport, the tooltip delay group, and
|
|
71
|
+
// the overlay portal container that dialogs/selects render into.
|
|
72
|
+
export { SpuntoProvider } from "./components/spunto-provider"
|
|
73
|
+
export type { SpuntoProviderProps, ToastPosition } from "./components/spunto-provider"
|
|
74
|
+
export { toast } from "./components/toast"
|
|
75
|
+
export type { ToastFn, ToastOptions, ToastVariant, ToastAction } from "./components/toast"
|