@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,80 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils/css";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4",
|
|
8
|
+
{
|
|
9
|
+
defaultVariants: {
|
|
10
|
+
variant: "default",
|
|
11
|
+
},
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default:
|
|
15
|
+
"bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
|
|
16
|
+
error:
|
|
17
|
+
"border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
|
|
18
|
+
info: "border-info/32 bg-info/4 [&>svg]:text-info",
|
|
19
|
+
success: "border-success/32 bg-success/4 [&>svg]:text-success",
|
|
20
|
+
warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function Alert({
|
|
27
|
+
className,
|
|
28
|
+
variant,
|
|
29
|
+
...props
|
|
30
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(alertVariants({ variant }), className)}
|
|
34
|
+
data-slot="alert"
|
|
35
|
+
role="alert"
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn("font-medium [svg~&]:col-start-2", className)}
|
|
45
|
+
data-slot="alert-title"
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function AlertDescription({
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
}: React.ComponentProps<"div">) {
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className={cn(
|
|
58
|
+
"flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
data-slot="alert-description"
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
data-slot="alert-action"
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
|
|
8
|
+
interface AnimatedCounterProps extends Omit<React.ComponentProps<"span">, "children"> {
|
|
9
|
+
/** The target value to animate to. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Tween duration in ms (default 650). */
|
|
12
|
+
duration?: number;
|
|
13
|
+
/** Decimal places to render (default 0). */
|
|
14
|
+
decimals?: number;
|
|
15
|
+
/** Prefix rendered before the number, e.g. "$". */
|
|
16
|
+
prefix?: string;
|
|
17
|
+
/** Suffix rendered after the number, e.g. "%". */
|
|
18
|
+
suffix?: string;
|
|
19
|
+
/** Group digits with thousands separators (default true). */
|
|
20
|
+
separator?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A number that tweens smoothly from its previous value to the next whenever
|
|
25
|
+
* `value` changes — e.g. dashboard stats re-counting when the time range
|
|
26
|
+
* switches. Dependency-free (requestAnimationFrame), so it stays tiny and works
|
|
27
|
+
* anywhere. Respects `prefers-reduced-motion` by snapping to the final value.
|
|
28
|
+
*
|
|
29
|
+
* The animation is purely visual; the accessible text is always the final
|
|
30
|
+
* value (via `aria-label`) so screen readers never read the intermediate tween.
|
|
31
|
+
*/
|
|
32
|
+
function AnimatedCounter({
|
|
33
|
+
value,
|
|
34
|
+
duration = 650,
|
|
35
|
+
decimals = 0,
|
|
36
|
+
prefix = "",
|
|
37
|
+
suffix = "",
|
|
38
|
+
separator = true,
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}: AnimatedCounterProps) {
|
|
42
|
+
const [display, setDisplay] = useState(value);
|
|
43
|
+
const fromRef = useRef(value);
|
|
44
|
+
const rafRef = useRef<number | null>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const from = fromRef.current;
|
|
48
|
+
const to = value;
|
|
49
|
+
if (from === to) return;
|
|
50
|
+
|
|
51
|
+
const reduce =
|
|
52
|
+
typeof window !== "undefined" &&
|
|
53
|
+
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
54
|
+
if (reduce || duration <= 0) {
|
|
55
|
+
fromRef.current = to;
|
|
56
|
+
setDisplay(to);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let start: number | null = null;
|
|
61
|
+
const step = (ts: number) => {
|
|
62
|
+
if (start === null) start = ts;
|
|
63
|
+
const t = Math.min(1, (ts - start) / duration);
|
|
64
|
+
// easeOutCubic — quick then settles, matching a "count up" feel.
|
|
65
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
66
|
+
const current = from + (to - from) * eased;
|
|
67
|
+
setDisplay(current);
|
|
68
|
+
if (t < 1) {
|
|
69
|
+
rafRef.current = requestAnimationFrame(step);
|
|
70
|
+
} else {
|
|
71
|
+
fromRef.current = to;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
rafRef.current = requestAnimationFrame(step);
|
|
75
|
+
return () => {
|
|
76
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
77
|
+
};
|
|
78
|
+
}, [value, duration]);
|
|
79
|
+
|
|
80
|
+
const rounded = Number(display.toFixed(decimals));
|
|
81
|
+
const formatted = separator
|
|
82
|
+
? rounded.toLocaleString(undefined, {
|
|
83
|
+
minimumFractionDigits: decimals,
|
|
84
|
+
maximumFractionDigits: decimals,
|
|
85
|
+
})
|
|
86
|
+
: rounded.toFixed(decimals);
|
|
87
|
+
|
|
88
|
+
const finalLabel = `${prefix}${value.toLocaleString(undefined, {
|
|
89
|
+
minimumFractionDigits: decimals,
|
|
90
|
+
maximumFractionDigits: decimals,
|
|
91
|
+
})}${suffix}`;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<span
|
|
95
|
+
className={cn("tabular-nums", className)}
|
|
96
|
+
data-slot="animated-counter"
|
|
97
|
+
aria-label={finalLabel}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<span aria-hidden>
|
|
101
|
+
{prefix}
|
|
102
|
+
{formatted}
|
|
103
|
+
{suffix}
|
|
104
|
+
</span>
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { AnimatedCounter, type AnimatedCounterProps };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MoonIcon, SunIcon } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { flushSync } from "react-dom";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
import { useTheme } from "../theme-provider";
|
|
9
|
+
import { buttonVariants } from "./button";
|
|
10
|
+
|
|
11
|
+
export type ThemeTogglerVariant =
|
|
12
|
+
| "circle"
|
|
13
|
+
| "square"
|
|
14
|
+
| "triangle"
|
|
15
|
+
| "diamond"
|
|
16
|
+
| "hexagon"
|
|
17
|
+
| "rectangle"
|
|
18
|
+
| "star";
|
|
19
|
+
|
|
20
|
+
interface AnimatedThemeTogglerProps
|
|
21
|
+
extends React.ComponentPropsWithoutRef<"button"> {
|
|
22
|
+
/** Duration of the reveal animation in milliseconds. */
|
|
23
|
+
duration?: number;
|
|
24
|
+
/** Clip-path shape used for the view-transition reveal. */
|
|
25
|
+
variant?: ThemeTogglerVariant;
|
|
26
|
+
/** Expand from the viewport center instead of the button center. */
|
|
27
|
+
fromCenter?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function polygonCollapsed(cx: number, cy: number, vertexCount: number): string {
|
|
31
|
+
const pairs = Array.from(
|
|
32
|
+
{ length: vertexCount },
|
|
33
|
+
() => `${cx}px ${cy}px`,
|
|
34
|
+
).join(", ");
|
|
35
|
+
return `polygon(${pairs})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getThemeTransitionClipPaths(
|
|
39
|
+
variant: ThemeTogglerVariant,
|
|
40
|
+
cx: number,
|
|
41
|
+
cy: number,
|
|
42
|
+
maxRadius: number,
|
|
43
|
+
viewportWidth: number,
|
|
44
|
+
viewportHeight: number,
|
|
45
|
+
): [string, string] {
|
|
46
|
+
switch (variant) {
|
|
47
|
+
case "circle":
|
|
48
|
+
return [
|
|
49
|
+
`circle(0px at ${cx}px ${cy}px)`,
|
|
50
|
+
`circle(${maxRadius}px at ${cx}px ${cy}px)`,
|
|
51
|
+
];
|
|
52
|
+
case "square": {
|
|
53
|
+
const halfW = Math.max(cx, viewportWidth - cx);
|
|
54
|
+
const halfH = Math.max(cy, viewportHeight - cy);
|
|
55
|
+
const halfSide = Math.max(halfW, halfH) * 1.05;
|
|
56
|
+
const end = [
|
|
57
|
+
`${cx - halfSide}px ${cy - halfSide}px`,
|
|
58
|
+
`${cx + halfSide}px ${cy - halfSide}px`,
|
|
59
|
+
`${cx + halfSide}px ${cy + halfSide}px`,
|
|
60
|
+
`${cx - halfSide}px ${cy + halfSide}px`,
|
|
61
|
+
].join(", ");
|
|
62
|
+
return [polygonCollapsed(cx, cy, 4), `polygon(${end})`];
|
|
63
|
+
}
|
|
64
|
+
case "triangle": {
|
|
65
|
+
const scale = maxRadius * 2.2;
|
|
66
|
+
const dx = (Math.sqrt(3) / 2) * scale;
|
|
67
|
+
const verts = [
|
|
68
|
+
`${cx}px ${cy - scale}px`,
|
|
69
|
+
`${cx + dx}px ${cy + 0.5 * scale}px`,
|
|
70
|
+
`${cx - dx}px ${cy + 0.5 * scale}px`,
|
|
71
|
+
].join(", ");
|
|
72
|
+
return [polygonCollapsed(cx, cy, 3), `polygon(${verts})`];
|
|
73
|
+
}
|
|
74
|
+
case "diamond": {
|
|
75
|
+
// Slightly larger than the circle radius so axis-aligned coverage matches.
|
|
76
|
+
const R = maxRadius * Math.SQRT2;
|
|
77
|
+
const end = [
|
|
78
|
+
`${cx}px ${cy - R}px`,
|
|
79
|
+
`${cx + R}px ${cy}px`,
|
|
80
|
+
`${cx}px ${cy + R}px`,
|
|
81
|
+
`${cx - R}px ${cy}px`,
|
|
82
|
+
].join(", ");
|
|
83
|
+
return [polygonCollapsed(cx, cy, 4), `polygon(${end})`];
|
|
84
|
+
}
|
|
85
|
+
case "hexagon": {
|
|
86
|
+
const R = maxRadius * Math.SQRT2;
|
|
87
|
+
const verts: string[] = [];
|
|
88
|
+
for (let i = 0; i < 6; i++) {
|
|
89
|
+
const a = -Math.PI / 2 + (i * Math.PI) / 3;
|
|
90
|
+
verts.push(`${cx + R * Math.cos(a)}px ${cy + R * Math.sin(a)}px`);
|
|
91
|
+
}
|
|
92
|
+
return [polygonCollapsed(cx, cy, 6), `polygon(${verts.join(", ")})`];
|
|
93
|
+
}
|
|
94
|
+
case "rectangle": {
|
|
95
|
+
const halfW = Math.max(cx, viewportWidth - cx);
|
|
96
|
+
const halfH = Math.max(cy, viewportHeight - cy);
|
|
97
|
+
const end = [
|
|
98
|
+
`${cx - halfW}px ${cy - halfH}px`,
|
|
99
|
+
`${cx + halfW}px ${cy - halfH}px`,
|
|
100
|
+
`${cx + halfW}px ${cy + halfH}px`,
|
|
101
|
+
`${cx - halfW}px ${cy + halfH}px`,
|
|
102
|
+
].join(", ");
|
|
103
|
+
return [polygonCollapsed(cx, cy, 4), `polygon(${end})`];
|
|
104
|
+
}
|
|
105
|
+
case "star": {
|
|
106
|
+
// Small overscan so the last frames never leave a 1px seam.
|
|
107
|
+
const R = maxRadius * Math.SQRT2 * 1.03;
|
|
108
|
+
const innerRatio = 0.42;
|
|
109
|
+
const starPolygon = (radius: number) => {
|
|
110
|
+
const verts: string[] = [];
|
|
111
|
+
for (let i = 0; i < 5; i++) {
|
|
112
|
+
const outerA = -Math.PI / 2 + (i * 2 * Math.PI) / 5;
|
|
113
|
+
verts.push(
|
|
114
|
+
`${cx + radius * Math.cos(outerA)}px ${cy + radius * Math.sin(outerA)}px`,
|
|
115
|
+
);
|
|
116
|
+
const innerA = outerA + Math.PI / 5;
|
|
117
|
+
verts.push(
|
|
118
|
+
`${cx + radius * innerRatio * Math.cos(innerA)}px ${cy + radius * innerRatio * Math.sin(innerA)}px`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return `polygon(${verts.join(", ")})`;
|
|
122
|
+
};
|
|
123
|
+
const startR = Math.max(2, R * 0.025);
|
|
124
|
+
return [starPolygon(startR), starPolygon(R)];
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return [
|
|
128
|
+
`circle(0px at ${cx}px ${cy}px)`,
|
|
129
|
+
`circle(${maxRadius}px at ${cx}px ${cy}px)`,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type DocumentWithViewTransition = Document & {
|
|
135
|
+
startViewTransition?: (callback: () => void) => {
|
|
136
|
+
finished: Promise<void>;
|
|
137
|
+
ready: Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Animated light/dark toggle using the View Transitions API.
|
|
143
|
+
*
|
|
144
|
+
* Drives the kit's `ThemeProvider` (via `useTheme`), so persistence and the
|
|
145
|
+
* `data-theme` attribute are handled for you. On browsers without the View
|
|
146
|
+
* Transitions API it falls back to an instant toggle.
|
|
147
|
+
*
|
|
148
|
+
* Requires the kit stylesheet (`@countryclub/ui-react/styles.css`), which
|
|
149
|
+
* defines the scoped `::view-transition` rules.
|
|
150
|
+
*/
|
|
151
|
+
function AnimatedThemeToggler({
|
|
152
|
+
className,
|
|
153
|
+
duration = 400,
|
|
154
|
+
variant,
|
|
155
|
+
fromCenter = false,
|
|
156
|
+
...props
|
|
157
|
+
}: AnimatedThemeTogglerProps) {
|
|
158
|
+
const shape = variant ?? "circle";
|
|
159
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
160
|
+
const [mounted, setMounted] = useState(false);
|
|
161
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
setMounted(true);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const isDark = resolvedTheme === "dark";
|
|
168
|
+
|
|
169
|
+
const toggleTheme = useCallback(() => {
|
|
170
|
+
const button = buttonRef.current;
|
|
171
|
+
if (!button) return;
|
|
172
|
+
|
|
173
|
+
const applyTheme = () => {
|
|
174
|
+
setTheme(isDark ? "light" : "dark");
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const doc = document as DocumentWithViewTransition;
|
|
178
|
+
if (typeof doc.startViewTransition !== "function") {
|
|
179
|
+
applyTheme();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
|
|
184
|
+
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
|
185
|
+
|
|
186
|
+
let x: number;
|
|
187
|
+
let y: number;
|
|
188
|
+
if (fromCenter) {
|
|
189
|
+
x = viewportWidth / 2;
|
|
190
|
+
y = viewportHeight / 2;
|
|
191
|
+
} else {
|
|
192
|
+
const { top, left, width, height } = button.getBoundingClientRect();
|
|
193
|
+
x = left + width / 2;
|
|
194
|
+
y = top + height / 2;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const maxRadius = Math.hypot(
|
|
198
|
+
Math.max(x, viewportWidth - x),
|
|
199
|
+
Math.max(y, viewportHeight - y),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const clipPath = getThemeTransitionClipPaths(
|
|
203
|
+
shape,
|
|
204
|
+
x,
|
|
205
|
+
y,
|
|
206
|
+
maxRadius,
|
|
207
|
+
viewportWidth,
|
|
208
|
+
viewportHeight,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const root = document.documentElement;
|
|
212
|
+
root.dataset.themeVt = "active";
|
|
213
|
+
root.style.setProperty("--theme-toggle-vt-duration", `${duration}ms`);
|
|
214
|
+
// Origin for the CSS initial clip-path, so the new snapshot starts hidden
|
|
215
|
+
// at the same point the JS animation expands from (prevents a pre-animation
|
|
216
|
+
// flash of the new theme).
|
|
217
|
+
root.style.setProperty("--theme-vt-x", `${x}px`);
|
|
218
|
+
root.style.setProperty("--theme-vt-y", `${y}px`);
|
|
219
|
+
const cleanup = () => {
|
|
220
|
+
delete root.dataset.themeVt;
|
|
221
|
+
root.style.removeProperty("--theme-toggle-vt-duration");
|
|
222
|
+
root.style.removeProperty("--theme-vt-x");
|
|
223
|
+
root.style.removeProperty("--theme-vt-y");
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const transition = doc.startViewTransition(() => {
|
|
227
|
+
flushSync(applyTheme);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
transition.finished.finally(cleanup);
|
|
231
|
+
|
|
232
|
+
transition.ready.then(() => {
|
|
233
|
+
document.documentElement.animate(
|
|
234
|
+
{ clipPath },
|
|
235
|
+
{
|
|
236
|
+
duration,
|
|
237
|
+
// Star: linear avoids easing overshoot that fights polygon interpolation.
|
|
238
|
+
easing: shape === "star" ? "linear" : "ease-in-out",
|
|
239
|
+
fill: "forwards",
|
|
240
|
+
pseudoElement: "::view-transition-new(root)",
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
}, [shape, fromCenter, duration, isDark, setTheme]);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<button
|
|
248
|
+
aria-label="Toggle theme"
|
|
249
|
+
className={cn(
|
|
250
|
+
buttonVariants({ size: "icon", variant: "ghost" }),
|
|
251
|
+
className,
|
|
252
|
+
)}
|
|
253
|
+
data-slot="animated-theme-toggler"
|
|
254
|
+
onClick={toggleTheme}
|
|
255
|
+
ref={buttonRef}
|
|
256
|
+
type="button"
|
|
257
|
+
{...props}
|
|
258
|
+
>
|
|
259
|
+
{mounted && isDark ? <SunIcon /> : <MoonIcon />}
|
|
260
|
+
<span className="sr-only">Toggle theme</span>
|
|
261
|
+
</button>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { AnimatedThemeToggler };
|
|
@@ -0,0 +1,182 @@
|
|
|
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 appStoreButtonVariants = cva(
|
|
11
|
+
"relative inline-flex h-12 shrink-0 cursor-pointer items-center justify-center gap-2.5 whitespace-nowrap rounded-lg border px-[calc(--spacing(3.5)-1px)] text-start outline-none transition-[box-shadow,filter] duration-100 [: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-6 [&_svg]:shrink-0",
|
|
12
|
+
{
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
variant: "default",
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
variant: {
|
|
18
|
+
default:
|
|
19
|
+
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-primary/90",
|
|
20
|
+
outline:
|
|
21
|
+
"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",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function GooglePlayLogoIcon(props: React.ComponentProps<"svg">) {
|
|
28
|
+
return (
|
|
29
|
+
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
30
|
+
<path
|
|
31
|
+
d="M1.337.924a1.486 1.486 0 0 0-.112.568v21.017c0 .217.045.419.124.6l11.155-11.087L1.337.924Z"
|
|
32
|
+
fill="#00AFFF"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
d="M13.544 10.989 16.802 7.75 3.45.195a1.466 1.466 0 0 0-.946-.179l11.04 10.973Z"
|
|
36
|
+
fill="#00DE76"
|
|
37
|
+
/>
|
|
38
|
+
<path
|
|
39
|
+
d="m22.018 13.298-3.919 2.218-3.515-3.493 3.543-3.521 3.891 2.202a1.49 1.49 0 0 1 0 2.594Z"
|
|
40
|
+
fill="#FFBD00"
|
|
41
|
+
/>
|
|
42
|
+
<path
|
|
43
|
+
d="m13.544 13.056-11 10.933c.298.036.612-.016.906-.183l13.324-7.54-3.23-3.21Z"
|
|
44
|
+
fill="#E12653"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AppleLogoIcon(props: React.ComponentProps<"svg">) {
|
|
51
|
+
return (
|
|
52
|
+
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
53
|
+
<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" />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function GalaxyStoreLogoIcon(props: React.ComponentProps<"svg">) {
|
|
59
|
+
return (
|
|
60
|
+
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
61
|
+
<path
|
|
62
|
+
d="M16.5 7.2v-.55a4.5 4.5 0 1 0-9 0v.55H5.1c-1 0-1.85.74-1.98 1.74l-1.05 8A2.9 2.9 0 0 0 4.95 20.2h14.1a2.9 2.9 0 0 0 2.88-3.28l-1.05-8A2 2 0 0 0 18.9 7.2h-2.4Zm-7.2-.55a2.7 2.7 0 1 1 5.4 0v.55H9.3v-.55Z"
|
|
63
|
+
fill="#4D7CFE"
|
|
64
|
+
fillRule="evenodd"
|
|
65
|
+
clipRule="evenodd"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
d="M12.34 9.74c.48 2.43 1.16 3.1 3.59 3.59.35.07.35.57 0 .64-2.43.48-3.1 1.16-3.59 3.59-.07.35-.57.35-.64 0-.48-2.43-1.16-3.1-3.59-3.59-.35-.07-.35-.57 0-.64 2.43-.48 3.1-1.16 3.59-3.59.07-.35.57-.35.64 0Z"
|
|
69
|
+
fill="#fff"
|
|
70
|
+
/>
|
|
71
|
+
</svg>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function AppGalleryLogoIcon(props: React.ComponentProps<"svg">) {
|
|
76
|
+
return (
|
|
77
|
+
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
78
|
+
<rect x="2.4" y="2.4" width="19.2" height="19.2" rx="4.8" fill="#E2334D" />
|
|
79
|
+
<path
|
|
80
|
+
d="M8.9 9V7.7a3.1 3.1 0 0 1 6.2 0V9"
|
|
81
|
+
stroke="#fff"
|
|
82
|
+
strokeWidth="1.5"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
/>
|
|
85
|
+
<path
|
|
86
|
+
d="M6.55 9h10.9c.58 0 1.05.47 1.05 1.05v6.1c0 .58-.47 1.05-1.05 1.05H6.55a1.05 1.05 0 0 1-1.05-1.05v-6.1C5.5 9.47 5.97 9 6.55 9Z"
|
|
87
|
+
stroke="#fff"
|
|
88
|
+
strokeWidth="1.5"
|
|
89
|
+
/>
|
|
90
|
+
</svg>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type AppStoreButtonStore =
|
|
95
|
+
| "google-play"
|
|
96
|
+
| "app-store"
|
|
97
|
+
| "galaxy-store"
|
|
98
|
+
| "app-gallery";
|
|
99
|
+
|
|
100
|
+
const stores: Record<
|
|
101
|
+
AppStoreButtonStore,
|
|
102
|
+
{
|
|
103
|
+
eyebrow: string;
|
|
104
|
+
name: string;
|
|
105
|
+
Icon: (props: React.ComponentProps<"svg">) => React.ReactElement;
|
|
106
|
+
}
|
|
107
|
+
> = {
|
|
108
|
+
"app-gallery": {
|
|
109
|
+
eyebrow: "EXPLORE IT ON",
|
|
110
|
+
name: "AppGallery",
|
|
111
|
+
Icon: AppGalleryLogoIcon,
|
|
112
|
+
},
|
|
113
|
+
"app-store": {
|
|
114
|
+
eyebrow: "Download on the",
|
|
115
|
+
name: "App Store",
|
|
116
|
+
Icon: AppleLogoIcon,
|
|
117
|
+
},
|
|
118
|
+
"galaxy-store": {
|
|
119
|
+
eyebrow: "Available on",
|
|
120
|
+
name: "Galaxy Store",
|
|
121
|
+
Icon: GalaxyStoreLogoIcon,
|
|
122
|
+
},
|
|
123
|
+
"google-play": {
|
|
124
|
+
eyebrow: "GET IT ON",
|
|
125
|
+
name: "Google Play",
|
|
126
|
+
Icon: GooglePlayLogoIcon,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
interface AppStoreButtonProps extends useRender.ComponentProps<"button"> {
|
|
131
|
+
/** Which store badge to render: google-play, app-store, galaxy-store or app-gallery. */
|
|
132
|
+
store: AppStoreButtonStore;
|
|
133
|
+
variant?: VariantProps<typeof appStoreButtonVariants>["variant"];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function AppStoreButton({
|
|
137
|
+
className,
|
|
138
|
+
store,
|
|
139
|
+
variant = "default",
|
|
140
|
+
render,
|
|
141
|
+
...props
|
|
142
|
+
}: AppStoreButtonProps) {
|
|
143
|
+
const { eyebrow, name, Icon } = stores[store];
|
|
144
|
+
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
|
|
145
|
+
render ? undefined : "button";
|
|
146
|
+
|
|
147
|
+
const defaultProps = {
|
|
148
|
+
className: cn(appStoreButtonVariants({ className, variant })),
|
|
149
|
+
"data-slot": "app-store-button",
|
|
150
|
+
"data-store": store,
|
|
151
|
+
type: typeValue,
|
|
152
|
+
children: (
|
|
153
|
+
<>
|
|
154
|
+
<Icon aria-hidden="true" />
|
|
155
|
+
<span className="flex flex-col" data-slot="app-store-button-label">
|
|
156
|
+
<span className="font-medium text-[10px] leading-3.5 opacity-72">
|
|
157
|
+
{eyebrow}
|
|
158
|
+
</span>
|
|
159
|
+
<span className="font-semibold text-base leading-5 tracking-[-0.01em]">
|
|
160
|
+
{name}
|
|
161
|
+
</span>
|
|
162
|
+
</span>
|
|
163
|
+
</>
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return useRender({
|
|
168
|
+
defaultTagName: "button",
|
|
169
|
+
props: mergeProps<"button">(defaultProps, props),
|
|
170
|
+
render,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
AppStoreButton,
|
|
176
|
+
appStoreButtonVariants,
|
|
177
|
+
GooglePlayLogoIcon,
|
|
178
|
+
AppleLogoIcon,
|
|
179
|
+
GalaxyStoreLogoIcon,
|
|
180
|
+
AppGalleryLogoIcon,
|
|
181
|
+
type AppStoreButtonStore,
|
|
182
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils/css";
|
|
4
|
+
|
|
5
|
+
function AspectRatio({
|
|
6
|
+
ratio = 1,
|
|
7
|
+
className,
|
|
8
|
+
style,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<"div"> & {
|
|
11
|
+
ratio?: number;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cn("relative w-full overflow-hidden", className)}
|
|
16
|
+
data-slot="aspect-ratio"
|
|
17
|
+
style={{ aspectRatio: ratio, ...style }}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { AspectRatio };
|