@boyernick/standard-ui-react 0.1.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 +9 -0
- package/package.json +46 -0
- package/src/accordion.tsx +84 -0
- package/src/alert-dialog.tsx +101 -0
- package/src/attachment.tsx +138 -0
- package/src/autocomplete.tsx +281 -0
- package/src/avatar.tsx +56 -0
- package/src/badge.tsx +28 -0
- package/src/brand.tsx +70 -0
- package/src/breadcrumb.tsx +81 -0
- package/src/button.tsx +129 -0
- package/src/calendar.tsx +84 -0
- package/src/card.tsx +54 -0
- package/src/carousel.tsx +253 -0
- package/src/chart.tsx +185 -0
- package/src/checkbox-group.tsx +14 -0
- package/src/checkbox.tsx +31 -0
- package/src/code-block.tsx +124 -0
- package/src/collapsible.tsx +61 -0
- package/src/combobox.tsx +324 -0
- package/src/command.tsx +377 -0
- package/src/context-menu.tsx +250 -0
- package/src/dialog.tsx +78 -0
- package/src/drawer.tsx +156 -0
- package/src/empty.tsx +52 -0
- package/src/field.tsx +75 -0
- package/src/fieldset.tsx +25 -0
- package/src/form.tsx +14 -0
- package/src/icons.tsx +91 -0
- package/src/illustrations.tsx +167 -0
- package/src/image-modal.tsx +110 -0
- package/src/index.ts +833 -0
- package/src/input.tsx +68 -0
- package/src/lib/cn.ts +3 -0
- package/src/lib/motion.ts +23 -0
- package/src/markdown-editor.tsx +302 -0
- package/src/menu.tsx +205 -0
- package/src/menubar.tsx +22 -0
- package/src/meter.tsx +61 -0
- package/src/navigation-menu.tsx +217 -0
- package/src/number-field.tsx +119 -0
- package/src/orb.tsx +33 -0
- package/src/otp-field.tsx +45 -0
- package/src/pagination.tsx +91 -0
- package/src/popover.tsx +92 -0
- package/src/preview-card.tsx +103 -0
- package/src/progress.tsx +60 -0
- package/src/radio.tsx +43 -0
- package/src/scroll-area.tsx +67 -0
- package/src/select.tsx +161 -0
- package/src/separator.tsx +17 -0
- package/src/sidebar.tsx +82 -0
- package/src/skeleton.tsx +32 -0
- package/src/slider.tsx +56 -0
- package/src/sounds.tsx +270 -0
- package/src/spinner.tsx +45 -0
- package/src/switch.tsx +19 -0
- package/src/table.tsx +81 -0
- package/src/tabs.tsx +62 -0
- package/src/text-animate.tsx +155 -0
- package/src/textarea.tsx +58 -0
- package/src/ticker.tsx +74 -0
- package/src/toast.tsx +142 -0
- package/src/toggle.tsx +32 -0
- package/src/toolbar.tsx +75 -0
- package/src/tooltip.tsx +55 -0
- package/src/video-player.tsx +241 -0
package/src/ticker.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, CSSProperties } from "react"
|
|
4
|
+
import { cn } from "./lib/cn"
|
|
5
|
+
|
|
6
|
+
export type TickerProps = ComponentProps<"div"> & {
|
|
7
|
+
/** Animation duration for one full loop. */
|
|
8
|
+
duration?: number
|
|
9
|
+
/** Pause scrolling while hovered or focused. */
|
|
10
|
+
pauseOnHover?: boolean
|
|
11
|
+
/** Reverse scroll direction. */
|
|
12
|
+
reverse?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TickerItemProps = ComponentProps<"div">
|
|
16
|
+
|
|
17
|
+
export const Ticker = ({
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
duration = 28,
|
|
21
|
+
pauseOnHover = true,
|
|
22
|
+
reverse = false,
|
|
23
|
+
style,
|
|
24
|
+
...props
|
|
25
|
+
}: TickerProps) => {
|
|
26
|
+
const mergedStyle = {
|
|
27
|
+
...style,
|
|
28
|
+
"--ticker-duration": `${duration}s`,
|
|
29
|
+
} as CSSProperties
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
data-slot="ticker"
|
|
34
|
+
role="marquee"
|
|
35
|
+
aria-label="Scrolling updates"
|
|
36
|
+
className={cn(
|
|
37
|
+
"group relative overflow-hidden rounded-xl border border-border-primary bg-surface",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
style={mergedStyle}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
"flex w-max gap-8 py-3 whitespace-nowrap",
|
|
46
|
+
"animate-[ticker-scroll_var(--ticker-duration)_linear_infinite]",
|
|
47
|
+
"motion-reduce:animate-none",
|
|
48
|
+
reverse && "[animation-direction:reverse]",
|
|
49
|
+
pauseOnHover &&
|
|
50
|
+
"group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]",
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex shrink-0 items-center gap-8 px-4">{children}</div>
|
|
54
|
+
<div
|
|
55
|
+
className="flex shrink-0 items-center gap-8 px-4"
|
|
56
|
+
aria-hidden
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const TickerItem = ({ className, ...props }: TickerItemProps) => (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="ticker-item"
|
|
68
|
+
className={cn(
|
|
69
|
+
"inline-flex items-center gap-2 text-sm text-fg-secondary",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
)
|
package/src/toast.tsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toast as BaseToast } from "@base-ui/react/toast"
|
|
4
|
+
import type { ComponentProps } from "react"
|
|
5
|
+
import { IconCrossSmall } from "./icons"
|
|
6
|
+
import { cn } from "./lib/cn"
|
|
7
|
+
import { motion } from "./lib/motion"
|
|
8
|
+
|
|
9
|
+
export type ToastProviderProps = ComponentProps<typeof BaseToast.Provider>
|
|
10
|
+
export type ToastPortalProps = ComponentProps<typeof BaseToast.Portal>
|
|
11
|
+
export type ToastViewportProps = ComponentProps<typeof BaseToast.Viewport>
|
|
12
|
+
export type ToastRootProps = ComponentProps<typeof BaseToast.Root>
|
|
13
|
+
export type ToastContentProps = ComponentProps<typeof BaseToast.Content>
|
|
14
|
+
export type ToastTitleProps = ComponentProps<typeof BaseToast.Title>
|
|
15
|
+
export type ToastDescriptionProps = ComponentProps<typeof BaseToast.Description>
|
|
16
|
+
export type ToastActionProps = ComponentProps<typeof BaseToast.Action>
|
|
17
|
+
export type ToastCloseProps = ComponentProps<typeof BaseToast.Close>
|
|
18
|
+
export type ToastPositionerProps = ComponentProps<typeof BaseToast.Positioner>
|
|
19
|
+
export type ToastArrowProps = ComponentProps<typeof BaseToast.Arrow>
|
|
20
|
+
|
|
21
|
+
export const ToastProvider = (props: ToastProviderProps) => (
|
|
22
|
+
<BaseToast.Provider {...props} />
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const ToastPortal = (props: ToastPortalProps) => (
|
|
26
|
+
<BaseToast.Portal {...props} />
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export const ToastViewport = ({ className, ...props }: ToastViewportProps) => (
|
|
30
|
+
<BaseToast.Viewport
|
|
31
|
+
className={cn(
|
|
32
|
+
"fixed right-4 bottom-4 z-[100] flex w-[min(100vw-2rem,24rem)] flex-col outline-none",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export const ToastRoot = ({ className, ...props }: ToastRootProps) => (
|
|
40
|
+
<BaseToast.Root
|
|
41
|
+
className={cn(
|
|
42
|
+
"relative z-[calc(1000-var(--toast-index))] mb-2 box-border w-full rounded-xl border border-border-primary bg-surface p-4 shadow-lg outline-none",
|
|
43
|
+
"h-[var(--toast-frontmost-height,var(--toast-height))]",
|
|
44
|
+
"transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none",
|
|
45
|
+
"data-starting-style:opacity-0 data-starting-style:translate-y-2",
|
|
46
|
+
"data-ending-style:opacity-0 data-ending-style:translate-y-2",
|
|
47
|
+
"data-expanded:h-[var(--toast-height)] data-expanded:translate-y-[var(--toast-offset-y)]",
|
|
48
|
+
"data-[type=error]:border-destructive/40",
|
|
49
|
+
"data-[type=success]:border-border-primary",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export const ToastContent = ({ className, ...props }: ToastContentProps) => (
|
|
57
|
+
<BaseToast.Content
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex flex-col gap-1 overflow-hidden transition-opacity duration-150",
|
|
60
|
+
"data-behind:pointer-events-none data-behind:opacity-0 data-expanded:data-behind:opacity-100",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const ToastTitle = ({ className, ...props }: ToastTitleProps) => (
|
|
68
|
+
<BaseToast.Title
|
|
69
|
+
className={cn("text-sm-strong pr-8 text-fg-primary", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
export const ToastDescription = ({
|
|
75
|
+
className,
|
|
76
|
+
...props
|
|
77
|
+
}: ToastDescriptionProps) => (
|
|
78
|
+
<BaseToast.Description
|
|
79
|
+
className={cn("text-sm leading-relaxed text-fg-secondary", className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export const ToastAction = ({ className, ...props }: ToastActionProps) => (
|
|
85
|
+
<BaseToast.Action
|
|
86
|
+
className={cn(
|
|
87
|
+
"mt-2 inline-flex h-8 cursor-pointer items-center justify-center rounded-md px-2.5 text-sm text-fg-primary outline-none",
|
|
88
|
+
motion.colors,
|
|
89
|
+
"hover:bg-background-tertiary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
export const ToastClose = ({
|
|
97
|
+
className,
|
|
98
|
+
children,
|
|
99
|
+
...props
|
|
100
|
+
}: ToastCloseProps) => (
|
|
101
|
+
<BaseToast.Close
|
|
102
|
+
className={cn(
|
|
103
|
+
"absolute top-3 right-3 inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-fg-tertiary outline-none",
|
|
104
|
+
motion.colors,
|
|
105
|
+
"hover:bg-background-tertiary hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{children ?? (
|
|
111
|
+
<IconCrossSmall size={14} className="size-3.5" aria-hidden />
|
|
112
|
+
)}
|
|
113
|
+
</BaseToast.Close>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
export const ToastPositioner = ({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: ToastPositionerProps) => (
|
|
120
|
+
<BaseToast.Positioner
|
|
121
|
+
className={cn("z-[100] outline-none", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
export const ToastArrow = ({ className, ...props }: ToastArrowProps) => (
|
|
127
|
+
<BaseToast.Arrow
|
|
128
|
+
className={cn(
|
|
129
|
+
"data-[side=bottom]:top-[-6px] data-[side=left]:right-[-6px] data-[side=right]:left-[-6px] data-[side=top]:bottom-[-6px]",
|
|
130
|
+
"size-2.5 rotate-45 border border-border-primary bg-surface",
|
|
131
|
+
"data-[side=bottom]:border-r-0 data-[side=bottom]:border-b-0",
|
|
132
|
+
"data-[side=top]:border-t-0 data-[side=top]:border-l-0",
|
|
133
|
+
"data-[side=left]:border-b-0 data-[side=left]:border-l-0",
|
|
134
|
+
"data-[side=right]:border-t-0 data-[side=right]:border-r-0",
|
|
135
|
+
className,
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
export const useToastManager = BaseToast.useToastManager
|
|
142
|
+
export const createToastManager = BaseToast.createToastManager
|
package/src/toggle.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toggle as BaseToggle } from "@base-ui/react/toggle"
|
|
4
|
+
import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group"
|
|
5
|
+
import type { ComponentProps } from "react"
|
|
6
|
+
import { cn } from "./lib/cn"
|
|
7
|
+
import { motion } from "./lib/motion"
|
|
8
|
+
|
|
9
|
+
export type ToggleProps = ComponentProps<typeof BaseToggle>
|
|
10
|
+
export type ToggleGroupProps = ComponentProps<typeof BaseToggleGroup>
|
|
11
|
+
|
|
12
|
+
export const Toggle = ({ className, ...props }: ToggleProps) => (
|
|
13
|
+
<BaseToggle
|
|
14
|
+
className={cn(
|
|
15
|
+
"inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-2.5 text-sm text-fg-secondary outline-none",
|
|
16
|
+
motion.colors,
|
|
17
|
+
"hover:bg-background-tertiary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-pressed:bg-background-tertiary data-pressed:text-fg-primary",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export const ToggleGroup = ({ className, ...props }: ToggleGroupProps) => (
|
|
25
|
+
<BaseToggleGroup
|
|
26
|
+
className={cn(
|
|
27
|
+
"inline-flex items-center gap-0.5 rounded-lg border border-border-primary bg-background-secondary p-0.5",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
package/src/toolbar.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toolbar as BaseToolbar } from "@base-ui/react/toolbar"
|
|
4
|
+
import type { ComponentProps } from "react"
|
|
5
|
+
import { cn } from "./lib/cn"
|
|
6
|
+
import { motion } from "./lib/motion"
|
|
7
|
+
|
|
8
|
+
export type ToolbarProps = ComponentProps<typeof BaseToolbar.Root>
|
|
9
|
+
export type ToolbarGroupProps = ComponentProps<typeof BaseToolbar.Group>
|
|
10
|
+
export type ToolbarButtonProps = ComponentProps<typeof BaseToolbar.Button>
|
|
11
|
+
export type ToolbarLinkProps = ComponentProps<typeof BaseToolbar.Link>
|
|
12
|
+
export type ToolbarInputProps = ComponentProps<typeof BaseToolbar.Input>
|
|
13
|
+
export type ToolbarSeparatorProps = ComponentProps<typeof BaseToolbar.Separator>
|
|
14
|
+
|
|
15
|
+
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
|
16
|
+
<BaseToolbar.Root
|
|
17
|
+
className={cn(
|
|
18
|
+
"inline-flex items-center gap-0.5 rounded-lg border border-border-primary bg-background-secondary p-0.5",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const ToolbarGroup = ({ className, ...props }: ToolbarGroupProps) => (
|
|
26
|
+
<BaseToolbar.Group
|
|
27
|
+
className={cn("flex items-center gap-0.5", className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export const ToolbarButton = ({ className, ...props }: ToolbarButtonProps) => (
|
|
33
|
+
<BaseToolbar.Button
|
|
34
|
+
className={cn(
|
|
35
|
+
"inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2.5 text-sm text-fg-secondary outline-none",
|
|
36
|
+
motion.colors,
|
|
37
|
+
"hover:bg-background-tertiary hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-pressed:bg-background-tertiary data-pressed:text-fg-primary",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const ToolbarLink = ({ className, ...props }: ToolbarLinkProps) => (
|
|
45
|
+
<BaseToolbar.Link
|
|
46
|
+
className={cn(
|
|
47
|
+
"inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2.5 text-sm text-fg-secondary outline-none",
|
|
48
|
+
motion.colors,
|
|
49
|
+
"hover:bg-background-tertiary hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export const ToolbarInput = ({ className, ...props }: ToolbarInputProps) => (
|
|
57
|
+
<BaseToolbar.Input
|
|
58
|
+
className={cn(
|
|
59
|
+
"h-8 min-w-28 cursor-text rounded-md border border-border-secondary bg-surface px-2.5 text-sm text-fg-primary outline-none placeholder:text-fg-quaternary",
|
|
60
|
+
"outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const ToolbarSeparator = ({
|
|
68
|
+
className,
|
|
69
|
+
...props
|
|
70
|
+
}: ToolbarSeparatorProps) => (
|
|
71
|
+
<BaseToolbar.Separator
|
|
72
|
+
className={cn("mx-0.5 h-5 w-px bg-border-primary", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
package/src/tooltip.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip"
|
|
4
|
+
import type { ComponentProps } from "react"
|
|
5
|
+
import { cn } from "./lib/cn"
|
|
6
|
+
import { motion } from "./lib/motion"
|
|
7
|
+
|
|
8
|
+
export type TooltipProviderProps = ComponentProps<typeof BaseTooltip.Provider>
|
|
9
|
+
export type TooltipProps = ComponentProps<typeof BaseTooltip.Root>
|
|
10
|
+
export type TooltipTriggerProps = ComponentProps<typeof BaseTooltip.Trigger>
|
|
11
|
+
export type TooltipPortalProps = ComponentProps<typeof BaseTooltip.Portal>
|
|
12
|
+
export type TooltipPositionerProps = ComponentProps<
|
|
13
|
+
typeof BaseTooltip.Positioner
|
|
14
|
+
>
|
|
15
|
+
export type TooltipPopupProps = ComponentProps<typeof BaseTooltip.Popup>
|
|
16
|
+
|
|
17
|
+
export const TooltipProvider = (props: TooltipProviderProps) => (
|
|
18
|
+
<BaseTooltip.Provider {...props} />
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const Tooltip = (props: TooltipProps) => <BaseTooltip.Root {...props} />
|
|
22
|
+
|
|
23
|
+
export const TooltipTrigger = ({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: TooltipTriggerProps) => (
|
|
27
|
+
<BaseTooltip.Trigger className={cn(className)} {...props} />
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export const TooltipPortal = (props: TooltipPortalProps) => (
|
|
31
|
+
<BaseTooltip.Portal {...props} />
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export const TooltipPositioner = ({
|
|
35
|
+
sideOffset = 6,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: TooltipPositionerProps) => (
|
|
39
|
+
<BaseTooltip.Positioner
|
|
40
|
+
sideOffset={sideOffset}
|
|
41
|
+
className={cn("z-50 outline-none", className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
export const TooltipPopup = ({ className, ...props }: TooltipPopupProps) => (
|
|
47
|
+
<BaseTooltip.Popup
|
|
48
|
+
className={cn(
|
|
49
|
+
"z-50 max-w-xs rounded-md bg-surface-inverted px-2.5 py-1 text-xs text-fg-inverted shadow-md outline-none",
|
|
50
|
+
motion.popupAnchor,
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentProps,
|
|
10
|
+
} from "react"
|
|
11
|
+
import { Button } from "./button"
|
|
12
|
+
import { cn } from "./lib/cn"
|
|
13
|
+
|
|
14
|
+
const formatTime = (seconds: number) => {
|
|
15
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "0:00"
|
|
16
|
+
const mins = Math.floor(seconds / 60)
|
|
17
|
+
const secs = Math.floor(seconds % 60)
|
|
18
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type VideoPlayerProps = ComponentProps<"div"> & {
|
|
22
|
+
src: string
|
|
23
|
+
poster?: string
|
|
24
|
+
title?: string
|
|
25
|
+
/** Accessible name for the video. Defaults to title. */
|
|
26
|
+
"aria-label"?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const VideoPlayer = ({
|
|
30
|
+
src,
|
|
31
|
+
poster,
|
|
32
|
+
title,
|
|
33
|
+
className,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
...props
|
|
36
|
+
}: VideoPlayerProps) => {
|
|
37
|
+
const videoRef = useRef<HTMLVideoElement>(null)
|
|
38
|
+
const seekId = useId()
|
|
39
|
+
const [playing, setPlaying] = useState(false)
|
|
40
|
+
const [muted, setMuted] = useState(false)
|
|
41
|
+
const [current, setCurrent] = useState(0)
|
|
42
|
+
const [duration, setDuration] = useState(0)
|
|
43
|
+
|
|
44
|
+
const handleTogglePlay = useCallback(() => {
|
|
45
|
+
const video = videoRef.current
|
|
46
|
+
if (!video) return
|
|
47
|
+
if (video.paused) {
|
|
48
|
+
void video.play()
|
|
49
|
+
} else {
|
|
50
|
+
video.pause()
|
|
51
|
+
}
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const handleToggleMute = useCallback(() => {
|
|
55
|
+
const video = videoRef.current
|
|
56
|
+
if (!video) return
|
|
57
|
+
video.muted = !video.muted
|
|
58
|
+
setMuted(video.muted)
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
const handleSeek = useCallback((value: number) => {
|
|
62
|
+
const video = videoRef.current
|
|
63
|
+
if (!video) return
|
|
64
|
+
video.currentTime = value
|
|
65
|
+
setCurrent(value)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const handleFullscreen = useCallback(() => {
|
|
69
|
+
const video = videoRef.current
|
|
70
|
+
if (!video) return
|
|
71
|
+
if (document.fullscreenElement) {
|
|
72
|
+
void document.exitFullscreen()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
void video.requestFullscreen?.()
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const video = videoRef.current
|
|
80
|
+
if (!video) return
|
|
81
|
+
|
|
82
|
+
const handlePlay = () => setPlaying(true)
|
|
83
|
+
const handlePause = () => setPlaying(false)
|
|
84
|
+
const handleTime = () => setCurrent(video.currentTime)
|
|
85
|
+
const handleMeta = () => setDuration(video.duration || 0)
|
|
86
|
+
|
|
87
|
+
video.addEventListener("play", handlePlay)
|
|
88
|
+
video.addEventListener("pause", handlePause)
|
|
89
|
+
video.addEventListener("timeupdate", handleTime)
|
|
90
|
+
video.addEventListener("loadedmetadata", handleMeta)
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
video.removeEventListener("play", handlePlay)
|
|
94
|
+
video.removeEventListener("pause", handlePause)
|
|
95
|
+
video.removeEventListener("timeupdate", handleTime)
|
|
96
|
+
video.removeEventListener("loadedmetadata", handleMeta)
|
|
97
|
+
}
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
const progress = duration > 0 ? (current / duration) * 100 : 0
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
data-slot="video-player"
|
|
105
|
+
className={cn(
|
|
106
|
+
"overflow-hidden rounded-xl border border-border-primary bg-surface",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
<div className="relative bg-gray-1000">
|
|
112
|
+
<video
|
|
113
|
+
ref={videoRef}
|
|
114
|
+
src={src}
|
|
115
|
+
poster={poster}
|
|
116
|
+
playsInline
|
|
117
|
+
className="aspect-video w-full cursor-pointer object-cover"
|
|
118
|
+
aria-label={ariaLabel ?? title ?? "Video"}
|
|
119
|
+
onClick={handleTogglePlay}
|
|
120
|
+
/>
|
|
121
|
+
{!playing ? (
|
|
122
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/20">
|
|
123
|
+
<span className="flex size-12 items-center justify-center rounded-full bg-surface/90 text-fg-primary shadow-md">
|
|
124
|
+
<PlayIcon />
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="flex flex-col gap-3 border-t border-border-primary bg-surface p-3">
|
|
131
|
+
{title ? (
|
|
132
|
+
<p className="text-sm-strong truncate text-fg-primary">{title}</p>
|
|
133
|
+
) : null}
|
|
134
|
+
|
|
135
|
+
<div className="flex items-center gap-3">
|
|
136
|
+
<label className="sr-only" htmlFor={seekId}>
|
|
137
|
+
Seek
|
|
138
|
+
</label>
|
|
139
|
+
<input
|
|
140
|
+
id={seekId}
|
|
141
|
+
type="range"
|
|
142
|
+
min={0}
|
|
143
|
+
max={duration || 0}
|
|
144
|
+
step={0.1}
|
|
145
|
+
value={current}
|
|
146
|
+
onChange={(event) => handleSeek(Number(event.target.value))}
|
|
147
|
+
className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-background-quaternary accent-[var(--brand-primary)] [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-brand-primary"
|
|
148
|
+
style={{
|
|
149
|
+
background: `linear-gradient(to right, var(--brand-primary) ${progress}%, var(--background-quaternary) ${progress}%)`,
|
|
150
|
+
}}
|
|
151
|
+
aria-valuetext={`${formatTime(current)} of ${formatTime(duration)}`}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
<Button
|
|
157
|
+
type="button"
|
|
158
|
+
variant="ghost"
|
|
159
|
+
size="sm"
|
|
160
|
+
iconOnly
|
|
161
|
+
rounded
|
|
162
|
+
aria-label={playing ? "Pause" : "Play"}
|
|
163
|
+
onClick={handleTogglePlay}
|
|
164
|
+
>
|
|
165
|
+
{playing ? <PauseIcon /> : <PlayIcon />}
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
type="button"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
size="sm"
|
|
171
|
+
iconOnly
|
|
172
|
+
rounded
|
|
173
|
+
aria-label={muted ? "Unmute" : "Mute"}
|
|
174
|
+
onClick={handleToggleMute}
|
|
175
|
+
>
|
|
176
|
+
{muted ? <MuteIcon /> : <VolumeIcon />}
|
|
177
|
+
</Button>
|
|
178
|
+
<span className="text-xs font-mono text-fg-tertiary">
|
|
179
|
+
{formatTime(current)} / {formatTime(duration)}
|
|
180
|
+
</span>
|
|
181
|
+
<div className="ml-auto">
|
|
182
|
+
<Button
|
|
183
|
+
type="button"
|
|
184
|
+
variant="ghost"
|
|
185
|
+
size="sm"
|
|
186
|
+
iconOnly
|
|
187
|
+
rounded
|
|
188
|
+
aria-label="Fullscreen"
|
|
189
|
+
onClick={handleFullscreen}
|
|
190
|
+
>
|
|
191
|
+
<FullscreenIcon />
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const PlayIcon = () => (
|
|
201
|
+
<svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
|
|
202
|
+
<path d="M9 7.5v9l8-4.5-8-4.5Z" fill="currentColor" />
|
|
203
|
+
</svg>
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const PauseIcon = () => (
|
|
207
|
+
<svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
|
|
208
|
+
<path d="M8 7h3v10H8V7Zm5 0h3v10h-3V7Z" fill="currentColor" />
|
|
209
|
+
</svg>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const VolumeIcon = () => (
|
|
213
|
+
<svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
|
|
214
|
+
<path
|
|
215
|
+
d="M4 10v4h3l4 3V7L7 10H4Zm11.5 2a2.5 2.5 0 0 0-1.5-2.3v4.6A2.5 2.5 0 0 0 15.5 12Zm0-5.5v1.6a4 4 0 0 1 0 7.8v1.6a5.5 5.5 0 0 0 0-11Z"
|
|
216
|
+
fill="currentColor"
|
|
217
|
+
/>
|
|
218
|
+
</svg>
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const MuteIcon = () => (
|
|
222
|
+
<svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
|
|
223
|
+
<path
|
|
224
|
+
d="M4 10v4h3l4 3V7L7 10H4Zm11.2-.8 1.4-1.4 1.4 1.4 1.4-1.4 1.4 1.4-1.4 1.4 1.4 1.4-1.4 1.4-1.4-1.4-1.4 1.4-1.4-1.4 1.4-1.4-1.4-1.4Z"
|
|
225
|
+
fill="currentColor"
|
|
226
|
+
/>
|
|
227
|
+
</svg>
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const FullscreenIcon = () => (
|
|
231
|
+
<svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
|
|
232
|
+
<path
|
|
233
|
+
d="M7 9V7h3v2H7Zm7 0V7h3v2h-3ZM7 17v-2h3v2H7Zm7 0v-2h3v2h-3Z"
|
|
234
|
+
fill="currentColor"
|
|
235
|
+
/>
|
|
236
|
+
<path
|
|
237
|
+
d="M5 5h5v2H7v3H5V5Zm9 0h5v5h-2V7h-3V5ZM5 14h2v3h3v2H5v-5Zm12 3h-3v2h5v-5h-2v3Z"
|
|
238
|
+
fill="currentColor"
|
|
239
|
+
/>
|
|
240
|
+
</svg>
|
|
241
|
+
)
|