@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.
Files changed (124) hide show
  1. package/dist/chunk-HDGMSYQS.js +26461 -0
  2. package/dist/chunk-HDGMSYQS.js.map +1 -0
  3. package/dist/chunk-PR4QN5HX.js +39 -0
  4. package/dist/chunk-PR4QN5HX.js.map +1 -0
  5. package/dist/form.d.ts +175 -0
  6. package/dist/form.js +5207 -0
  7. package/dist/form.js.map +1 -0
  8. package/dist/index.d.ts +1462 -0
  9. package/dist/index.js +81862 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/input-CZvh825j.d.ts +24 -0
  12. package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
  13. package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
  14. package/package.json +79 -0
  15. package/src/components/form/checkbox-group-field.tsx +101 -0
  16. package/src/components/form/date-field.tsx +79 -0
  17. package/src/components/form/date-range-field.tsx +106 -0
  18. package/src/components/form/form-context.ts +10 -0
  19. package/src/components/form/form.tsx +54 -0
  20. package/src/components/form/number-field.tsx +69 -0
  21. package/src/components/form/select-field.tsx +76 -0
  22. package/src/components/form/submit-button.tsx +28 -0
  23. package/src/components/form/text-field.tsx +107 -0
  24. package/src/components/layout/dashboard-header.tsx +54 -0
  25. package/src/components/layout/dashboard-panel.tsx +34 -0
  26. package/src/components/theme-provider.tsx +403 -0
  27. package/src/components/ui/accordion.tsx +69 -0
  28. package/src/components/ui/alert-dialog.tsx +169 -0
  29. package/src/components/ui/alert.tsx +80 -0
  30. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  31. package/src/components/ui/app-store-buttons.tsx +182 -0
  32. package/src/components/ui/aspect-ratio.tsx +23 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +95 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/checkbox-group.tsx +16 -0
  45. package/src/components/ui/checkbox.tsx +82 -0
  46. package/src/components/ui/collapsible.tsx +45 -0
  47. package/src/components/ui/combobox.tsx +411 -0
  48. package/src/components/ui/command.tsx +264 -0
  49. package/src/components/ui/context-menu.tsx +271 -0
  50. package/src/components/ui/credit-card.tsx +214 -0
  51. package/src/components/ui/dialog.tsx +196 -0
  52. package/src/components/ui/drawer.tsx +135 -0
  53. package/src/components/ui/empty.tsx +127 -0
  54. package/src/components/ui/featured-icon.tsx +149 -0
  55. package/src/components/ui/field.tsx +88 -0
  56. package/src/components/ui/fieldset.tsx +29 -0
  57. package/src/components/ui/form.tsx +17 -0
  58. package/src/components/ui/frame.tsx +82 -0
  59. package/src/components/ui/generic-empty.tsx +142 -0
  60. package/src/components/ui/group.tsx +97 -0
  61. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  62. package/src/components/ui/input-group.tsx +102 -0
  63. package/src/components/ui/input-otp.tsx +96 -0
  64. package/src/components/ui/input.tsx +66 -0
  65. package/src/components/ui/item.tsx +198 -0
  66. package/src/components/ui/kbd.tsx +30 -0
  67. package/src/components/ui/label.tsx +28 -0
  68. package/src/components/ui/menu.tsx +312 -0
  69. package/src/components/ui/menubar.tsx +93 -0
  70. package/src/components/ui/meter.tsx +67 -0
  71. package/src/components/ui/multi-select.tsx +308 -0
  72. package/src/components/ui/navigation-menu.tsx +143 -0
  73. package/src/components/ui/number-field.tsx +160 -0
  74. package/src/components/ui/pagination-controls.tsx +74 -0
  75. package/src/components/ui/pagination.tsx +149 -0
  76. package/src/components/ui/popover.tsx +119 -0
  77. package/src/components/ui/preview-card.tsx +55 -0
  78. package/src/components/ui/progress.tsx +289 -0
  79. package/src/components/ui/qr-code.tsx +150 -0
  80. package/src/components/ui/radio-group.tsx +103 -0
  81. package/src/components/ui/resizable.tsx +56 -0
  82. package/src/components/ui/scroll-area.tsx +90 -0
  83. package/src/components/ui/scroller.tsx +38 -0
  84. package/src/components/ui/section-header.tsx +118 -0
  85. package/src/components/ui/select.tsx +181 -0
  86. package/src/components/ui/separator.tsx +23 -0
  87. package/src/components/ui/sheet.tsx +224 -0
  88. package/src/components/ui/sidebar.tsx +744 -0
  89. package/src/components/ui/skeleton.tsx +16 -0
  90. package/src/components/ui/slider.tsx +108 -0
  91. package/src/components/ui/smooth-scroll.tsx +143 -0
  92. package/src/components/ui/social-button.tsx +247 -0
  93. package/src/components/ui/spinner-on-demand.tsx +32 -0
  94. package/src/components/ui/spinner.tsx +18 -0
  95. package/src/components/ui/stat.tsx +187 -0
  96. package/src/components/ui/stepper.tsx +167 -0
  97. package/src/components/ui/switch.tsx +56 -0
  98. package/src/components/ui/table.tsx +126 -0
  99. package/src/components/ui/tabs.tsx +90 -0
  100. package/src/components/ui/tag.tsx +229 -0
  101. package/src/components/ui/target-countdown.tsx +46 -0
  102. package/src/components/ui/text-editor.tsx +313 -0
  103. package/src/components/ui/textarea.tsx +51 -0
  104. package/src/components/ui/timeline.tsx +116 -0
  105. package/src/components/ui/toast.tsx +268 -0
  106. package/src/components/ui/toggle-group.tsx +101 -0
  107. package/src/components/ui/toggle.tsx +45 -0
  108. package/src/components/ui/toolbar.tsx +89 -0
  109. package/src/components/ui/tooltip.tsx +102 -0
  110. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  111. package/src/components/ui/video-player.tsx +275 -0
  112. package/src/components/upload/avatar-upload-base.tsx +131 -0
  113. package/src/components/upload/image-upload-base.tsx +112 -0
  114. package/src/form.ts +17 -0
  115. package/src/index.ts +125 -0
  116. package/src/lib/hooks/use-callback-ref.ts +15 -0
  117. package/src/lib/hooks/use-first-render.ts +11 -0
  118. package/src/lib/hooks/use-hover.ts +53 -0
  119. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  120. package/src/lib/hooks/use-media-query.ts +164 -0
  121. package/src/lib/utils/css.ts +6 -0
  122. package/src/styles.css +300 -0
  123. package/src/types/helpers.ts +24 -0
  124. 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
+ };