@momentumworld/ui-react 0.17.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/dist/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +356 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2162 -0
- package/dist/index.js +11640 -0
- package/dist/index.js.map +1 -0
- package/dist/input-JCZs61MO.d.ts +24 -0
- package/package.json +90 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-counter.tsx +109 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/aurora-orb.tsx +163 -0
- package/src/components/ui/aurora-text.tsx +76 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +150 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/chat.tsx +494 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/color-picker.tsx +389 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/confetti.tsx +196 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/date-picker.tsx +154 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/help-tip.tsx +94 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/markdown.tsx +188 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/pipeline.tsx +102 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/segmented-control.tsx +83 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +774 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +206 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +242 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/thinking-orb.tsx +6 -0
- package/src/components/ui/time-picker.tsx +308 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +143 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +329 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import {
|
|
5
|
+
Maximize2Icon,
|
|
6
|
+
Minimize2Icon,
|
|
7
|
+
PauseIcon,
|
|
8
|
+
PlayIcon,
|
|
9
|
+
Volume2Icon,
|
|
10
|
+
VolumeXIcon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../../lib/utils/css";
|
|
15
|
+
|
|
16
|
+
const videoPlayerVariants = cva(
|
|
17
|
+
"group relative overflow-hidden rounded-2xl bg-black ring-1 ring-inset ring-white/10",
|
|
18
|
+
{
|
|
19
|
+
defaultVariants: { size: "md" },
|
|
20
|
+
variants: {
|
|
21
|
+
size: {
|
|
22
|
+
sm: "w-full max-w-xs",
|
|
23
|
+
md: "w-full max-w-lg",
|
|
24
|
+
lg: "w-full max-w-2xl",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
interface VideoPlayerProps
|
|
31
|
+
extends Omit<React.ComponentProps<"div">, "title">,
|
|
32
|
+
VariantProps<typeof videoPlayerVariants> {
|
|
33
|
+
src: string;
|
|
34
|
+
poster?: string;
|
|
35
|
+
title?: string;
|
|
36
|
+
autoPlay?: boolean;
|
|
37
|
+
loop?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function VideoPlayer({
|
|
41
|
+
className,
|
|
42
|
+
size,
|
|
43
|
+
src,
|
|
44
|
+
poster,
|
|
45
|
+
title,
|
|
46
|
+
autoPlay = false,
|
|
47
|
+
loop = false,
|
|
48
|
+
...props
|
|
49
|
+
}: VideoPlayerProps) {
|
|
50
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
51
|
+
const videoRef = React.useRef<HTMLVideoElement>(null);
|
|
52
|
+
const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
53
|
+
|
|
54
|
+
const [playing, setPlaying] = React.useState(false);
|
|
55
|
+
const [currentTime, setCurrentTime] = React.useState(0);
|
|
56
|
+
const [duration, setDuration] = React.useState(0);
|
|
57
|
+
const [muted, setMuted] = React.useState(false);
|
|
58
|
+
const [fullscreen, setFullscreen] = React.useState(false);
|
|
59
|
+
const [controlsVisible, setControlsVisible] = React.useState(true);
|
|
60
|
+
|
|
61
|
+
const clearHideTimer = () => {
|
|
62
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const scheduleHide = React.useCallback(() => {
|
|
66
|
+
clearHideTimer();
|
|
67
|
+
hideTimerRef.current = setTimeout(() => setControlsVisible(false), 2500);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => () => clearHideTimer(), []);
|
|
71
|
+
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (!playing) {
|
|
74
|
+
clearHideTimer();
|
|
75
|
+
setControlsVisible(true);
|
|
76
|
+
}
|
|
77
|
+
}, [playing]);
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
const handler = () => setFullscreen(!!document.fullscreenElement);
|
|
81
|
+
document.addEventListener("fullscreenchange", handler);
|
|
82
|
+
return () => document.removeEventListener("fullscreenchange", handler);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const handleMouseMove = () => {
|
|
86
|
+
setControlsVisible(true);
|
|
87
|
+
if (playing) scheduleHide();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleMouseLeave = () => {
|
|
91
|
+
if (playing) setControlsVisible(false);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const togglePlay = () => {
|
|
95
|
+
const v = videoRef.current;
|
|
96
|
+
if (!v) return;
|
|
97
|
+
if (v.paused) {
|
|
98
|
+
v.play();
|
|
99
|
+
} else {
|
|
100
|
+
v.pause();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
105
|
+
const v = videoRef.current;
|
|
106
|
+
if (!v) return;
|
|
107
|
+
const t = Number(e.target.value);
|
|
108
|
+
v.currentTime = t;
|
|
109
|
+
setCurrentTime(t);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const toggleMute = () => {
|
|
113
|
+
const v = videoRef.current;
|
|
114
|
+
if (!v) return;
|
|
115
|
+
v.muted = !v.muted;
|
|
116
|
+
setMuted(v.muted);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const toggleFullscreen = () => {
|
|
120
|
+
const el = containerRef.current;
|
|
121
|
+
if (!el) return;
|
|
122
|
+
if (!document.fullscreenElement) {
|
|
123
|
+
el.requestFullscreen().catch(() => {});
|
|
124
|
+
} else {
|
|
125
|
+
document.exitFullscreen().catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const formatTime = (s: number) => {
|
|
130
|
+
if (!isFinite(s) || s < 0) return "0:00";
|
|
131
|
+
const m = Math.floor(s / 60);
|
|
132
|
+
const sec = Math.floor(s % 60);
|
|
133
|
+
return `${m}:${sec.toString().padStart(2, "0")}`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
ref={containerRef}
|
|
141
|
+
className={cn(videoPlayerVariants({ size }), className)}
|
|
142
|
+
data-slot="video-player"
|
|
143
|
+
onMouseLeave={handleMouseLeave}
|
|
144
|
+
onMouseMove={handleMouseMove}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{/* Video */}
|
|
148
|
+
<video
|
|
149
|
+
ref={videoRef}
|
|
150
|
+
autoPlay={autoPlay}
|
|
151
|
+
className="aspect-video w-full object-cover"
|
|
152
|
+
loop={loop}
|
|
153
|
+
playsInline
|
|
154
|
+
poster={poster}
|
|
155
|
+
src={src}
|
|
156
|
+
onEnded={() => setPlaying(false)}
|
|
157
|
+
onLoadedMetadata={() => {
|
|
158
|
+
const v = videoRef.current;
|
|
159
|
+
if (v) setDuration(v.duration);
|
|
160
|
+
}}
|
|
161
|
+
onPause={() => setPlaying(false)}
|
|
162
|
+
onPlay={() => {
|
|
163
|
+
setPlaying(true);
|
|
164
|
+
scheduleHide();
|
|
165
|
+
}}
|
|
166
|
+
onTimeUpdate={() => {
|
|
167
|
+
const v = videoRef.current;
|
|
168
|
+
if (v) setCurrentTime(v.currentTime);
|
|
169
|
+
}}
|
|
170
|
+
onClick={togglePlay}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{/* Center play overlay — visible when paused */}
|
|
174
|
+
{!playing && (
|
|
175
|
+
<button
|
|
176
|
+
className="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/20"
|
|
177
|
+
data-slot="video-player-overlay"
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={togglePlay}
|
|
180
|
+
>
|
|
181
|
+
<span className="flex size-14 items-center justify-center rounded-full bg-white/90 text-black shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-sm transition-transform hover:scale-105">
|
|
182
|
+
<PlayIcon className="size-5 translate-x-0.5" />
|
|
183
|
+
</span>
|
|
184
|
+
</button>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Controls */}
|
|
188
|
+
<div
|
|
189
|
+
className={cn(
|
|
190
|
+
"absolute inset-x-0 bottom-0 flex flex-col gap-2 bg-gradient-to-t from-black/70 via-black/30 to-transparent px-3 pb-3 pt-10 transition-opacity duration-200",
|
|
191
|
+
controlsVisible ? "opacity-100" : "opacity-0",
|
|
192
|
+
)}
|
|
193
|
+
data-slot="video-player-controls"
|
|
194
|
+
>
|
|
195
|
+
{/* Scrubber */}
|
|
196
|
+
<div className="relative flex items-center">
|
|
197
|
+
<div className="relative h-1 w-full overflow-hidden rounded-full bg-white/25">
|
|
198
|
+
<div
|
|
199
|
+
className="pointer-events-none h-full rounded-full bg-white"
|
|
200
|
+
style={{ width: `${progress}%` }}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
<input
|
|
204
|
+
aria-label="Seek"
|
|
205
|
+
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
|
206
|
+
max={duration || 100}
|
|
207
|
+
min={0}
|
|
208
|
+
step={0.1}
|
|
209
|
+
type="range"
|
|
210
|
+
value={currentTime}
|
|
211
|
+
onChange={handleSeek}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Button row */}
|
|
216
|
+
<div className="flex items-center justify-between gap-2">
|
|
217
|
+
<div className="flex items-center gap-0.5">
|
|
218
|
+
<button
|
|
219
|
+
aria-label={playing ? "Pause" : "Play"}
|
|
220
|
+
className="flex size-7 items-center justify-center rounded-md text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={togglePlay}
|
|
223
|
+
>
|
|
224
|
+
{playing ? (
|
|
225
|
+
<PauseIcon className="size-4" />
|
|
226
|
+
) : (
|
|
227
|
+
<PlayIcon className="size-4" />
|
|
228
|
+
)}
|
|
229
|
+
</button>
|
|
230
|
+
|
|
231
|
+
<button
|
|
232
|
+
aria-label={muted ? "Unmute" : "Mute"}
|
|
233
|
+
className="flex size-7 items-center justify-center rounded-md text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={toggleMute}
|
|
236
|
+
>
|
|
237
|
+
{muted ? (
|
|
238
|
+
<VolumeXIcon className="size-4" />
|
|
239
|
+
) : (
|
|
240
|
+
<Volume2Icon className="size-4" />
|
|
241
|
+
)}
|
|
242
|
+
</button>
|
|
243
|
+
|
|
244
|
+
<span className="select-none pl-1 text-[11px] tabular-nums text-white/60">
|
|
245
|
+
{formatTime(currentTime)} / {formatTime(duration)}
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div className="flex items-center gap-1.5">
|
|
250
|
+
{title ? (
|
|
251
|
+
<span className="max-w-28 truncate text-[11px] font-medium text-white/50">
|
|
252
|
+
{title}
|
|
253
|
+
</span>
|
|
254
|
+
) : null}
|
|
255
|
+
|
|
256
|
+
<button
|
|
257
|
+
aria-label={fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
258
|
+
className="flex size-7 items-center justify-center rounded-md text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={toggleFullscreen}
|
|
261
|
+
>
|
|
262
|
+
{fullscreen ? (
|
|
263
|
+
<Minimize2Icon className="size-4" />
|
|
264
|
+
) : (
|
|
265
|
+
<Maximize2Icon className="size-4" />
|
|
266
|
+
)}
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { VideoPlayer, videoPlayerVariants };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react";
|
|
4
|
+
import { Transition } from "@headlessui/react";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { CloudUploadIcon, TrashIcon, UserIcon } from "lucide-react";
|
|
7
|
+
import { useDropzone } from "react-dropzone";
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { SpinnerOnDemand } from "../ui/spinner-on-demand";
|
|
11
|
+
|
|
12
|
+
export interface AvatarUploadBaseProps
|
|
13
|
+
extends Omit<React.ComponentProps<typeof Avatar>, "onDrop" | "children"> {
|
|
14
|
+
onDrop: (file: File) => void;
|
|
15
|
+
onDelete?: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
progress: number;
|
|
19
|
+
imageUrl: string | undefined;
|
|
20
|
+
initials: React.ReactNode;
|
|
21
|
+
alt: string;
|
|
22
|
+
avatarClassName?: string;
|
|
23
|
+
mode?: "image" | "image&video";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AvatarUploadBase({
|
|
27
|
+
onDrop,
|
|
28
|
+
onDelete,
|
|
29
|
+
disabled,
|
|
30
|
+
loading = false,
|
|
31
|
+
progress,
|
|
32
|
+
imageUrl,
|
|
33
|
+
initials,
|
|
34
|
+
alt,
|
|
35
|
+
className,
|
|
36
|
+
avatarClassName,
|
|
37
|
+
mode = "image",
|
|
38
|
+
...props
|
|
39
|
+
}: AvatarUploadBaseProps) {
|
|
40
|
+
const {
|
|
41
|
+
getRootProps,
|
|
42
|
+
getInputProps,
|
|
43
|
+
open: onDropOpen,
|
|
44
|
+
isDragActive,
|
|
45
|
+
} = useDropzone({
|
|
46
|
+
onDrop: ([file]) => file && onDrop(file),
|
|
47
|
+
noClick: true,
|
|
48
|
+
accept: {
|
|
49
|
+
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],
|
|
50
|
+
...(mode === "image&video"
|
|
51
|
+
? { "video/*": [".webm", ".mp4"] }
|
|
52
|
+
: undefined),
|
|
53
|
+
},
|
|
54
|
+
multiple: false,
|
|
55
|
+
disabled: disabled || loading,
|
|
56
|
+
});
|
|
57
|
+
const progressValue = Math.trunc(100 * Math.min(1, progress));
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
{...getRootProps()}
|
|
62
|
+
tabIndex={undefined}
|
|
63
|
+
className={clsx(
|
|
64
|
+
"relative flex items-center gap-3 rounded-full",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<input {...getInputProps()} />
|
|
69
|
+
<Avatar className={avatarClassName} key={imageUrl} {...props}>
|
|
70
|
+
<AvatarImage src={imageUrl} alt={alt} />
|
|
71
|
+
<AvatarFallback className="text-xl">
|
|
72
|
+
{initials || <UserIcon />}
|
|
73
|
+
</AvatarFallback>
|
|
74
|
+
</Avatar>
|
|
75
|
+
<div className="relative flex items-center gap-3 transition">
|
|
76
|
+
<Button
|
|
77
|
+
size="sm"
|
|
78
|
+
variant="outline"
|
|
79
|
+
disabled={loading || disabled}
|
|
80
|
+
onClick={onDropOpen}
|
|
81
|
+
className="grow relative max-w-55 mx-auto"
|
|
82
|
+
>
|
|
83
|
+
<SpinnerOnDemand icon={<CloudUploadIcon />} isLoading={loading} />
|
|
84
|
+
Upload
|
|
85
|
+
<Transition
|
|
86
|
+
show={loading}
|
|
87
|
+
as={ProgressPrimitive.Root}
|
|
88
|
+
className="absolute inset-0 grow overflow-hidden rounded-[inherit] pointer-events-none"
|
|
89
|
+
style={{
|
|
90
|
+
// Fix overflow clipping in Safari
|
|
91
|
+
// https://gist.github.com/domske/b66047671c780a238b51c51ffde8d3a0
|
|
92
|
+
transform: "translateZ(0)",
|
|
93
|
+
}}
|
|
94
|
+
value={progressValue}
|
|
95
|
+
>
|
|
96
|
+
<ProgressPrimitive.Track className="size-full">
|
|
97
|
+
<ProgressPrimitive.Indicator
|
|
98
|
+
className="duration-660 ease-[cubic-bezier(0.65,0,0.35,1)] size-full bg-accent/8 transition-transform"
|
|
99
|
+
style={{ transform: `translateX(-${100 - progressValue}%)` }}
|
|
100
|
+
/>
|
|
101
|
+
</ProgressPrimitive.Track>
|
|
102
|
+
</Transition>
|
|
103
|
+
</Button>
|
|
104
|
+
{imageUrl && onDelete && (
|
|
105
|
+
<Button
|
|
106
|
+
disabled={disabled || loading}
|
|
107
|
+
size="icon-sm"
|
|
108
|
+
variant="outline"
|
|
109
|
+
onClick={onDelete}
|
|
110
|
+
>
|
|
111
|
+
<TrashIcon />
|
|
112
|
+
<span className="sr-only">Remove</span>
|
|
113
|
+
</Button>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
<Transition
|
|
117
|
+
as="div"
|
|
118
|
+
show={isDragActive}
|
|
119
|
+
className={clsx(
|
|
120
|
+
"pointer-events-none absolute inset-0 flex flex-col items-center justify-center rounded-2xl transition duration-200 [--opacity:1] data-closed:[--opacity:0]",
|
|
121
|
+
"bg-accent/[calc(var(--opacity)*8%)] ring-1 ring-border/[calc(var(--opacity)*100%)] backdrop-blur-[calc(var(--opacity)*var(--blur-sm))]",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<CloudUploadIcon className="size-8 text-foreground/[calc(var(--opacity)*100%)] transition-colors" />
|
|
125
|
+
<p className="text-sm font-medium text-foreground/[calc(var(--opacity)*100%)] transition-colors">
|
|
126
|
+
Drop files here
|
|
127
|
+
</p>
|
|
128
|
+
</Transition>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react";
|
|
4
|
+
import { Transition } from "@headlessui/react";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { CloudUploadIcon, TrashIcon } from "lucide-react";
|
|
7
|
+
import { useDropzone } from "react-dropzone";
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { SpinnerOnDemand } from "../ui/spinner-on-demand";
|
|
11
|
+
|
|
12
|
+
export interface ImageUploadBaseProps
|
|
13
|
+
extends Omit<React.ComponentProps<"div">, "onDrop"> {
|
|
14
|
+
onDrop: (file: File) => void;
|
|
15
|
+
onDelete?: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
progress: number;
|
|
19
|
+
imageUrl: string | null;
|
|
20
|
+
alt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ImageUploadBase({
|
|
24
|
+
onDrop,
|
|
25
|
+
onDelete,
|
|
26
|
+
disabled,
|
|
27
|
+
loading = false,
|
|
28
|
+
progress,
|
|
29
|
+
imageUrl,
|
|
30
|
+
alt,
|
|
31
|
+
className,
|
|
32
|
+
...props
|
|
33
|
+
}: ImageUploadBaseProps) {
|
|
34
|
+
const {
|
|
35
|
+
getRootProps,
|
|
36
|
+
getInputProps,
|
|
37
|
+
open: onDropOpen,
|
|
38
|
+
isDragActive,
|
|
39
|
+
} = useDropzone({
|
|
40
|
+
onDrop: ([file]) => file && onDrop(file),
|
|
41
|
+
noClick: true,
|
|
42
|
+
accept: {
|
|
43
|
+
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],
|
|
44
|
+
},
|
|
45
|
+
multiple: false,
|
|
46
|
+
disabled: disabled || loading,
|
|
47
|
+
});
|
|
48
|
+
const progressValue = Math.trunc(100 * Math.min(1, progress));
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
{...getRootProps()}
|
|
53
|
+
tabIndex={undefined}
|
|
54
|
+
data-slot="image-upload-root"
|
|
55
|
+
className={clsx(
|
|
56
|
+
"group/image-upload-root flex flex-col rounded-lg border-2 p-0.5 border-dashed border-input bg-muted/30 text-center transition-colors hover:border-muted-foreground/40 hover:bg-muted/50 has-data-[active=true]:border-primary/60 has-data-[active=true]:bg-primary/5",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<div className="grow flex flex-col relative rounded-md">
|
|
62
|
+
<input {...getInputProps()} />
|
|
63
|
+
<Avatar
|
|
64
|
+
key={imageUrl ?? "none"}
|
|
65
|
+
className="peer/avatar absolute size-full inset-0 rounded-[inherit]"
|
|
66
|
+
>
|
|
67
|
+
{imageUrl && (
|
|
68
|
+
<AvatarImage key={imageUrl ?? "none"} src={imageUrl} alt={alt} />
|
|
69
|
+
)}
|
|
70
|
+
<AvatarFallback className="bg-linear-to-br from-muted to-background" />
|
|
71
|
+
</Avatar>
|
|
72
|
+
<div
|
|
73
|
+
data-active={loading || isDragActive}
|
|
74
|
+
className="grow flex justify-center items-center [--opacity:0] p-8 data-[active=true]:[--opacity:1] peer-has-data-[slot=avatar-fallback]/avatar:[--opacity:1] group-hover/image-upload-root:[--opacity:1] relative z-10 rounded-[inherit] bg-background/[calc(55%*var(--opacity))] backdrop-blur-[calc(var(--blur-xs)*var(--opacity))] gap-3 transition"
|
|
75
|
+
>
|
|
76
|
+
<Button
|
|
77
|
+
size="sm"
|
|
78
|
+
variant="outline"
|
|
79
|
+
disabled={loading || disabled}
|
|
80
|
+
onClick={onDropOpen}
|
|
81
|
+
className="grow relative max-w-32 opacity-(--opacity)"
|
|
82
|
+
>
|
|
83
|
+
<SpinnerOnDemand icon={<CloudUploadIcon />} isLoading={loading} />
|
|
84
|
+
{isDragActive ? "Drop files here" : "Upload"}
|
|
85
|
+
<Transition
|
|
86
|
+
show={loading}
|
|
87
|
+
as={ProgressPrimitive.Root}
|
|
88
|
+
className="absolute inset-0 opacity-(--opacity) grow overflow-hidden rounded-[inherit] pointer-events-none"
|
|
89
|
+
value={progressValue}
|
|
90
|
+
>
|
|
91
|
+
<ProgressPrimitive.Track className="size-full">
|
|
92
|
+
<ProgressPrimitive.Indicator className="duration-660 ease-[cubic-bezier(0.65,0,0.35,1)] size-full bg-accent/8 transition-all" />
|
|
93
|
+
</ProgressPrimitive.Track>
|
|
94
|
+
</Transition>
|
|
95
|
+
</Button>
|
|
96
|
+
{imageUrl && onDelete && !isDragActive && (
|
|
97
|
+
<Button
|
|
98
|
+
disabled={disabled || loading}
|
|
99
|
+
size="icon-sm"
|
|
100
|
+
variant="outline"
|
|
101
|
+
onClick={onDelete}
|
|
102
|
+
className="opacity-(--opacity)"
|
|
103
|
+
aria-label="Remove"
|
|
104
|
+
>
|
|
105
|
+
<TrashIcon />
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/form.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Country Club UI Kit — TanStack Form bindings.
|
|
2
|
+
// Import from "@countryclub/ui-react/form".
|
|
3
|
+
|
|
4
|
+
export * from "./components/form/checkbox-group-field";
|
|
5
|
+
export * from "./components/form/date-field";
|
|
6
|
+
export * from "./components/form/date-range-field";
|
|
7
|
+
export * from "./components/form/form";
|
|
8
|
+
export {
|
|
9
|
+
appFieldContext,
|
|
10
|
+
appFormContext,
|
|
11
|
+
useAppFieldContext,
|
|
12
|
+
useAppFormContext,
|
|
13
|
+
} from "./components/form/form-context";
|
|
14
|
+
export * from "./components/form/number-field";
|
|
15
|
+
export * from "./components/form/select-field";
|
|
16
|
+
export * from "./components/form/submit-button";
|
|
17
|
+
export * from "./components/form/text-field";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Country Club UI Kit — React component library.
|
|
2
|
+
// Design tokens live in ./styles.css (Tailwind CSS v4).
|
|
3
|
+
|
|
4
|
+
// Core UI components
|
|
5
|
+
export * from "./components/ui/accordion";
|
|
6
|
+
export * from "./components/ui/alert";
|
|
7
|
+
export * from "./components/ui/alert-dialog";
|
|
8
|
+
export * from "./components/ui/animated-theme-toggler";
|
|
9
|
+
export * from "./components/ui/app-store-buttons";
|
|
10
|
+
export * from "./components/ui/aspect-ratio";
|
|
11
|
+
export * from "./components/ui/aurora-orb";
|
|
12
|
+
export * from "./components/ui/aurora-text";
|
|
13
|
+
export * from "./components/ui/autocomplete";
|
|
14
|
+
export * from "./components/ui/avatar";
|
|
15
|
+
export * from "./components/ui/avatar-group";
|
|
16
|
+
export * from "./components/ui/animated-counter";
|
|
17
|
+
export * from "./components/ui/badge";
|
|
18
|
+
export * from "./components/ui/badge-group";
|
|
19
|
+
export * from "./components/ui/breadcrumb";
|
|
20
|
+
export * from "./components/ui/button";
|
|
21
|
+
export * from "./components/ui/calendar";
|
|
22
|
+
export * from "./components/ui/date-picker";
|
|
23
|
+
export * from "./components/ui/time-picker";
|
|
24
|
+
export * from "./components/ui/card";
|
|
25
|
+
export * from "./components/ui/carousel";
|
|
26
|
+
export * from "./components/ui/chart";
|
|
27
|
+
export * from "./components/ui/chat";
|
|
28
|
+
export * from "./components/ui/chat-divider";
|
|
29
|
+
export * from "./components/ui/checkbox";
|
|
30
|
+
export * from "./components/ui/checkbox-group";
|
|
31
|
+
export * from "./components/ui/collapsible";
|
|
32
|
+
export * from "./components/ui/color-picker";
|
|
33
|
+
export * from "./components/ui/combobox";
|
|
34
|
+
export * from "./components/ui/command";
|
|
35
|
+
export * from "./components/ui/confetti";
|
|
36
|
+
export * from "./components/ui/context-meter";
|
|
37
|
+
export * from "./components/ui/credit-card";
|
|
38
|
+
export * from "./components/ui/data-table-paged";
|
|
39
|
+
export * from "./components/ui/drawer";
|
|
40
|
+
export * from "./components/ui/context-menu";
|
|
41
|
+
export * from "./components/ui/dialog";
|
|
42
|
+
export * from "./components/ui/empty";
|
|
43
|
+
export * from "./components/ui/fab";
|
|
44
|
+
export * from "./components/ui/featured-icon";
|
|
45
|
+
export * from "./components/ui/field";
|
|
46
|
+
export * from "./components/ui/fieldset";
|
|
47
|
+
export * from "./components/ui/form";
|
|
48
|
+
export * from "./components/ui/frame";
|
|
49
|
+
export * from "./components/ui/generic-empty";
|
|
50
|
+
export * from "./components/ui/group";
|
|
51
|
+
export * from "./components/ui/help-tip";
|
|
52
|
+
export * from "./components/ui/horizontal-scroll-fader";
|
|
53
|
+
export * from "./components/ui/input";
|
|
54
|
+
export * from "./components/ui/input-group";
|
|
55
|
+
export * from "./components/ui/input-otp";
|
|
56
|
+
export * from "./components/ui/item";
|
|
57
|
+
export * from "./components/ui/kbd";
|
|
58
|
+
export * from "./components/ui/label";
|
|
59
|
+
export * from "./components/ui/markdown";
|
|
60
|
+
export * from "./components/ui/menu";
|
|
61
|
+
export * from "./components/ui/menubar";
|
|
62
|
+
export * from "./components/ui/meter";
|
|
63
|
+
export * from "./components/ui/multi-select";
|
|
64
|
+
export * from "./components/ui/navigation-menu";
|
|
65
|
+
export * from "./components/ui/number-field";
|
|
66
|
+
export * from "./components/ui/pagination";
|
|
67
|
+
export * from "./components/ui/pagination-controls";
|
|
68
|
+
export * from "./components/ui/popover";
|
|
69
|
+
export * from "./components/ui/preview-card";
|
|
70
|
+
export * from "./components/ui/progress";
|
|
71
|
+
export * from "./components/ui/qr-code";
|
|
72
|
+
export * from "./components/ui/radio-group";
|
|
73
|
+
export * from "./components/ui/resizable";
|
|
74
|
+
export * from "./components/ui/scroll-area";
|
|
75
|
+
export * from "./components/ui/scroller";
|
|
76
|
+
export * from "./components/ui/section-header";
|
|
77
|
+
export * from "./components/ui/smooth-scroll";
|
|
78
|
+
export * from "./components/ui/social-button";
|
|
79
|
+
export * from "./components/ui/stepper";
|
|
80
|
+
export * from "./components/ui/pipeline";
|
|
81
|
+
export * from "./components/ui/segmented-control";
|
|
82
|
+
export * from "./components/ui/select";
|
|
83
|
+
export * from "./components/ui/separator";
|
|
84
|
+
export * from "./components/ui/sheet";
|
|
85
|
+
export * from "./components/ui/sidebar";
|
|
86
|
+
export * from "./components/ui/skeleton";
|
|
87
|
+
export * from "./components/ui/slider";
|
|
88
|
+
export * from "./components/ui/spinner";
|
|
89
|
+
export * from "./components/ui/spinner-on-demand";
|
|
90
|
+
export * from "./components/ui/stat";
|
|
91
|
+
export * from "./components/ui/switch";
|
|
92
|
+
export * from "./components/ui/table";
|
|
93
|
+
export * from "./components/ui/tabs";
|
|
94
|
+
export * from "./components/ui/tag";
|
|
95
|
+
export { default as TargetCountdown } from "./components/ui/target-countdown";
|
|
96
|
+
export * from "./components/ui/thinking-orb";
|
|
97
|
+
export * from "./components/ui/timeline";
|
|
98
|
+
export * from "./components/ui/text-editor";
|
|
99
|
+
export * from "./components/ui/textarea";
|
|
100
|
+
export * from "./components/ui/toast";
|
|
101
|
+
export * from "./components/ui/toggle";
|
|
102
|
+
export {
|
|
103
|
+
ToggleGroup,
|
|
104
|
+
ToggleGroupItem,
|
|
105
|
+
ToggleGroupSeparator,
|
|
106
|
+
} from "./components/ui/toggle-group";
|
|
107
|
+
export * from "./components/ui/toolbar";
|
|
108
|
+
export * from "./components/ui/tooltip";
|
|
109
|
+
export * from "./components/ui/vertical-scroll-fader";
|
|
110
|
+
export * from "./components/ui/video-player";
|
|
111
|
+
|
|
112
|
+
// Layout components
|
|
113
|
+
export * from "./components/layout/dashboard-header";
|
|
114
|
+
export * from "./components/layout/dashboard-panel";
|
|
115
|
+
export * from "./components/layout/dashboard-topbar";
|
|
116
|
+
|
|
117
|
+
// Theme
|
|
118
|
+
export * from "./components/theme-provider";
|
|
119
|
+
|
|
120
|
+
// Upload
|
|
121
|
+
export * from "./components/upload/avatar-upload-base";
|
|
122
|
+
export * from "./components/upload/image-upload-base";
|
|
123
|
+
|
|
124
|
+
// Utilities
|
|
125
|
+
export { cn } from "./lib/utils/css";
|
|
126
|
+
|
|
127
|
+
// Hooks
|
|
128
|
+
export { default as useCallbackRef } from "./lib/hooks/use-callback-ref";
|
|
129
|
+
export { useFirstRender } from "./lib/hooks/use-first-render";
|
|
130
|
+
export { useHover } from "./lib/hooks/use-hover";
|
|
131
|
+
export { default as useIsTabActive } from "./lib/hooks/use-is-tab-active";
|
|
132
|
+
export {
|
|
133
|
+
useIs2xl,
|
|
134
|
+
useIsLg,
|
|
135
|
+
useIsMd,
|
|
136
|
+
useIsSm,
|
|
137
|
+
useIsXl,
|
|
138
|
+
useMediaQuery,
|
|
139
|
+
useSubscribeBreakpoints,
|
|
140
|
+
} from "./lib/hooks/use-media-query";
|
|
141
|
+
|
|
142
|
+
// Type helpers
|
|
143
|
+
export type * from "./types/helpers";
|