@cntyclub/ui-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -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/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-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/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -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/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/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -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/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/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/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/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/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 +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -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 +229 -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/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 +125 -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 +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
|
|
8
|
+
|
|
9
|
+
const AlertDialog = AlertDialogPrimitive.Root;
|
|
10
|
+
|
|
11
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
12
|
+
|
|
13
|
+
function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
|
|
14
|
+
return (
|
|
15
|
+
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function AlertDialogBackdrop({
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: AlertDialogPrimitive.Backdrop.Props) {
|
|
23
|
+
return (
|
|
24
|
+
<AlertDialogPrimitive.Backdrop
|
|
25
|
+
className={cn(
|
|
26
|
+
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 ease-out data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
data-slot="alert-dialog-backdrop"
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function AlertDialogViewport({
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: AlertDialogPrimitive.Viewport.Props) {
|
|
39
|
+
return (
|
|
40
|
+
<AlertDialogPrimitive.Viewport
|
|
41
|
+
className={cn(
|
|
42
|
+
"fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
data-slot="alert-dialog-viewport"
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function AlertDialogPopup({
|
|
52
|
+
className,
|
|
53
|
+
bottomStickOnMobile = true,
|
|
54
|
+
...props
|
|
55
|
+
}: AlertDialogPrimitive.Popup.Props & {
|
|
56
|
+
bottomStickOnMobile?: boolean;
|
|
57
|
+
}) {
|
|
58
|
+
return (
|
|
59
|
+
<AlertDialogPortal>
|
|
60
|
+
<AlertDialogBackdrop />
|
|
61
|
+
<AlertDialogViewport
|
|
62
|
+
className={cn(
|
|
63
|
+
bottomStickOnMobile &&
|
|
64
|
+
"max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<AlertDialogPrimitive.Popup
|
|
68
|
+
className={cn(
|
|
69
|
+
"-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
70
|
+
bottomStickOnMobile &&
|
|
71
|
+
"max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
data-slot="alert-dialog-popup"
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
</AlertDialogViewport>
|
|
78
|
+
</AlertDialogPortal>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function AlertDialogHeader({
|
|
83
|
+
className,
|
|
84
|
+
...props
|
|
85
|
+
}: React.ComponentProps<"div">) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={cn(
|
|
89
|
+
"flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left",
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
92
|
+
data-slot="alert-dialog-header"
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function AlertDialogFooter({
|
|
99
|
+
className,
|
|
100
|
+
variant = "default",
|
|
101
|
+
...props
|
|
102
|
+
}: React.ComponentProps<"div"> & {
|
|
103
|
+
variant?: "default" | "bare";
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
|
|
109
|
+
variant === "default" && "border-t bg-muted/72 py-4",
|
|
110
|
+
variant === "bare" && "pb-6",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
data-slot="alert-dialog-footer"
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function AlertDialogTitle({
|
|
120
|
+
className,
|
|
121
|
+
...props
|
|
122
|
+
}: AlertDialogPrimitive.Title.Props) {
|
|
123
|
+
return (
|
|
124
|
+
<AlertDialogPrimitive.Title
|
|
125
|
+
className={cn(
|
|
126
|
+
"font-heading font-semibold text-xl leading-none",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
data-slot="alert-dialog-title"
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function AlertDialogDescription({
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: AlertDialogPrimitive.Description.Props) {
|
|
139
|
+
return (
|
|
140
|
+
<AlertDialogPrimitive.Description
|
|
141
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
142
|
+
data-slot="alert-dialog-description"
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
|
|
149
|
+
return (
|
|
150
|
+
<AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
AlertDialogCreateHandle,
|
|
156
|
+
AlertDialog,
|
|
157
|
+
AlertDialogPortal,
|
|
158
|
+
AlertDialogBackdrop,
|
|
159
|
+
AlertDialogBackdrop as AlertDialogOverlay,
|
|
160
|
+
AlertDialogTrigger,
|
|
161
|
+
AlertDialogPopup,
|
|
162
|
+
AlertDialogPopup as AlertDialogContent,
|
|
163
|
+
AlertDialogHeader,
|
|
164
|
+
AlertDialogFooter,
|
|
165
|
+
AlertDialogTitle,
|
|
166
|
+
AlertDialogDescription,
|
|
167
|
+
AlertDialogClose,
|
|
168
|
+
AlertDialogViewport,
|
|
169
|
+
};
|
|
@@ -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,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
|
+
};
|