@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.
Files changed (138) hide show
  1. package/dist/chunk-2HQPCV2L.js +895 -0
  2. package/dist/chunk-2HQPCV2L.js.map +1 -0
  3. package/dist/form.d.ts +175 -0
  4. package/dist/form.js +356 -0
  5. package/dist/form.js.map +1 -0
  6. package/dist/index.d.ts +2162 -0
  7. package/dist/index.js +11640 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/input-JCZs61MO.d.ts +24 -0
  10. package/package.json +90 -0
  11. package/src/components/form/checkbox-group-field.tsx +101 -0
  12. package/src/components/form/date-field.tsx +79 -0
  13. package/src/components/form/date-range-field.tsx +106 -0
  14. package/src/components/form/form-context.ts +10 -0
  15. package/src/components/form/form.tsx +54 -0
  16. package/src/components/form/number-field.tsx +69 -0
  17. package/src/components/form/select-field.tsx +76 -0
  18. package/src/components/form/submit-button.tsx +28 -0
  19. package/src/components/form/text-field.tsx +107 -0
  20. package/src/components/layout/dashboard-header.tsx +54 -0
  21. package/src/components/layout/dashboard-panel.tsx +34 -0
  22. package/src/components/layout/dashboard-topbar.tsx +221 -0
  23. package/src/components/theme-provider.tsx +403 -0
  24. package/src/components/ui/accordion.tsx +69 -0
  25. package/src/components/ui/alert-dialog.tsx +169 -0
  26. package/src/components/ui/alert.tsx +80 -0
  27. package/src/components/ui/animated-counter.tsx +109 -0
  28. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  29. package/src/components/ui/app-store-buttons.tsx +182 -0
  30. package/src/components/ui/aspect-ratio.tsx +23 -0
  31. package/src/components/ui/aurora-orb.tsx +163 -0
  32. package/src/components/ui/aurora-text.tsx +76 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +150 -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/chat-divider.tsx +67 -0
  45. package/src/components/ui/chat.tsx +494 -0
  46. package/src/components/ui/checkbox-group.tsx +16 -0
  47. package/src/components/ui/checkbox.tsx +82 -0
  48. package/src/components/ui/collapsible.tsx +45 -0
  49. package/src/components/ui/color-picker.tsx +389 -0
  50. package/src/components/ui/combobox.tsx +411 -0
  51. package/src/components/ui/command.tsx +264 -0
  52. package/src/components/ui/confetti.tsx +196 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/context-meter.tsx +134 -0
  55. package/src/components/ui/credit-card.tsx +214 -0
  56. package/src/components/ui/data-table-paged.tsx +163 -0
  57. package/src/components/ui/date-picker.tsx +154 -0
  58. package/src/components/ui/dialog.tsx +196 -0
  59. package/src/components/ui/drawer.tsx +135 -0
  60. package/src/components/ui/empty.tsx +127 -0
  61. package/src/components/ui/fab.tsx +45 -0
  62. package/src/components/ui/featured-icon.tsx +149 -0
  63. package/src/components/ui/field.tsx +88 -0
  64. package/src/components/ui/fieldset.tsx +29 -0
  65. package/src/components/ui/form.tsx +17 -0
  66. package/src/components/ui/frame.tsx +82 -0
  67. package/src/components/ui/generic-empty.tsx +142 -0
  68. package/src/components/ui/group.tsx +97 -0
  69. package/src/components/ui/help-tip.tsx +94 -0
  70. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  71. package/src/components/ui/input-group.tsx +102 -0
  72. package/src/components/ui/input-otp.tsx +96 -0
  73. package/src/components/ui/input.tsx +66 -0
  74. package/src/components/ui/item.tsx +198 -0
  75. package/src/components/ui/kbd.tsx +30 -0
  76. package/src/components/ui/label.tsx +28 -0
  77. package/src/components/ui/markdown.tsx +188 -0
  78. package/src/components/ui/menu.tsx +312 -0
  79. package/src/components/ui/menubar.tsx +93 -0
  80. package/src/components/ui/meter.tsx +67 -0
  81. package/src/components/ui/multi-select.tsx +308 -0
  82. package/src/components/ui/navigation-menu.tsx +143 -0
  83. package/src/components/ui/number-field.tsx +160 -0
  84. package/src/components/ui/pagination-controls.tsx +74 -0
  85. package/src/components/ui/pagination.tsx +149 -0
  86. package/src/components/ui/pipeline.tsx +102 -0
  87. package/src/components/ui/popover.tsx +119 -0
  88. package/src/components/ui/preview-card.tsx +55 -0
  89. package/src/components/ui/progress.tsx +289 -0
  90. package/src/components/ui/qr-code.tsx +150 -0
  91. package/src/components/ui/radio-group.tsx +103 -0
  92. package/src/components/ui/resizable.tsx +56 -0
  93. package/src/components/ui/scroll-area.tsx +90 -0
  94. package/src/components/ui/scroller.tsx +38 -0
  95. package/src/components/ui/section-header.tsx +118 -0
  96. package/src/components/ui/segmented-control.tsx +83 -0
  97. package/src/components/ui/select.tsx +181 -0
  98. package/src/components/ui/separator.tsx +23 -0
  99. package/src/components/ui/sheet.tsx +224 -0
  100. package/src/components/ui/sidebar.tsx +774 -0
  101. package/src/components/ui/skeleton.tsx +16 -0
  102. package/src/components/ui/slider.tsx +108 -0
  103. package/src/components/ui/smooth-scroll.tsx +206 -0
  104. package/src/components/ui/social-button.tsx +247 -0
  105. package/src/components/ui/spinner-on-demand.tsx +32 -0
  106. package/src/components/ui/spinner.tsx +18 -0
  107. package/src/components/ui/stat.tsx +187 -0
  108. package/src/components/ui/stepper.tsx +167 -0
  109. package/src/components/ui/switch.tsx +56 -0
  110. package/src/components/ui/table.tsx +126 -0
  111. package/src/components/ui/tabs.tsx +90 -0
  112. package/src/components/ui/tag.tsx +242 -0
  113. package/src/components/ui/target-countdown.tsx +46 -0
  114. package/src/components/ui/text-editor.tsx +313 -0
  115. package/src/components/ui/textarea.tsx +51 -0
  116. package/src/components/ui/thinking-orb.tsx +6 -0
  117. package/src/components/ui/time-picker.tsx +308 -0
  118. package/src/components/ui/timeline.tsx +116 -0
  119. package/src/components/ui/toast.tsx +268 -0
  120. package/src/components/ui/toggle-group.tsx +101 -0
  121. package/src/components/ui/toggle.tsx +45 -0
  122. package/src/components/ui/toolbar.tsx +89 -0
  123. package/src/components/ui/tooltip.tsx +102 -0
  124. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  125. package/src/components/ui/video-player.tsx +275 -0
  126. package/src/components/upload/avatar-upload-base.tsx +131 -0
  127. package/src/components/upload/image-upload-base.tsx +112 -0
  128. package/src/form.ts +17 -0
  129. package/src/index.ts +143 -0
  130. package/src/lib/hooks/use-callback-ref.ts +15 -0
  131. package/src/lib/hooks/use-first-render.ts +11 -0
  132. package/src/lib/hooks/use-hover.ts +53 -0
  133. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  134. package/src/lib/hooks/use-media-query.ts +164 -0
  135. package/src/lib/utils/css.ts +6 -0
  136. package/src/styles.css +329 -0
  137. package/src/types/helpers.ts +24 -0
  138. 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 };