@boyernick/standard-ui-react 0.1.1-canary.3 → 0.1.1-canary.34
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/package.json +5 -3
- package/src/accordion.tsx +5 -2
- package/src/alert-dialog.tsx +1 -1
- package/src/attachment.tsx +5 -2
- package/src/autocomplete.tsx +12 -9
- package/src/badge.tsx +73 -9
- package/src/block-editor.tsx +1747 -0
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +46 -21
- package/src/button.tsx +23 -12
- package/src/calendar.tsx +25 -11
- package/src/card.tsx +57 -14
- package/src/carousel.tsx +339 -15
- package/src/checkbox.tsx +36 -13
- package/src/code-block.tsx +66 -5
- package/src/collapsible.tsx +7 -3
- package/src/combobox.tsx +17 -15
- package/src/command.tsx +41 -14
- package/src/context-menu.tsx +6 -3
- package/src/date-picker.tsx +174 -0
- package/src/dialog.tsx +33 -10
- package/src/empty.tsx +4 -4
- package/src/field.tsx +4 -3
- package/src/filter-group.tsx +143 -0
- package/src/icons.tsx +55 -1
- package/src/illustrations.tsx +219 -141
- package/src/index.ts +263 -18
- package/src/input.tsx +8 -5
- package/src/kbd.tsx +49 -0
- package/src/lib/focus.ts +41 -0
- package/src/lib/motion.ts +16 -1
- package/src/lib/popup.ts +45 -0
- package/src/lifeline/company-icon.tsx +71 -0
- package/src/lifeline/index.ts +42 -0
- package/src/lifeline/lifeline-data.ts +97 -0
- package/src/lifeline/lifeline-desktop.tsx +204 -0
- package/src/lifeline/lifeline-event.tsx +108 -0
- package/src/lifeline/lifeline-fireworks.tsx +302 -0
- package/src/lifeline/lifeline-hover-image.tsx +269 -0
- package/src/lifeline/lifeline-icons.tsx +39 -0
- package/src/lifeline/lifeline-intro-timing.ts +105 -0
- package/src/lifeline/lifeline-labels.tsx +24 -0
- package/src/lifeline/lifeline-layout.ts +1 -0
- package/src/lifeline/lifeline-legend.tsx +31 -0
- package/src/lifeline/lifeline-lightbox.tsx +309 -0
- package/src/lifeline/lifeline-marker.tsx +183 -0
- package/src/lifeline/lifeline-people.tsx +99 -0
- package/src/lifeline/lifeline-photos.tsx +366 -0
- package/src/lifeline/lifeline-shell.tsx +135 -0
- package/src/lifeline/lifeline-utils.ts +83 -0
- package/src/lifeline/lifeline-vertical.tsx +482 -0
- package/src/lifeline/lifeline.tsx +69 -0
- package/src/lifeline/types.ts +112 -0
- package/src/lifeline/use-lifeline-intro.ts +130 -0
- package/src/lifeline/use-lifeline-scroll.ts +1011 -0
- package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
- package/src/menu.tsx +7 -4
- package/src/menubar.tsx +9 -2
- package/src/minimap.tsx +296 -0
- package/src/modal.tsx +608 -0
- package/src/navigation-menu.tsx +340 -67
- package/src/number-field.tsx +336 -61
- package/src/otp-field.tsx +4 -3
- package/src/pagination.tsx +262 -42
- package/src/password-protection.tsx +345 -0
- package/src/popover.tsx +3 -1
- package/src/preview-card.tsx +3 -1
- package/src/progress.tsx +5 -0
- package/src/questionnaire.tsx +284 -0
- package/src/radio.tsx +4 -2
- package/src/scroll-area.tsx +4 -1
- package/src/select.tsx +12 -7
- package/src/sidebar.tsx +113 -28
- package/src/slider.tsx +66 -6
- package/src/sounds.tsx +6 -10
- package/src/spinner.tsx +105 -32
- package/src/switch.tsx +4 -1
- package/src/table.tsx +82 -15
- package/src/tabs.tsx +189 -38
- package/src/text-animate.tsx +71 -28
- package/src/textarea.tsx +6 -1
- package/src/timeline.tsx +378 -0
- package/src/toast.tsx +214 -35
- package/src/toggle.tsx +4 -2
- package/src/toolbar.tsx +12 -7
- package/src/tooltip.tsx +43 -3
- package/src/video-player.tsx +396 -127
- package/src/image-modal.tsx +0 -110
- package/src/markdown-editor.tsx +0 -302
package/src/timeline.tsx
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
type ComponentProps,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type WheelEvent,
|
|
9
|
+
useContext,
|
|
10
|
+
} from "react"
|
|
11
|
+
import { cn } from "./lib/cn"
|
|
12
|
+
import { focusRing, focusRingBorder } from "./lib/focus"
|
|
13
|
+
import { motion } from "./lib/motion"
|
|
14
|
+
|
|
15
|
+
const timelineVariants = cva("group/timeline relative text-fg-primary", {
|
|
16
|
+
variants: {
|
|
17
|
+
orientation: {
|
|
18
|
+
vertical: "w-full",
|
|
19
|
+
horizontal: cn(
|
|
20
|
+
"w-full min-h-0 overflow-x-auto overscroll-x-contain scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:rounded-md",
|
|
21
|
+
focusRingBorder,
|
|
22
|
+
focusRing,
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
orientation: "vertical",
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const timelineTrackVariants = cva("relative m-0 list-none p-0", {
|
|
32
|
+
variants: {
|
|
33
|
+
orientation: {
|
|
34
|
+
vertical:
|
|
35
|
+
"before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[calc(4.5rem+0.75rem+0.5rem)] before:w-px before:-translate-x-1/2 before:border-l before:border-dashed before:border-border-primary before:content-['']",
|
|
36
|
+
horizontal:
|
|
37
|
+
"flex w-max items-start before:pointer-events-none before:absolute before:inset-x-0 before:top-[var(--timeline-rail)] before:border-t before:border-dashed before:border-border-primary before:content-['']",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: {
|
|
41
|
+
orientation: "vertical",
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const timelineItemVariants = cva("group/timeline-item relative", {
|
|
46
|
+
variants: {
|
|
47
|
+
orientation: {
|
|
48
|
+
vertical: "grid grid-cols-[4.5rem_1rem_minmax(0,1fr)] gap-x-3 pb-8 last:pb-0",
|
|
49
|
+
horizontal:
|
|
50
|
+
"w-[min(18rem,70vw)] shrink-0 pr-10 last:pr-0 [--timeline-rail:2.75rem]",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: {
|
|
54
|
+
orientation: "vertical",
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const timelineMarkerVariants = cva("z-[1] shrink-0", {
|
|
59
|
+
variants: {
|
|
60
|
+
orientation: {
|
|
61
|
+
vertical: "col-start-2 row-start-1 mt-2 justify-self-center",
|
|
62
|
+
horizontal:
|
|
63
|
+
"absolute top-[var(--timeline-rail)] left-0 -translate-y-1/2",
|
|
64
|
+
},
|
|
65
|
+
shape: {
|
|
66
|
+
tick: "bg-fg-quaternary group-hover/timeline-item:bg-fg-tertiary",
|
|
67
|
+
dot: "size-2.5 rounded-full border-2 border-background-primary shadow-hairline",
|
|
68
|
+
},
|
|
69
|
+
tone: {
|
|
70
|
+
neutral: "",
|
|
71
|
+
accent: "",
|
|
72
|
+
info: "",
|
|
73
|
+
success: "",
|
|
74
|
+
warning: "",
|
|
75
|
+
critical: "",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
compoundVariants: [
|
|
79
|
+
{
|
|
80
|
+
shape: "tick",
|
|
81
|
+
orientation: "vertical",
|
|
82
|
+
class: "h-px w-2.5",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
shape: "tick",
|
|
86
|
+
orientation: "horizontal",
|
|
87
|
+
class: "h-2.5 w-px",
|
|
88
|
+
},
|
|
89
|
+
{ shape: "tick", tone: "neutral", class: "bg-fg-quaternary" },
|
|
90
|
+
{ shape: "tick", tone: "accent", class: "bg-brand-primary" },
|
|
91
|
+
{ shape: "tick", tone: "info", class: "bg-status-info" },
|
|
92
|
+
{ shape: "tick", tone: "success", class: "bg-status-success" },
|
|
93
|
+
{ shape: "tick", tone: "warning", class: "bg-status-warning" },
|
|
94
|
+
{ shape: "tick", tone: "critical", class: "bg-status-critical" },
|
|
95
|
+
{ shape: "dot", tone: "neutral", class: "bg-fg-tertiary" },
|
|
96
|
+
{ shape: "dot", tone: "accent", class: "bg-brand-primary" },
|
|
97
|
+
{ shape: "dot", tone: "info", class: "bg-status-info" },
|
|
98
|
+
{ shape: "dot", tone: "success", class: "bg-status-success" },
|
|
99
|
+
{ shape: "dot", tone: "warning", class: "bg-status-warning" },
|
|
100
|
+
{ shape: "dot", tone: "critical", class: "bg-status-critical" },
|
|
101
|
+
],
|
|
102
|
+
defaultVariants: {
|
|
103
|
+
orientation: "vertical",
|
|
104
|
+
shape: "tick",
|
|
105
|
+
tone: "neutral",
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const timelineTimeVariants = cva(
|
|
110
|
+
cn(
|
|
111
|
+
"text-sm-strong block tabular-nums text-fg-secondary",
|
|
112
|
+
motion.colors,
|
|
113
|
+
"group-hover/timeline-item:text-fg-primary",
|
|
114
|
+
),
|
|
115
|
+
{
|
|
116
|
+
variants: {
|
|
117
|
+
orientation: {
|
|
118
|
+
vertical: "col-start-1 row-start-1 pt-0.5 text-right leading-5",
|
|
119
|
+
horizontal: "mb-6 h-5 leading-5",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
defaultVariants: {
|
|
123
|
+
orientation: "vertical",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
type TimelineOrientation = "horizontal" | "vertical"
|
|
129
|
+
|
|
130
|
+
const TimelineContext = createContext<TimelineOrientation>("vertical")
|
|
131
|
+
|
|
132
|
+
export type TimelineProps = ComponentProps<"div"> &
|
|
133
|
+
VariantProps<typeof timelineVariants> & {
|
|
134
|
+
/** Distance moved by the horizontal timeline's arrow keys. */
|
|
135
|
+
scrollStep?: number
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type TimelineTrackProps = ComponentProps<"ol">
|
|
139
|
+
export type TimelineItemProps = ComponentProps<"li">
|
|
140
|
+
export type TimelineMarkerProps = ComponentProps<"span"> &
|
|
141
|
+
VariantProps<typeof timelineMarkerVariants>
|
|
142
|
+
export type TimelineTimeProps = ComponentProps<"time">
|
|
143
|
+
export type TimelineContentProps = ComponentProps<"div">
|
|
144
|
+
export type TimelineTitleProps = ComponentProps<"h3">
|
|
145
|
+
export type TimelineDescriptionProps = ComponentProps<"p">
|
|
146
|
+
export type TimelineMediaProps = ComponentProps<"div">
|
|
147
|
+
|
|
148
|
+
export const Timeline = ({
|
|
149
|
+
orientation = "vertical",
|
|
150
|
+
scrollStep = 256,
|
|
151
|
+
className,
|
|
152
|
+
children,
|
|
153
|
+
tabIndex,
|
|
154
|
+
onKeyDown,
|
|
155
|
+
onWheel,
|
|
156
|
+
"aria-label": ariaLabel = "Timeline",
|
|
157
|
+
...props
|
|
158
|
+
}: TimelineProps) => {
|
|
159
|
+
const resolvedOrientation = orientation ?? "vertical"
|
|
160
|
+
|
|
161
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
162
|
+
onKeyDown?.(event)
|
|
163
|
+
if (
|
|
164
|
+
event.defaultPrevented ||
|
|
165
|
+
resolvedOrientation !== "horizontal" ||
|
|
166
|
+
event.target !== event.currentTarget
|
|
167
|
+
) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const node = event.currentTarget
|
|
172
|
+
const prefersReducedMotion = window.matchMedia(
|
|
173
|
+
"(prefers-reduced-motion: reduce)",
|
|
174
|
+
).matches
|
|
175
|
+
const behavior = prefersReducedMotion ? "auto" : "smooth"
|
|
176
|
+
|
|
177
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
178
|
+
event.preventDefault()
|
|
179
|
+
node.scrollBy({
|
|
180
|
+
left: event.key === "ArrowRight" ? scrollStep : -scrollStep,
|
|
181
|
+
behavior,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (event.key === "Home" || event.key === "End") {
|
|
186
|
+
event.preventDefault()
|
|
187
|
+
node.scrollTo({
|
|
188
|
+
left: event.key === "Home" ? 0 : node.scrollWidth,
|
|
189
|
+
behavior,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleWheel = (event: WheelEvent<HTMLDivElement>) => {
|
|
195
|
+
onWheel?.(event)
|
|
196
|
+
if (
|
|
197
|
+
event.defaultPrevented ||
|
|
198
|
+
resolvedOrientation !== "horizontal" ||
|
|
199
|
+
Math.abs(event.deltaX) >= Math.abs(event.deltaY)
|
|
200
|
+
) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const node = event.currentTarget
|
|
205
|
+
const maxScrollLeft = node.scrollWidth - node.clientWidth
|
|
206
|
+
const canScroll =
|
|
207
|
+
event.deltaY > 0 ? node.scrollLeft < maxScrollLeft : node.scrollLeft > 0
|
|
208
|
+
|
|
209
|
+
if (canScroll) {
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
node.scrollLeft += event.deltaY
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<TimelineContext.Provider value={resolvedOrientation}>
|
|
217
|
+
<div
|
|
218
|
+
data-slot="timeline"
|
|
219
|
+
data-orientation={resolvedOrientation}
|
|
220
|
+
role="region"
|
|
221
|
+
aria-label={ariaLabel}
|
|
222
|
+
tabIndex={
|
|
223
|
+
tabIndex ?? (resolvedOrientation === "horizontal" ? 0 : undefined)
|
|
224
|
+
}
|
|
225
|
+
className={cn(
|
|
226
|
+
timelineVariants({ orientation: resolvedOrientation }),
|
|
227
|
+
className,
|
|
228
|
+
)}
|
|
229
|
+
onKeyDown={handleKeyDown}
|
|
230
|
+
onWheel={handleWheel}
|
|
231
|
+
{...props}
|
|
232
|
+
>
|
|
233
|
+
{children}
|
|
234
|
+
</div>
|
|
235
|
+
</TimelineContext.Provider>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const TimelineTrack = ({
|
|
240
|
+
className,
|
|
241
|
+
...props
|
|
242
|
+
}: TimelineTrackProps) => {
|
|
243
|
+
const orientation = useContext(TimelineContext)
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<ol
|
|
247
|
+
data-slot="timeline-track"
|
|
248
|
+
data-orientation={orientation}
|
|
249
|
+
className={cn(
|
|
250
|
+
timelineTrackVariants({ orientation }),
|
|
251
|
+
orientation === "horizontal" && "[--timeline-rail:2.75rem]",
|
|
252
|
+
className,
|
|
253
|
+
)}
|
|
254
|
+
{...props}
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const TimelineItem = ({
|
|
260
|
+
className,
|
|
261
|
+
...props
|
|
262
|
+
}: TimelineItemProps) => {
|
|
263
|
+
const orientation = useContext(TimelineContext)
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<li
|
|
267
|
+
data-slot="timeline-item"
|
|
268
|
+
data-orientation={orientation}
|
|
269
|
+
className={cn(timelineItemVariants({ orientation }), className)}
|
|
270
|
+
{...props}
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const TimelineMarker = ({
|
|
276
|
+
className,
|
|
277
|
+
tone,
|
|
278
|
+
shape,
|
|
279
|
+
...props
|
|
280
|
+
}: TimelineMarkerProps) => {
|
|
281
|
+
const orientation = useContext(TimelineContext)
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<span
|
|
285
|
+
data-slot="timeline-marker"
|
|
286
|
+
aria-hidden
|
|
287
|
+
className={cn(
|
|
288
|
+
timelineMarkerVariants({ orientation, shape, tone }),
|
|
289
|
+
motion.colors,
|
|
290
|
+
className,
|
|
291
|
+
)}
|
|
292
|
+
{...props}
|
|
293
|
+
/>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const TimelineTime = ({
|
|
298
|
+
className,
|
|
299
|
+
...props
|
|
300
|
+
}: TimelineTimeProps) => {
|
|
301
|
+
const orientation = useContext(TimelineContext)
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<time
|
|
305
|
+
data-slot="timeline-time"
|
|
306
|
+
className={cn(timelineTimeVariants({ orientation }), className)}
|
|
307
|
+
{...props}
|
|
308
|
+
/>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const TimelineContent = ({
|
|
313
|
+
className,
|
|
314
|
+
...props
|
|
315
|
+
}: TimelineContentProps) => {
|
|
316
|
+
const orientation = useContext(TimelineContext)
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
data-slot="timeline-content"
|
|
321
|
+
className={cn(
|
|
322
|
+
"min-w-0",
|
|
323
|
+
orientation === "vertical" && "col-start-3 row-start-1",
|
|
324
|
+
orientation === "horizontal" && "pt-6",
|
|
325
|
+
className,
|
|
326
|
+
)}
|
|
327
|
+
{...props}
|
|
328
|
+
/>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const TimelineTitle = ({
|
|
333
|
+
className,
|
|
334
|
+
...props
|
|
335
|
+
}: TimelineTitleProps) => (
|
|
336
|
+
<h3
|
|
337
|
+
data-slot="timeline-title"
|
|
338
|
+
className={cn(
|
|
339
|
+
"text-sm-strong text-fg-primary",
|
|
340
|
+
motion.colors,
|
|
341
|
+
className,
|
|
342
|
+
)}
|
|
343
|
+
{...props}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
export const TimelineDescription = ({
|
|
348
|
+
className,
|
|
349
|
+
...props
|
|
350
|
+
}: TimelineDescriptionProps) => (
|
|
351
|
+
<p
|
|
352
|
+
data-slot="timeline-description"
|
|
353
|
+
className={cn("text-sm mt-1 leading-relaxed text-fg-secondary", className)}
|
|
354
|
+
{...props}
|
|
355
|
+
/>
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
export const TimelineMedia = ({
|
|
359
|
+
className,
|
|
360
|
+
...props
|
|
361
|
+
}: TimelineMediaProps) => (
|
|
362
|
+
<div
|
|
363
|
+
data-slot="timeline-media"
|
|
364
|
+
className={cn(
|
|
365
|
+
"mt-4 overflow-hidden rounded-xl ring-1 ring-border-primary [&>img]:block [&>img]:aspect-4/3 [&>img]:w-full [&>img]:object-cover",
|
|
366
|
+
className,
|
|
367
|
+
)}
|
|
368
|
+
{...props}
|
|
369
|
+
/>
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
export {
|
|
373
|
+
timelineVariants,
|
|
374
|
+
timelineItemVariants,
|
|
375
|
+
timelineMarkerVariants,
|
|
376
|
+
timelineTimeVariants,
|
|
377
|
+
timelineTrackVariants,
|
|
378
|
+
}
|
package/src/toast.tsx
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { Toast as BaseToast } from "@base-ui/react/toast"
|
|
4
|
-
import type
|
|
5
|
-
import {
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
type ComponentProps,
|
|
9
|
+
} from "react"
|
|
10
|
+
import {
|
|
11
|
+
IconCircleCheck,
|
|
12
|
+
IconCircleInfo,
|
|
13
|
+
IconCrossSmall,
|
|
14
|
+
IconExclamationCircle,
|
|
15
|
+
IconExclamationTriangle,
|
|
16
|
+
} from "./icons"
|
|
17
|
+
import { Spinner } from "./spinner"
|
|
6
18
|
import { cn } from "./lib/cn"
|
|
19
|
+
import { focusRing, focusRingBorder } from "./lib/focus"
|
|
7
20
|
import { motion } from "./lib/motion"
|
|
8
21
|
|
|
22
|
+
export type ToastPlacement = "top-center" | "bottom-right"
|
|
23
|
+
|
|
24
|
+
const ToastPlacementContext = createContext<ToastPlacement>("top-center")
|
|
25
|
+
|
|
9
26
|
export type ToastProviderProps = ComponentProps<typeof BaseToast.Provider>
|
|
10
27
|
export type ToastPortalProps = ComponentProps<typeof BaseToast.Portal>
|
|
11
|
-
export type ToastViewportProps = ComponentProps<typeof BaseToast.Viewport>
|
|
12
|
-
|
|
28
|
+
export type ToastViewportProps = ComponentProps<typeof BaseToast.Viewport> &
|
|
29
|
+
VariantProps<typeof toastViewportVariants>
|
|
30
|
+
export type ToastRootProps = ComponentProps<typeof BaseToast.Root> &
|
|
31
|
+
VariantProps<typeof toastRootVariants>
|
|
13
32
|
export type ToastContentProps = ComponentProps<typeof BaseToast.Content>
|
|
14
33
|
export type ToastTitleProps = ComponentProps<typeof BaseToast.Title>
|
|
15
34
|
export type ToastDescriptionProps = ComponentProps<typeof BaseToast.Description>
|
|
@@ -18,6 +37,10 @@ export type ToastCloseProps = ComponentProps<typeof BaseToast.Close>
|
|
|
18
37
|
export type ToastPositionerProps = ComponentProps<typeof BaseToast.Positioner>
|
|
19
38
|
export type ToastArrowProps = ComponentProps<typeof BaseToast.Arrow>
|
|
20
39
|
|
|
40
|
+
/** The types that carry a glyph. Anything else renders no icon. */
|
|
41
|
+
export type ToastType = "success" | "error" | "warning" | "info" | "loading"
|
|
42
|
+
export type ToastIconProps = ComponentProps<"span"> & { type?: string }
|
|
43
|
+
|
|
21
44
|
export const ToastProvider = (props: ToastProviderProps) => (
|
|
22
45
|
<BaseToast.Provider {...props} />
|
|
23
46
|
)
|
|
@@ -26,37 +49,182 @@ export const ToastPortal = (props: ToastPortalProps) => (
|
|
|
26
49
|
<BaseToast.Portal {...props} />
|
|
27
50
|
)
|
|
28
51
|
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
export const toastViewportVariants = cva(
|
|
53
|
+
// No flex column: the toasts inside are absolutely positioned so they pile
|
|
54
|
+
// up as a stack. In flow they would march down past the edge.
|
|
55
|
+
"fixed z-[100] w-[min(100vw-2rem,28rem)] outline-none",
|
|
56
|
+
{
|
|
57
|
+
variants: {
|
|
58
|
+
placement: {
|
|
59
|
+
"top-center": "top-8 left-1/2 -translate-x-1/2",
|
|
60
|
+
"bottom-right": "right-4 bottom-4 sm:right-8 sm:bottom-8",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: { placement: "top-center" },
|
|
64
|
+
},
|
|
37
65
|
)
|
|
38
66
|
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
export const ToastViewport = ({
|
|
68
|
+
className,
|
|
69
|
+
placement = "top-center",
|
|
70
|
+
children,
|
|
71
|
+
...props
|
|
72
|
+
}: ToastViewportProps) => (
|
|
73
|
+
<ToastPlacementContext.Provider value={placement ?? "top-center"}>
|
|
74
|
+
<BaseToast.Viewport
|
|
75
|
+
data-placement={placement}
|
|
76
|
+
className={cn(toastViewportVariants({ placement }), className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</BaseToast.Viewport>
|
|
81
|
+
</ToastPlacementContext.Provider>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
/** Shared chrome. Placement owns where the stack anchors and how cards move. */
|
|
85
|
+
const toastRootVariants = cva(
|
|
86
|
+
cn(
|
|
87
|
+
"absolute right-0 left-0 z-[calc(1000-var(--toast-index))] box-border w-full",
|
|
88
|
+
"[--gap:0.625rem] [--peek:0.875rem]",
|
|
89
|
+
"[--scale:calc(max(0,1-(var(--toast-index)*0.05)))]",
|
|
90
|
+
"[--height:var(--toast-frontmost-height,var(--toast-height))]",
|
|
91
|
+
// A row, not an overlay: the action and the close sit inline at the right,
|
|
92
|
+
// so the close cannot be absolutely positioned over reserved padding.
|
|
93
|
+
"flex items-start gap-2 rounded-xl border py-2.5 pr-2 pl-4 shadow-lg outline-none select-none",
|
|
94
|
+
"h-[var(--height)] data-expanded:h-[var(--toast-height)]",
|
|
95
|
+
// Bridges the gap between cards so crossing it does not collapse the fan.
|
|
96
|
+
"after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
|
97
|
+
"transition-[transform,opacity,height] duration-[var(--duration-md)] ease-enter motion-reduce:transition-none",
|
|
98
|
+
"data-ending-style:opacity-0 data-limited:opacity-0",
|
|
99
|
+
),
|
|
100
|
+
{
|
|
101
|
+
variants: {
|
|
102
|
+
variant: {
|
|
103
|
+
// The page's own surface. The border stays transparent so the edge is
|
|
104
|
+
// `shadow-lg`'s hairline, the same one every dropdown carries.
|
|
105
|
+
default: "border-transparent bg-surface text-fg-primary",
|
|
106
|
+
// Flipped against the page. Here the hairline is invisible — a black
|
|
107
|
+
// line on a near-black card — so the edge is drawn with the inverted
|
|
108
|
+
// foreground, which flips with the theme alongside the surface.
|
|
109
|
+
inverted: "border-fg-inverted/10 bg-surface-inverted text-fg-inverted",
|
|
110
|
+
},
|
|
111
|
+
placement: {
|
|
112
|
+
/** Pinned to the top of the viewport; behind cards peek below. */
|
|
113
|
+
"top-center": cn(
|
|
114
|
+
"top-0 origin-top",
|
|
115
|
+
"[--offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y,0px))]",
|
|
116
|
+
"[transform:translateX(var(--toast-swipe-movement-x,0px))_translateY(calc(var(--toast-swipe-movement-y,0px)+(var(--toast-index)*var(--peek))))_scale(var(--scale))]",
|
|
117
|
+
"data-expanded:[transform:translateX(var(--toast-swipe-movement-x,0px))_translateY(var(--offset-y))]",
|
|
118
|
+
"data-starting-style:[transform:translateY(-150%)]",
|
|
119
|
+
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]",
|
|
120
|
+
),
|
|
121
|
+
/** Pinned to the bottom-right; behind cards peek above. */
|
|
122
|
+
"bottom-right": cn(
|
|
123
|
+
"bottom-0 origin-bottom",
|
|
124
|
+
"[--shrink:calc(1-var(--scale))]",
|
|
125
|
+
"[--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y,0px))]",
|
|
126
|
+
"[transform:translateX(var(--toast-swipe-movement-x,0px))_translateY(calc(var(--toast-swipe-movement-y,0px)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]",
|
|
127
|
+
"data-expanded:[transform:translateX(var(--toast-swipe-movement-x,0px))_translateY(var(--offset-y))]",
|
|
128
|
+
"data-starting-style:[transform:translateY(150%)]",
|
|
129
|
+
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
defaultVariants: { variant: "default", placement: "top-center" },
|
|
134
|
+
},
|
|
54
135
|
)
|
|
55
136
|
|
|
137
|
+
/** The variant is published as `data-variant` rather than passed down: the
|
|
138
|
+
* title, description, action, close and icon all have to follow the surface,
|
|
139
|
+
* and a data attribute lets each one carry its own pair of rules instead of
|
|
140
|
+
* every caller threading a prop through five components.
|
|
141
|
+
*
|
|
142
|
+
* Placement defaults from the nearest `ToastViewport` so the stack and the
|
|
143
|
+
* corner stay in sync without repeating the prop on every root. */
|
|
144
|
+
export const ToastRoot = ({
|
|
145
|
+
className,
|
|
146
|
+
variant = "default",
|
|
147
|
+
placement: placementProp,
|
|
148
|
+
...props
|
|
149
|
+
}: ToastRootProps) => {
|
|
150
|
+
const placementFromViewport = useContext(ToastPlacementContext)
|
|
151
|
+
const placement = placementProp ?? placementFromViewport
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<BaseToast.Root
|
|
155
|
+
data-variant={variant}
|
|
156
|
+
data-placement={placement}
|
|
157
|
+
className={cn(
|
|
158
|
+
"group/toast",
|
|
159
|
+
toastRootVariants({ variant, placement }),
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { toastRootVariants }
|
|
168
|
+
|
|
169
|
+
const toastGlyphs = {
|
|
170
|
+
success: {
|
|
171
|
+
Icon: IconCircleCheck,
|
|
172
|
+
tone: `text-status-success group-data-[variant=inverted]/toast:text-status-success-inverted`,
|
|
173
|
+
},
|
|
174
|
+
error: {
|
|
175
|
+
Icon: IconExclamationCircle,
|
|
176
|
+
tone: `text-status-critical group-data-[variant=inverted]/toast:text-status-critical-inverted`,
|
|
177
|
+
},
|
|
178
|
+
warning: {
|
|
179
|
+
Icon: IconExclamationTriangle,
|
|
180
|
+
tone: `text-status-warning group-data-[variant=inverted]/toast:text-status-warning-inverted`,
|
|
181
|
+
},
|
|
182
|
+
info: {
|
|
183
|
+
Icon: IconCircleInfo,
|
|
184
|
+
tone: `text-status-info group-data-[variant=inverted]/toast:text-status-info-inverted`,
|
|
185
|
+
},
|
|
186
|
+
} as const
|
|
187
|
+
|
|
188
|
+
/** The glyph tracks the title, not the block. The row is `items-start` so the
|
|
189
|
+
* glyph and the content column measure from the same top edge, and `mt-0.5`
|
|
190
|
+
* is the half-leading above a 14px title in a 20px line — which centres the
|
|
191
|
+
* 16px glyph on the title whether or not a description follows it.
|
|
192
|
+
*
|
|
193
|
+
* The margin is on both axes, not just the top. Every item beside the content
|
|
194
|
+
* column is collapsed to a 20px outer box — the height of the title's line —
|
|
195
|
+
* so it centres on that line while contributing nothing extra to the row. A
|
|
196
|
+
* top-only offset lines the item up but leaves its full height in the box, and
|
|
197
|
+
* the tallest control then stretches the toast downward and everything reads
|
|
198
|
+
* high. Hit areas are untouched: the boxes overflow, they do not shrink. */
|
|
199
|
+
const ICON_ALIGN = "my-0.5 flex shrink-0"
|
|
200
|
+
|
|
201
|
+
/** The glyph for a toast's type. Each tone carries both surfaces: the plain
|
|
202
|
+
* status tokens are tuned for the page, and the `-inverted` pair for a card
|
|
203
|
+
* that flips against it. Pass `toast.type` from the render loop — Base UI
|
|
204
|
+
* gives no way to read the toast from inside the root. */
|
|
205
|
+
export const ToastIcon = ({ type, className, ...props }: ToastIconProps) => {
|
|
206
|
+
if (type === "loading") {
|
|
207
|
+
return (
|
|
208
|
+
<span className={cn(ICON_ALIGN, className)} {...props}>
|
|
209
|
+
<Spinner size="sm" />
|
|
210
|
+
</span>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const glyph = toastGlyphs[type as keyof typeof toastGlyphs]
|
|
215
|
+
if (!glyph) return null
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<span className={cn(ICON_ALIGN, glyph.tone, className)} {...props}>
|
|
219
|
+
<glyph.Icon size={16} className="size-4" aria-hidden />
|
|
220
|
+
</span>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
56
224
|
export const ToastContent = ({ className, ...props }: ToastContentProps) => (
|
|
57
225
|
<BaseToast.Content
|
|
58
226
|
className={cn(
|
|
59
|
-
"flex flex-col gap-
|
|
227
|
+
"flex min-w-0 flex-1 flex-col gap-0.5 overflow-hidden transition-opacity duration-[var(--duration-sm)]",
|
|
60
228
|
"data-behind:pointer-events-none data-behind:opacity-0 data-expanded:data-behind:opacity-100",
|
|
61
229
|
className,
|
|
62
230
|
)}
|
|
@@ -66,7 +234,10 @@ export const ToastContent = ({ className, ...props }: ToastContentProps) => (
|
|
|
66
234
|
|
|
67
235
|
export const ToastTitle = ({ className, ...props }: ToastTitleProps) => (
|
|
68
236
|
<BaseToast.Title
|
|
69
|
-
className={cn(
|
|
237
|
+
className={cn(
|
|
238
|
+
"text-sm-strong text-fg-primary group-data-[variant=inverted]/toast:text-fg-inverted",
|
|
239
|
+
className,
|
|
240
|
+
)}
|
|
70
241
|
{...props}
|
|
71
242
|
/>
|
|
72
243
|
)
|
|
@@ -76,7 +247,10 @@ export const ToastDescription = ({
|
|
|
76
247
|
...props
|
|
77
248
|
}: ToastDescriptionProps) => (
|
|
78
249
|
<BaseToast.Description
|
|
79
|
-
className={cn(
|
|
250
|
+
className={cn(
|
|
251
|
+
"text-sm text-fg-secondary group-data-[variant=inverted]/toast:text-fg-inverted-secondary",
|
|
252
|
+
className,
|
|
253
|
+
)}
|
|
80
254
|
{...props}
|
|
81
255
|
/>
|
|
82
256
|
)
|
|
@@ -84,9 +258,11 @@ export const ToastDescription = ({
|
|
|
84
258
|
export const ToastAction = ({ className, ...props }: ToastActionProps) => (
|
|
85
259
|
<BaseToast.Action
|
|
86
260
|
className={cn(
|
|
87
|
-
"
|
|
261
|
+
"text-xs -my-1 inline-flex h-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-border-secondary px-2.5 text-fg-primary",
|
|
88
262
|
motion.colors,
|
|
89
|
-
|
|
263
|
+
focusRing,
|
|
264
|
+
"hover:bg-background-tertiary",
|
|
265
|
+
"group-data-[variant=inverted]/toast:border-fg-inverted/15 group-data-[variant=inverted]/toast:text-fg-inverted group-data-[variant=inverted]/toast:hover:bg-fg-inverted/10 group-data-[variant=inverted]/toast:focus-visible:border-fg-inverted/50 group-data-[variant=inverted]/toast:focus-visible:ring-fg-inverted/50",
|
|
90
266
|
className,
|
|
91
267
|
)}
|
|
92
268
|
{...props}
|
|
@@ -100,15 +276,18 @@ export const ToastClose = ({
|
|
|
100
276
|
}: ToastCloseProps) => (
|
|
101
277
|
<BaseToast.Close
|
|
102
278
|
className={cn(
|
|
103
|
-
"
|
|
279
|
+
"-my-0.5 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-fg-tertiary",
|
|
280
|
+
focusRingBorder,
|
|
104
281
|
motion.colors,
|
|
105
|
-
|
|
282
|
+
focusRing,
|
|
283
|
+
"hover:bg-background-tertiary hover:text-fg-primary",
|
|
284
|
+
"group-data-[variant=inverted]/toast:text-fg-inverted/50 group-data-[variant=inverted]/toast:hover:bg-fg-inverted/10 group-data-[variant=inverted]/toast:hover:text-fg-inverted group-data-[variant=inverted]/toast:focus-visible:border-fg-inverted/50 group-data-[variant=inverted]/toast:focus-visible:ring-fg-inverted/50",
|
|
106
285
|
className,
|
|
107
286
|
)}
|
|
108
287
|
{...props}
|
|
109
288
|
>
|
|
110
289
|
{children ?? (
|
|
111
|
-
<IconCrossSmall size={
|
|
290
|
+
<IconCrossSmall size={18} className="size-4.5" aria-hidden />
|
|
112
291
|
)}
|
|
113
292
|
</BaseToast.Close>
|
|
114
293
|
)
|