@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,16 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils/css";
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className={cn(
|
|
7
|
+
"animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
|
|
8
|
+
className,
|
|
9
|
+
)}
|
|
10
|
+
data-slot="skeleton"
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { Skeleton };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
|
|
8
|
+
type SliderLabelPosition = "bottom" | "floating";
|
|
9
|
+
|
|
10
|
+
function Slider({
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
defaultValue,
|
|
14
|
+
value,
|
|
15
|
+
min = 0,
|
|
16
|
+
max = 100,
|
|
17
|
+
labelPosition,
|
|
18
|
+
formatLabel = (labelValue) => String(labelValue),
|
|
19
|
+
...props
|
|
20
|
+
}: SliderPrimitive.Root.Props & {
|
|
21
|
+
/** Render each thumb's value beneath it, or in a floating bubble above it. */
|
|
22
|
+
labelPosition?: SliderLabelPosition;
|
|
23
|
+
formatLabel?: (value: number) => React.ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
const _values = React.useMemo(() => {
|
|
26
|
+
if (value !== undefined) {
|
|
27
|
+
return Array.isArray(value) ? value : [value];
|
|
28
|
+
}
|
|
29
|
+
if (defaultValue !== undefined) {
|
|
30
|
+
return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
|
|
31
|
+
}
|
|
32
|
+
return [min];
|
|
33
|
+
}, [value, defaultValue, min]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SliderPrimitive.Root
|
|
37
|
+
className={cn("data-[orientation=horizontal]:w-full", className)}
|
|
38
|
+
defaultValue={defaultValue}
|
|
39
|
+
max={max}
|
|
40
|
+
min={min}
|
|
41
|
+
thumbAlignment="edge"
|
|
42
|
+
value={value}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
<SliderPrimitive.Control
|
|
47
|
+
className="flex touch-none select-none data-disabled:pointer-events-none data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:min-w-44 data-[orientation=vertical]:flex-col data-disabled:opacity-64"
|
|
48
|
+
data-slot="slider-control"
|
|
49
|
+
>
|
|
50
|
+
<SliderPrimitive.Track
|
|
51
|
+
className="relative grow select-none before:absolute before:rounded-full before:bg-input data-[orientation=horizontal]:h-1 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1 data-[orientation=horizontal]:before:inset-x-0.5 data-[orientation=vertical]:before:inset-x-0 data-[orientation=horizontal]:before:inset-y-0 data-[orientation=vertical]:before:inset-y-0.5"
|
|
52
|
+
data-slot="slider-track"
|
|
53
|
+
>
|
|
54
|
+
<SliderPrimitive.Indicator
|
|
55
|
+
className="select-none rounded-full bg-primary data-[orientation=horizontal]:ms-0.5 data-[orientation=vertical]:mb-0.5"
|
|
56
|
+
data-slot="slider-indicator"
|
|
57
|
+
/>
|
|
58
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
59
|
+
<SliderPrimitive.Thumb
|
|
60
|
+
className="block size-5 shrink-0 select-none rounded-full border border-input bg-white not-dark:bg-clip-padding shadow-xs/5 outline-none transition-[box-shadow,scale] duration-150 ease-out before:absolute before:inset-0 before:rounded-full before:shadow-[0_1px_--theme(--color-black/6%)] has-focus-visible:ring-[3px] has-focus-visible:ring-ring/24 data-dragging:data-[active]:scale-115 sm:size-4 dark:border-background dark:has-focus-visible:ring-ring/48 [:has(*:focus-visible),[data-dragging]]:shadow-none"
|
|
61
|
+
data-slot="slider-thumb"
|
|
62
|
+
index={index}
|
|
63
|
+
key={String(index)}
|
|
64
|
+
render={(thumbProps, state) => (
|
|
65
|
+
<div
|
|
66
|
+
{...thumbProps}
|
|
67
|
+
data-active={
|
|
68
|
+
state.activeThumbIndex === index ? "" : undefined
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{thumbProps.children}
|
|
72
|
+
{labelPosition === "bottom" && (
|
|
73
|
+
<span
|
|
74
|
+
className="pointer-events-none absolute start-1/2 top-full mt-2 -translate-x-1/2 select-none whitespace-nowrap font-medium text-foreground text-sm rtl:translate-x-1/2"
|
|
75
|
+
data-slot="slider-thumb-label"
|
|
76
|
+
>
|
|
77
|
+
{formatLabel(state.values[index] ?? min)}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{labelPosition === "floating" && (
|
|
81
|
+
<span
|
|
82
|
+
className="pointer-events-none absolute bottom-full start-1/2 mb-2.5 -translate-x-1/2 select-none whitespace-nowrap rounded-md border bg-popover not-dark:bg-clip-padding px-2 py-1 font-semibold text-popover-foreground text-xs shadow-md/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] rtl:translate-x-1/2 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]"
|
|
83
|
+
data-slot="slider-thumb-label"
|
|
84
|
+
>
|
|
85
|
+
{formatLabel(state.values[index] ?? min)}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</SliderPrimitive.Track>
|
|
93
|
+
</SliderPrimitive.Control>
|
|
94
|
+
</SliderPrimitive.Root>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function SliderValue({ className, ...props }: SliderPrimitive.Value.Props) {
|
|
99
|
+
return (
|
|
100
|
+
<SliderPrimitive.Value
|
|
101
|
+
className={cn("flex justify-end text-sm", className)}
|
|
102
|
+
data-slot="slider-value"
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { Slider, SliderValue };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
export interface SmoothWheelOptions {
|
|
8
|
+
/** Turn the behaviour on/off (so it can be called unconditionally). */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Translate vertical wheel movement into horizontal scrolling. */
|
|
11
|
+
horizontal?: boolean;
|
|
12
|
+
/** Scales the wheel delta — lower feels slower/heavier. Default 0.8. */
|
|
13
|
+
multiplier?: number;
|
|
14
|
+
/** Lerp factor toward the target each frame (0–1). Lower = smoother. */
|
|
15
|
+
ease?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Eased, momentum-style wheel scrolling for any scrollable element. Instead of
|
|
20
|
+
* jumping `scrollTop`/`scrollLeft` by the raw wheel delta (which feels fast and
|
|
21
|
+
* abrupt), it animates toward an accumulated target with `requestAnimationFrame`.
|
|
22
|
+
*
|
|
23
|
+
* It only smooths genuine mouse wheels. Trackpads already scroll smoothly with
|
|
24
|
+
* OS-level inertia, so layering our own easing on top makes it fight the native
|
|
25
|
+
* momentum — the container sticks, or snaps back to where the animation was
|
|
26
|
+
* still heading. In the default vertical mode a trackpad therefore keeps native
|
|
27
|
+
* behaviour; in horizontal mode we still translate a vertical mouse wheel into
|
|
28
|
+
* horizontal scrolling (the whole point of the mode) but let a real horizontal
|
|
29
|
+
* trackpad swipe scroll natively. Every time control returns to native
|
|
30
|
+
* scrolling — trackpad, or an edge — the in-flight animation is cancelled and
|
|
31
|
+
* the target resynced so it can never pull the position back.
|
|
32
|
+
*/
|
|
33
|
+
export function useSmoothWheel(
|
|
34
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
35
|
+
{
|
|
36
|
+
enabled = true,
|
|
37
|
+
horizontal = false,
|
|
38
|
+
multiplier = 0.8,
|
|
39
|
+
ease = 0.12,
|
|
40
|
+
}: SmoothWheelOptions = {},
|
|
41
|
+
) {
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (!enabled) return;
|
|
44
|
+
const el = ref.current;
|
|
45
|
+
if (!el) return;
|
|
46
|
+
|
|
47
|
+
// Respect users who prefer no motion — fall back to native scrolling.
|
|
48
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const axisProp = horizontal ? "scrollLeft" : "scrollTop";
|
|
53
|
+
const sizeProp = horizontal ? "scrollWidth" : "scrollHeight";
|
|
54
|
+
const clientProp = horizontal ? "clientWidth" : "clientHeight";
|
|
55
|
+
|
|
56
|
+
let target = el[axisProp];
|
|
57
|
+
let raf = 0;
|
|
58
|
+
let animating = false;
|
|
59
|
+
let kind: "mouse" | "trackpad" | null = null;
|
|
60
|
+
let lastTs = 0;
|
|
61
|
+
|
|
62
|
+
// Cancel the animation and resync the target to the real position, so any
|
|
63
|
+
// native scrolling that follows is never yanked back to a stale target.
|
|
64
|
+
const releaseToNative = () => {
|
|
65
|
+
if (raf) cancelAnimationFrame(raf);
|
|
66
|
+
raf = 0;
|
|
67
|
+
animating = false;
|
|
68
|
+
target = el[axisProp];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const tick = () => {
|
|
72
|
+
const max = el[sizeProp] - el[clientProp];
|
|
73
|
+
target = Math.max(0, Math.min(target, max));
|
|
74
|
+
const current = el[axisProp];
|
|
75
|
+
const diff = target - current;
|
|
76
|
+
if (Math.abs(diff) < 0.5) {
|
|
77
|
+
el[axisProp] = target;
|
|
78
|
+
raf = 0;
|
|
79
|
+
animating = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
el[axisProp] = current + diff * ease;
|
|
83
|
+
raf = requestAnimationFrame(tick);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Strong per-event trackpad signals: fractional deltas, a horizontal
|
|
87
|
+
// component (mice almost never emit deltaX), or small pixel steps.
|
|
88
|
+
const looksLikeTrackpad = (e: WheelEvent) =>
|
|
89
|
+
e.deltaMode === 0 &&
|
|
90
|
+
(!Number.isInteger(e.deltaY) ||
|
|
91
|
+
e.deltaX !== 0 ||
|
|
92
|
+
Math.abs(e.deltaY) < 50);
|
|
93
|
+
|
|
94
|
+
// Sticky, gesture-scoped classification: decide once per burst of events
|
|
95
|
+
// (< 200ms apart) so a hard trackpad flick whose mid-gesture deltas look
|
|
96
|
+
// mouse-like stays classified as a trackpad the whole way through.
|
|
97
|
+
const classify = (e: WheelEvent, now: number): "mouse" | "trackpad" => {
|
|
98
|
+
if (kind === null || now - lastTs > 200) {
|
|
99
|
+
kind = looksLikeTrackpad(e) ? "trackpad" : "mouse";
|
|
100
|
+
} else if (
|
|
101
|
+
kind === "mouse" &&
|
|
102
|
+
(e.deltaX !== 0 || !Number.isInteger(e.deltaY))
|
|
103
|
+
) {
|
|
104
|
+
kind = "trackpad";
|
|
105
|
+
}
|
|
106
|
+
return kind;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onWheel = (event: WheelEvent) => {
|
|
110
|
+
if (event.ctrlKey) return; // pinch-zoom
|
|
111
|
+
const max = el[sizeProp] - el[clientProp];
|
|
112
|
+
if (max <= 0) return;
|
|
113
|
+
|
|
114
|
+
const now = performance.now();
|
|
115
|
+
const source = classify(event, now);
|
|
116
|
+
lastTs = now;
|
|
117
|
+
|
|
118
|
+
if (horizontal) {
|
|
119
|
+
// Native handles a real horizontal swipe; we only translate the
|
|
120
|
+
// vertical wheel (mouse, or a trackpad's vertical swipe) into
|
|
121
|
+
// horizontal motion — that translation is the reason the mode exists.
|
|
122
|
+
if (event.deltaX !== 0 && Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
|
|
123
|
+
releaseToNative();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} else if (source === "trackpad") {
|
|
127
|
+
// Vertical mode: let the OS scroll the trackpad natively.
|
|
128
|
+
releaseToNative();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let delta =
|
|
133
|
+
horizontal && event.deltaX !== 0 ? event.deltaX : event.deltaY;
|
|
134
|
+
// deltaMode: 0 = pixel, 1 = line, 2 = page.
|
|
135
|
+
if (event.deltaMode === 1) delta *= 16;
|
|
136
|
+
else if (event.deltaMode === 2) delta *= el[clientProp];
|
|
137
|
+
delta *= multiplier;
|
|
138
|
+
if (delta === 0) return;
|
|
139
|
+
|
|
140
|
+
if (!animating) target = el[axisProp];
|
|
141
|
+
|
|
142
|
+
const atStart = target <= 0;
|
|
143
|
+
const atEnd = target >= max;
|
|
144
|
+
// Let the wheel bubble at the edges so the page keeps scrolling.
|
|
145
|
+
if ((delta < 0 && atStart) || (delta > 0 && atEnd)) {
|
|
146
|
+
releaseToNative();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
target = Math.max(0, Math.min(target + delta, max));
|
|
152
|
+
if (!animating) {
|
|
153
|
+
animating = true;
|
|
154
|
+
raf = requestAnimationFrame(tick);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
159
|
+
return () => {
|
|
160
|
+
el.removeEventListener("wheel", onWheel);
|
|
161
|
+
cancelAnimationFrame(raf);
|
|
162
|
+
};
|
|
163
|
+
}, [ref, enabled, horizontal, multiplier, ease]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface SmoothScrollProps extends React.ComponentProps<"div"> {
|
|
167
|
+
/** Scroll horizontally (vertical wheel is translated to horizontal). */
|
|
168
|
+
horizontal?: boolean;
|
|
169
|
+
/** Scales the wheel delta — lower feels slower/heavier. Default 0.8. */
|
|
170
|
+
multiplier?: number;
|
|
171
|
+
/** Lerp factor toward the target each frame (0–1). Lower = smoother. */
|
|
172
|
+
ease?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A scroll container with eased, momentum-style wheel scrolling. Drop any
|
|
177
|
+
* overflowing content inside; pass `horizontal` for a row that scrolls
|
|
178
|
+
* smoothly with a normal mouse wheel.
|
|
179
|
+
*/
|
|
180
|
+
function SmoothScroll({
|
|
181
|
+
className,
|
|
182
|
+
children,
|
|
183
|
+
horizontal = false,
|
|
184
|
+
multiplier,
|
|
185
|
+
ease,
|
|
186
|
+
...props
|
|
187
|
+
}: SmoothScrollProps) {
|
|
188
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
189
|
+
useSmoothWheel(ref, { ease, horizontal, multiplier });
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
className={cn(
|
|
194
|
+
horizontal ? "overflow-x-auto overflow-y-hidden" : "overflow-y-auto",
|
|
195
|
+
className,
|
|
196
|
+
)}
|
|
197
|
+
data-slot="smooth-scroll"
|
|
198
|
+
ref={ref}
|
|
199
|
+
{...props}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { SmoothScroll };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils/css";
|
|
9
|
+
|
|
10
|
+
const socialButtonVariants = cva(
|
|
11
|
+
"relative inline-flex h-10 shrink-0 cursor-pointer items-center justify-center gap-2.5 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-[box-shadow,filter] duration-100 sm:h-9 sm:text-sm [:active:not(:disabled)]:brightness-95 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0 sm:[&_svg]:size-4.5",
|
|
12
|
+
{
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
iconOnly: false,
|
|
15
|
+
theme: "brand",
|
|
16
|
+
},
|
|
17
|
+
variants: {
|
|
18
|
+
iconOnly: {
|
|
19
|
+
false: "px-[calc(--spacing(4)-1px)]",
|
|
20
|
+
true: "w-10 px-0 sm:w-9",
|
|
21
|
+
},
|
|
22
|
+
theme: {
|
|
23
|
+
// Filled with the provider's brand color (set per provider below).
|
|
24
|
+
brand:
|
|
25
|
+
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
|
|
26
|
+
// Outline surface with the provider's full-color logo.
|
|
27
|
+
color:
|
|
28
|
+
"border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64",
|
|
29
|
+
// Neutral tonal surface with a monochrome logo.
|
|
30
|
+
gray: "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
|
|
31
|
+
},
|
|
32
|
+
provider: {
|
|
33
|
+
apple: "",
|
|
34
|
+
dribbble: "",
|
|
35
|
+
facebook: "",
|
|
36
|
+
figma: "",
|
|
37
|
+
google: "",
|
|
38
|
+
twitter: "",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
compoundVariants: [
|
|
42
|
+
{
|
|
43
|
+
provider: "google",
|
|
44
|
+
theme: "brand",
|
|
45
|
+
className:
|
|
46
|
+
"border-[#4285F4] bg-[#4285F4] text-white shadow-[#4285F4]/24 [:hover,[data-pressed]]:bg-[#4285F4]/90",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
provider: "facebook",
|
|
50
|
+
theme: "brand",
|
|
51
|
+
className:
|
|
52
|
+
"border-[#1877F2] bg-[#1877F2] text-white shadow-[#1877F2]/24 [:hover,[data-pressed]]:bg-[#1877F2]/90",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
provider: "dribbble",
|
|
56
|
+
theme: "brand",
|
|
57
|
+
className:
|
|
58
|
+
"border-[#EA4C89] bg-[#EA4C89] text-white shadow-[#EA4C89]/24 [:hover,[data-pressed]]:bg-[#EA4C89]/90",
|
|
59
|
+
},
|
|
60
|
+
// Black-brand providers invert in dark mode so they stay visible.
|
|
61
|
+
...["apple", "twitter", "figma"].map((provider) => ({
|
|
62
|
+
provider: provider as SocialButtonProvider,
|
|
63
|
+
theme: "brand" as const,
|
|
64
|
+
className:
|
|
65
|
+
"border-black bg-black text-white shadow-black/24 [:hover,[data-pressed]]:bg-black/90 dark:border-white dark:bg-white dark:text-black dark:shadow-white/8 dark:not-disabled:inset-shadow-none dark:[:hover,[data-pressed]]:bg-white/90",
|
|
66
|
+
})),
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
interface SocialLogoProps extends React.ComponentProps<"svg"> {
|
|
72
|
+
/** Render the provider's full-color logo instead of currentColor. */
|
|
73
|
+
colored?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function GoogleLogoIcon({ colored, ...props }: SocialLogoProps) {
|
|
77
|
+
return (
|
|
78
|
+
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
79
|
+
<path
|
|
80
|
+
d="M23.52 12.273c0-.851-.076-1.67-.218-2.455H12v4.642h6.458a5.52 5.52 0 0 1-2.394 3.622v3.01h3.878c2.269-2.088 3.578-5.165 3.578-8.819Z"
|
|
81
|
+
fill={colored ? "#4285F4" : "currentColor"}
|
|
82
|
+
/>
|
|
83
|
+
<path
|
|
84
|
+
d="M12 24c3.24 0 5.957-1.075 7.942-2.907l-3.878-3.011c-1.075.72-2.45 1.145-4.064 1.145-3.125 0-5.77-2.11-6.715-4.947H1.276v3.11A11.995 11.995 0 0 0 12 24Z"
|
|
85
|
+
fill={colored ? "#34A853" : "currentColor"}
|
|
86
|
+
/>
|
|
87
|
+
<path
|
|
88
|
+
d="M5.285 14.28A7.213 7.213 0 0 1 4.91 12c0-.79.136-1.56.376-2.28V6.61H1.276a11.995 11.995 0 0 0 0 10.78l4.009-3.11Z"
|
|
89
|
+
fill={colored ? "#FBBC05" : "currentColor"}
|
|
90
|
+
/>
|
|
91
|
+
<path
|
|
92
|
+
d="M12 4.773c1.762 0 3.344.605 4.587 1.794l3.442-3.442C17.952 1.19 15.235 0 12 0A11.995 11.995 0 0 0 1.276 6.61l4.01 3.11C6.228 6.884 8.874 4.773 12 4.773Z"
|
|
93
|
+
fill={colored ? "#EA4335" : "currentColor"}
|
|
94
|
+
/>
|
|
95
|
+
</svg>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function FacebookLogoIcon({ colored, ...props }: SocialLogoProps) {
|
|
100
|
+
return (
|
|
101
|
+
<svg
|
|
102
|
+
fill={colored ? "#1877F2" : "currentColor"}
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
<path d="M24 12c0-6.627-5.373-12-12-12S0 5.373 0 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078V12h3.047V9.356c0-3.007 1.792-4.668 4.533-4.668 1.312 0 2.686.234 2.686.234v2.953h-1.514c-1.491 0-1.955.926-1.955 1.875V12h3.328l-.532 3.469h-2.796v8.385C19.612 22.954 24 17.99 24 12Z" />
|
|
108
|
+
</svg>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function AppleLogoIcon({ colored: _colored, ...props }: SocialLogoProps) {
|
|
113
|
+
return (
|
|
114
|
+
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
115
|
+
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
|
|
116
|
+
</svg>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function XLogoIcon({ colored: _colored, ...props }: SocialLogoProps) {
|
|
121
|
+
return (
|
|
122
|
+
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
123
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117l11.966 15.644Z" />
|
|
124
|
+
</svg>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function FigmaLogoIcon({ colored, ...props }: SocialLogoProps) {
|
|
129
|
+
return (
|
|
130
|
+
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
131
|
+
<path
|
|
132
|
+
d="M8.167 24a3.834 3.834 0 0 0 3.833-3.833V16.333H8.167a3.834 3.834 0 0 0 0 7.667Z"
|
|
133
|
+
fill={colored ? "#0ACF83" : "currentColor"}
|
|
134
|
+
/>
|
|
135
|
+
<path
|
|
136
|
+
d="M4.333 12a3.834 3.834 0 0 1 3.834-3.833H12v7.666H8.167A3.834 3.834 0 0 1 4.333 12Z"
|
|
137
|
+
fill={colored ? "#A259FF" : "currentColor"}
|
|
138
|
+
/>
|
|
139
|
+
<path
|
|
140
|
+
d="M4.333 4.167A3.834 3.834 0 0 1 8.167.333H12V8H8.167a3.834 3.834 0 0 1-3.834-3.833Z"
|
|
141
|
+
fill={colored ? "#F24E1E" : "currentColor"}
|
|
142
|
+
/>
|
|
143
|
+
<path
|
|
144
|
+
d="M12 .333h3.833a3.834 3.834 0 0 1 0 7.667H12V.333Z"
|
|
145
|
+
fill={colored ? "#FF7262" : "currentColor"}
|
|
146
|
+
/>
|
|
147
|
+
<path
|
|
148
|
+
d="M19.667 12a3.834 3.834 0 1 1-7.667 0 3.834 3.834 0 0 1 7.667 0Z"
|
|
149
|
+
fill={colored ? "#1ABCFE" : "currentColor"}
|
|
150
|
+
/>
|
|
151
|
+
</svg>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function DribbbleLogoIcon({ colored, ...props }: SocialLogoProps) {
|
|
156
|
+
return (
|
|
157
|
+
<svg
|
|
158
|
+
fill={colored ? "#EA4C89" : "currentColor"}
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
161
|
+
{...props}
|
|
162
|
+
>
|
|
163
|
+
<path
|
|
164
|
+
d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12Zm10.12-10.358c-.35-.11-3.17-.953-6.384-.438 1.34 3.684 1.887 6.684 1.992 7.308 2.3-1.555 3.936-4.02 4.395-6.87Zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4 1.73 1.358 3.92 2.166 6.29 2.166 1.42 0 2.77-.29 4-.814Zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765.135-.045.27-.084.405-.12-.26-.585-.54-1.167-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855ZM1.96 9.92c.46.008 4.683.026 9.477-1.248-1.698-3.018-3.53-5.558-3.8-5.928-2.868 1.35-5.01 3.99-5.676 7.17ZM9.6 2.052c.282.38 2.145 2.914 3.822 6 3.645-1.365 5.19-3.44 5.373-3.702-1.81-1.61-4.19-2.586-6.795-2.586-.825 0-1.63.1-2.4.285Zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04.24.49.47.985.68 1.486.08.18.15.36.22.53 3.41-.43 6.8.26 7.14.33-.02-2.42-.88-4.64-2.31-6.38Z"
|
|
165
|
+
fillRule="evenodd"
|
|
166
|
+
clipRule="evenodd"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type SocialButtonProvider =
|
|
173
|
+
| "google"
|
|
174
|
+
| "facebook"
|
|
175
|
+
| "apple"
|
|
176
|
+
| "twitter"
|
|
177
|
+
| "figma"
|
|
178
|
+
| "dribbble";
|
|
179
|
+
|
|
180
|
+
const providers: Record<
|
|
181
|
+
SocialButtonProvider,
|
|
182
|
+
{
|
|
183
|
+
label: string;
|
|
184
|
+
Icon: (props: SocialLogoProps) => React.ReactElement;
|
|
185
|
+
}
|
|
186
|
+
> = {
|
|
187
|
+
apple: { Icon: AppleLogoIcon, label: "Sign in with Apple" },
|
|
188
|
+
dribbble: { Icon: DribbbleLogoIcon, label: "Sign in with Dribbble" },
|
|
189
|
+
facebook: { Icon: FacebookLogoIcon, label: "Sign in with Facebook" },
|
|
190
|
+
figma: { Icon: FigmaLogoIcon, label: "Sign in with Figma" },
|
|
191
|
+
google: { Icon: GoogleLogoIcon, label: "Sign in with Google" },
|
|
192
|
+
twitter: { Icon: XLogoIcon, label: "Sign in with X" },
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
interface SocialButtonProps extends useRender.ComponentProps<"button"> {
|
|
196
|
+
/** Which provider to render: google, facebook, apple, twitter, figma or dribbble. */
|
|
197
|
+
provider: SocialButtonProvider;
|
|
198
|
+
/** brand (provider-filled), color (outline + full-color logo) or gray (tonal). */
|
|
199
|
+
theme?: VariantProps<typeof socialButtonVariants>["theme"];
|
|
200
|
+
/** Render a square button with the logo only. */
|
|
201
|
+
iconOnly?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function SocialButton({
|
|
205
|
+
className,
|
|
206
|
+
provider,
|
|
207
|
+
theme = "brand",
|
|
208
|
+
iconOnly = false,
|
|
209
|
+
render,
|
|
210
|
+
children,
|
|
211
|
+
...props
|
|
212
|
+
}: SocialButtonProps) {
|
|
213
|
+
const { label, Icon } = providers[provider];
|
|
214
|
+
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
|
|
215
|
+
render ? undefined : "button";
|
|
216
|
+
|
|
217
|
+
const defaultProps = {
|
|
218
|
+
"aria-label": iconOnly ? label : undefined,
|
|
219
|
+
className: cn(socialButtonVariants({ className, iconOnly, provider, theme })),
|
|
220
|
+
"data-provider": provider,
|
|
221
|
+
"data-slot": "social-button",
|
|
222
|
+
type: typeValue,
|
|
223
|
+
children: (
|
|
224
|
+
<>
|
|
225
|
+
<Icon aria-hidden="true" colored={theme === "color"} />
|
|
226
|
+
{!iconOnly && <span>{children ?? label}</span>}
|
|
227
|
+
</>
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return useRender({
|
|
232
|
+
defaultTagName: "button",
|
|
233
|
+
props: mergeProps<"button">(defaultProps, props),
|
|
234
|
+
render,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
SocialButton,
|
|
240
|
+
socialButtonVariants,
|
|
241
|
+
GoogleLogoIcon,
|
|
242
|
+
FacebookLogoIcon,
|
|
243
|
+
XLogoIcon,
|
|
244
|
+
FigmaLogoIcon,
|
|
245
|
+
DribbbleLogoIcon,
|
|
246
|
+
type SocialButtonProvider,
|
|
247
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Loader2Icon } from "lucide-react";
|
|
4
|
+
import { useSpinDelay } from "spin-delay";
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
function SpinnerOnDemand({
|
|
8
|
+
className,
|
|
9
|
+
options,
|
|
10
|
+
isLoading,
|
|
11
|
+
icon,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof Loader2Icon> & {
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
options?: Parameters<typeof useSpinDelay>[1];
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
const shouldShow = useSpinDelay(isLoading, options);
|
|
19
|
+
|
|
20
|
+
if (!shouldShow) return icon || null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Loader2Icon
|
|
24
|
+
aria-label="Loading"
|
|
25
|
+
className={cn("animate-spin", className)}
|
|
26
|
+
role="status"
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { SpinnerOnDemand };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/utils/css";
|
|
3
|
+
|
|
4
|
+
function Spinner({
|
|
5
|
+
className,
|
|
6
|
+
...props
|
|
7
|
+
}: React.ComponentProps<typeof Loader2Icon>) {
|
|
8
|
+
return (
|
|
9
|
+
<Loader2Icon
|
|
10
|
+
aria-label="Loading"
|
|
11
|
+
className={cn("animate-spin", className)}
|
|
12
|
+
role="status"
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Spinner };
|