@hunterchen/canvas 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 (141) hide show
  1. package/dist/components/canvas/canvas.d.ts +29 -0
  2. package/dist/components/canvas/canvas.d.ts.map +1 -0
  3. package/dist/components/canvas/canvas.js +419 -0
  4. package/dist/components/canvas/canvas.js.map +1 -0
  5. package/dist/components/canvas/component.d.ts +47 -0
  6. package/dist/components/canvas/component.d.ts.map +1 -0
  7. package/dist/components/canvas/component.js +177 -0
  8. package/dist/components/canvas/component.js.map +1 -0
  9. package/dist/components/canvas/cursor.d.ts +8 -0
  10. package/dist/components/canvas/cursor.d.ts.map +1 -0
  11. package/dist/components/canvas/cursor.js +32 -0
  12. package/dist/components/canvas/cursor.js.map +1 -0
  13. package/dist/components/canvas/draggable.d.ts +21 -0
  14. package/dist/components/canvas/draggable.d.ts.map +1 -0
  15. package/dist/components/canvas/draggable.js +163 -0
  16. package/dist/components/canvas/draggable.js.map +1 -0
  17. package/dist/components/canvas/navbar/index.d.ts +19 -0
  18. package/dist/components/canvas/navbar/index.d.ts.map +1 -0
  19. package/dist/components/canvas/navbar/index.js +106 -0
  20. package/dist/components/canvas/navbar/index.js.map +1 -0
  21. package/dist/components/canvas/navbar/single-button.d.ts +17 -0
  22. package/dist/components/canvas/navbar/single-button.d.ts.map +1 -0
  23. package/dist/components/canvas/navbar/single-button.js +97 -0
  24. package/dist/components/canvas/navbar/single-button.js.map +1 -0
  25. package/dist/components/canvas/offest.d.ts +6 -0
  26. package/dist/components/canvas/offest.d.ts.map +1 -0
  27. package/dist/components/canvas/offest.js +12 -0
  28. package/dist/components/canvas/offest.js.map +1 -0
  29. package/dist/components/canvas/reset.d.ts +5 -0
  30. package/dist/components/canvas/reset.d.ts.map +1 -0
  31. package/dist/components/canvas/reset.js +7 -0
  32. package/dist/components/canvas/reset.js.map +1 -0
  33. package/dist/components/canvas/toolbar.d.ts +7 -0
  34. package/dist/components/canvas/toolbar.d.ts.map +1 -0
  35. package/dist/components/canvas/toolbar.js +28 -0
  36. package/dist/components/canvas/toolbar.js.map +1 -0
  37. package/dist/components/canvas/wrapper.d.ts +26 -0
  38. package/dist/components/canvas/wrapper.d.ts.map +1 -0
  39. package/dist/components/canvas/wrapper.js +107 -0
  40. package/dist/components/canvas/wrapper.js.map +1 -0
  41. package/dist/components/ui/FolderIcon.d.ts +9 -0
  42. package/dist/components/ui/FolderIcon.d.ts.map +1 -0
  43. package/dist/components/ui/FolderIcon.js +25 -0
  44. package/dist/components/ui/FolderIcon.js.map +1 -0
  45. package/dist/components/ui/button.d.ts +14 -0
  46. package/dist/components/ui/button.d.ts.map +1 -0
  47. package/dist/components/ui/button.js +54 -0
  48. package/dist/components/ui/button.js.map +1 -0
  49. package/dist/components/ui/label.d.ts +6 -0
  50. package/dist/components/ui/label.d.ts.map +1 -0
  51. package/dist/components/ui/label.js +10 -0
  52. package/dist/components/ui/label.js.map +1 -0
  53. package/dist/components/ui/toast.d.ts +16 -0
  54. package/dist/components/ui/toast.d.ts.map +1 -0
  55. package/dist/components/ui/toast.js +41 -0
  56. package/dist/components/ui/toast.js.map +1 -0
  57. package/dist/components/ui/toaster.d.ts +2 -0
  58. package/dist/components/ui/toaster.d.ts.map +1 -0
  59. package/dist/components/ui/toaster.js +10 -0
  60. package/dist/components/ui/toaster.js.map +1 -0
  61. package/dist/contexts/CanvasContext.d.ts +26 -0
  62. package/dist/contexts/CanvasContext.d.ts.map +1 -0
  63. package/dist/contexts/CanvasContext.js +22 -0
  64. package/dist/contexts/CanvasContext.js.map +1 -0
  65. package/dist/contexts/PerformanceContext.d.ts +31 -0
  66. package/dist/contexts/PerformanceContext.d.ts.map +1 -0
  67. package/dist/contexts/PerformanceContext.js +56 -0
  68. package/dist/contexts/PerformanceContext.js.map +1 -0
  69. package/dist/hooks/use-mobile.d.ts +2 -0
  70. package/dist/hooks/use-mobile.d.ts.map +1 -0
  71. package/dist/hooks/use-mobile.js +16 -0
  72. package/dist/hooks/use-mobile.js.map +1 -0
  73. package/dist/hooks/use-toast.d.ts +45 -0
  74. package/dist/hooks/use-toast.d.ts.map +1 -0
  75. package/dist/hooks/use-toast.js +126 -0
  76. package/dist/hooks/use-toast.js.map +1 -0
  77. package/dist/hooks/usePerformanceMode.d.ts +6 -0
  78. package/dist/hooks/usePerformanceMode.d.ts.map +1 -0
  79. package/dist/hooks/usePerformanceMode.js +6 -0
  80. package/dist/hooks/usePerformanceMode.js.map +1 -0
  81. package/dist/hooks/useWindowDimensions.d.ts +7 -0
  82. package/dist/hooks/useWindowDimensions.d.ts.map +1 -0
  83. package/dist/hooks/useWindowDimensions.js +22 -0
  84. package/dist/hooks/useWindowDimensions.js.map +1 -0
  85. package/dist/index.d.ts +26 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +28 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/lib/canvas.d.ts +35 -0
  90. package/dist/lib/canvas.d.ts.map +1 -0
  91. package/dist/lib/canvas.js +82 -0
  92. package/dist/lib/canvas.js.map +1 -0
  93. package/dist/lib/constants.d.ts +78 -0
  94. package/dist/lib/constants.d.ts.map +1 -0
  95. package/dist/lib/constants.js +122 -0
  96. package/dist/lib/constants.js.map +1 -0
  97. package/dist/lib/copy.d.ts +2 -0
  98. package/dist/lib/copy.d.ts.map +1 -0
  99. package/dist/lib/copy.js +20 -0
  100. package/dist/lib/copy.js.map +1 -0
  101. package/dist/lib/utils.d.ts +4 -0
  102. package/dist/lib/utils.d.ts.map +1 -0
  103. package/dist/lib/utils.js +14 -0
  104. package/dist/lib/utils.js.map +1 -0
  105. package/dist/types/index.d.ts +19 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/index.js +14 -0
  108. package/dist/types/index.js.map +1 -0
  109. package/dist/utils/performance.d.ts +9 -0
  110. package/dist/utils/performance.d.ts.map +1 -0
  111. package/dist/utils/performance.js +29 -0
  112. package/dist/utils/performance.js.map +1 -0
  113. package/package.json +55 -0
  114. package/src/components/canvas/canvas.tsx +728 -0
  115. package/src/components/canvas/component.tsx +230 -0
  116. package/src/components/canvas/cursor.tsx +161 -0
  117. package/src/components/canvas/draggable.tsx +298 -0
  118. package/src/components/canvas/navbar/index.tsx +213 -0
  119. package/src/components/canvas/navbar/single-button.tsx +199 -0
  120. package/src/components/canvas/offest.tsx +23 -0
  121. package/src/components/canvas/reset.tsx +21 -0
  122. package/src/components/canvas/toolbar.tsx +67 -0
  123. package/src/components/canvas/wrapper.tsx +219 -0
  124. package/src/components/ui/FolderIcon.tsx +116 -0
  125. package/src/components/ui/button.tsx +162 -0
  126. package/src/components/ui/label.tsx +24 -0
  127. package/src/components/ui/toast.tsx +136 -0
  128. package/src/components/ui/toaster.tsx +33 -0
  129. package/src/contexts/CanvasContext.tsx +54 -0
  130. package/src/contexts/PerformanceContext.tsx +81 -0
  131. package/src/hooks/use-mobile.ts +21 -0
  132. package/src/hooks/use-toast.ts +186 -0
  133. package/src/hooks/usePerformanceMode.ts +5 -0
  134. package/src/hooks/useWindowDimensions.ts +32 -0
  135. package/src/index.ts +36 -0
  136. package/src/lib/canvas.ts +132 -0
  137. package/src/lib/constants.ts +153 -0
  138. package/src/lib/copy.ts +18 -0
  139. package/src/lib/utils.ts +18 -0
  140. package/src/types/index.ts +20 -0
  141. package/src/utils/performance.ts +37 -0
@@ -0,0 +1,219 @@
1
+ import { motion, type MotionValue, type Transition, useMotionValue } from "framer-motion";
2
+ import { useState, useEffect, useRef, type ReactNode } from "react";
3
+ import Image from "next/image";
4
+ import {
5
+ MAX_DIM_RATIO,
6
+ GROW_TRANSITION,
7
+ BLUR_TRANSITION,
8
+ INTRO_ASPECT_RATIO,
9
+ } from "../../lib/constants";
10
+
11
+ // Re-export for backward compatibility
12
+ export { GROW_TRANSITION as growTransition } from "../../lib/constants";
13
+
14
+ interface CanvasWrapperProps {
15
+ children: React.ReactNode;
16
+ /** Shared progress MV (0->1) for the grow animation */
17
+ introProgress: MotionValue<number>;
18
+ /** Callback when the grow (stage1) completes */
19
+ onIntroGrowComplete?: () => void;
20
+
21
+ // ============== Intro Customization ==============
22
+ /** Disable intro animation entirely (starts at full size) */
23
+ skipIntro?: boolean;
24
+ /** Custom intro content to show during loading */
25
+ introContent?: ReactNode;
26
+ /** Custom loading text (default: "LOADING CANVAS") */
27
+ loadingText?: string;
28
+ /** Background gradient for intro screen */
29
+ introBackgroundGradient?: string;
30
+ /** Canvas box gradient for blur mask */
31
+ canvasBoxGradient?: string;
32
+ /** Grow animation transition config */
33
+ growTransition?: Transition;
34
+ /** Blur animation transition config */
35
+ blurTransition?: Transition;
36
+ }
37
+
38
+ /**
39
+ * Default intro content (Hack Western branding)
40
+ * Positioned in the upper third of the screen
41
+ */
42
+ const DefaultIntroContent = () => (
43
+ <div className="absolute left-1/2 top-24 flex -translate-x-1/2 flex-col items-center text-center">
44
+ <Image
45
+ src="/horse.svg"
46
+ alt="Hack Western Logo"
47
+ width={64}
48
+ height={64}
49
+ className="mb-4"
50
+ />
51
+ <div className="font-jetbrains-mono font-semibold text-[#543C5AB2]">
52
+ HACK WESTERN 12
53
+ </div>
54
+ </div>
55
+ );
56
+
57
+ export const CanvasWrapper = ({
58
+ children,
59
+ introProgress,
60
+ onIntroGrowComplete,
61
+ skipIntro = false,
62
+ introContent,
63
+ loadingText = "LOADING CANVAS",
64
+ introBackgroundGradient = "linear-gradient(to top, #FEB6AF 0%, var(--salmon) 15%, var(--beige) 50%)",
65
+ canvasBoxGradient = "radial-gradient(130.38% 95% at 50.03% 97.25%, #EFB8A0 0%, #EAD2DF 48.09%, #EFE3E1 100%)",
66
+ growTransition = GROW_TRANSITION,
67
+ blurTransition = BLUR_TRANSITION,
68
+ }: CanvasWrapperProps) => {
69
+ const [dimensions, setDimensions] = useState<{
70
+ width: number;
71
+ height: number;
72
+ } | null>(null);
73
+ const [dots, setDots] = useState<string>("..");
74
+ const [stage1NotFinished, setStage1NotFinished] = useState(true);
75
+ const completedRef = useRef(false);
76
+
77
+ // If skipIntro is true, immediately complete the intro
78
+ useEffect(() => {
79
+ if (skipIntro && !completedRef.current) {
80
+ completedRef.current = true;
81
+ introProgress.set(1);
82
+ onIntroGrowComplete?.();
83
+ }
84
+ }, [skipIntro, introProgress, onIntroGrowComplete]);
85
+
86
+ // add up to 4 dots, then go back down to 2
87
+ useEffect(() => {
88
+ if (skipIntro) return; // Don't animate dots if skipping intro
89
+
90
+ const interval = setInterval(() => {
91
+ setDots((prevDots) => {
92
+ if (prevDots.length < 3) {
93
+ return prevDots + ".";
94
+ } else {
95
+ return ".";
96
+ }
97
+ });
98
+ }, 500);
99
+ return () => clearInterval(interval);
100
+ }, [skipIntro]);
101
+
102
+ useEffect(() => {
103
+ if (skipIntro) return; // Don't calculate dimensions if skipping intro
104
+
105
+ // calculate the initial 3:2 box size with margins (client-only)
106
+ const calculateInitialSize = () => {
107
+ const vw = window.innerWidth;
108
+ const vh = window.innerHeight;
109
+
110
+ const maxWidth = vw * MAX_DIM_RATIO.width;
111
+ const maxHeight = vh * MAX_DIM_RATIO.height;
112
+
113
+ // width or height as limiter
114
+ if (maxWidth / INTRO_ASPECT_RATIO <= maxHeight) {
115
+ return { width: maxWidth, height: maxWidth / INTRO_ASPECT_RATIO };
116
+ } else {
117
+ return { height: maxHeight, width: maxHeight * INTRO_ASPECT_RATIO };
118
+ }
119
+ };
120
+
121
+ setDimensions(calculateInitialSize());
122
+ }, [skipIntro]);
123
+
124
+ // If skipIntro, render children directly without animation wrapper
125
+ if (skipIntro) {
126
+ return (
127
+ <motion.div
128
+ className="fixed inset-0 overflow-hidden"
129
+ style={{
130
+ touchAction: "none",
131
+ userSelect: "none",
132
+ pointerEvents: "none",
133
+ }}
134
+ onContextMenu={(e) => e.preventDefault()}
135
+ >
136
+ {children}
137
+ </motion.div>
138
+ );
139
+ }
140
+
141
+ return (
142
+ <motion.div
143
+ className="fixed inset-0 overflow-hidden"
144
+ style={{
145
+ backgroundImage: stage1NotFinished ? introBackgroundGradient : undefined,
146
+ touchAction: "none",
147
+ userSelect: "none",
148
+ pointerEvents: "none",
149
+ }}
150
+ onContextMenu={(e) => e.preventDefault()}
151
+ >
152
+ {stage1NotFinished && (
153
+ <>
154
+ {/* Render custom intro content or default */}
155
+ {introContent !== undefined ? introContent : <DefaultIntroContent />}
156
+ </>
157
+ )}
158
+
159
+ {dimensions && (
160
+ <>
161
+ {/* Blurring mask box */}
162
+ <motion.div
163
+ initial={{
164
+ width: dimensions.width,
165
+ height: dimensions.height,
166
+ opacity: 1,
167
+ backgroundImage: canvasBoxGradient,
168
+ }}
169
+ animate={{
170
+ opacity: 0,
171
+ display: "none",
172
+ }}
173
+ transition={blurTransition}
174
+ className="absolute left-1/2 top-1/2 z-20 origin-center -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg"
175
+ />
176
+ {/* Growing wrapper drives introProgress */}
177
+ <motion.div
178
+ initial={{
179
+ width: dimensions.width,
180
+ height: dimensions.height,
181
+ }}
182
+ animate={{
183
+ width: "100vw",
184
+ height: "100vh",
185
+ }}
186
+ transition={growTransition}
187
+ onUpdate={(latest: { width?: number; height?: number }) => {
188
+ if (completedRef.current) return;
189
+ if (typeof latest.width === "number") {
190
+ const w0 = dimensions.width;
191
+ const w1 = window.innerWidth;
192
+ const progress =
193
+ w1 === w0 ? 1 : (latest.width - w0) / (w1 - w0);
194
+ const clamped = Math.min(Math.max(progress, 0), 1);
195
+ introProgress.set(clamped);
196
+ }
197
+ }}
198
+ onAnimationComplete={() => {
199
+ if (!completedRef.current) {
200
+ completedRef.current = true;
201
+ introProgress.set(1);
202
+ setStage1NotFinished(false);
203
+ onIntroGrowComplete?.();
204
+ }
205
+ }}
206
+ className="absolute left-1/2 top-1/2 z-10 origin-center -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg shadow-[0_20px_40px_rgba(103,86,86,0.15)]"
207
+ >
208
+ <div className="h-full w-full">{children}</div>
209
+ </motion.div>
210
+ </>
211
+ )}
212
+ {stage1NotFinished && loadingText && (
213
+ <div className="absolute bottom-24 left-1/2 -translate-x-1/2 text-center font-jetbrains-mono font-semibold text-[#543C5AB2]">
214
+ {loadingText}{dots}
215
+ </div>
216
+ )}
217
+ </motion.div>
218
+ );
219
+ };
@@ -0,0 +1,116 @@
1
+ import { motion } from "framer-motion";
2
+ import useWindowDimensions from "../../hooks/useWindowDimensions";
3
+
4
+ type FolderIconProps = {
5
+ className?: string;
6
+ gradientId?: string;
7
+ strokeColor?: string;
8
+ isOpen?: boolean;
9
+ };
10
+
11
+ const AnimatedFolderIcon = ({
12
+ className = "",
13
+ gradientId = "defaultGradient",
14
+ strokeColor = "rgba(119, 103, 128, 0.1)",
15
+ isOpen = false,
16
+ }: FolderIconProps) => {
17
+ const { width } = useWindowDimensions();
18
+
19
+ return (
20
+ <svg
21
+ viewBox="0 0 208 163"
22
+ className={className}
23
+ fill="none"
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ >
26
+ <g filter="url(#folderShadow)">
27
+ {/* Closed Folder */}
28
+ <motion.g
29
+ initial={false}
30
+ animate={{
31
+ opacity: !isOpen || width < 640 ? 1 : 0,
32
+ scale: isOpen ? 1 : 1,
33
+ y: isOpen ? 5 : 0,
34
+ x: isOpen ? 15 : 15,
35
+ }}
36
+ transition={{
37
+ duration: 0.2,
38
+ ease: "easeInOut",
39
+ }}
40
+ >
41
+ <path
42
+ d="M4.34444 14.3331C4.1561 8.68286 8.68546 4 14.3389 4L76.1646 4C80.3832 4 84.4311 5.66604 87.4274 8.63558L98.3226 19.4333C101.319 22.4028 105.367 24.0689 109.585 24.0689H173.601C179.277 24.0689 183.815 28.7881 183.593 34.4601L179.876 129.391C179.666 134.758 175.255 139 169.884 139H18.1722C12.7791 139 8.35744 134.723 8.17777 129.333L4.34444 14.3331Z"
43
+ fill={`url(#${gradientId})`}
44
+ stroke={strokeColor}
45
+ strokeWidth="0.75"
46
+ fillOpacity={width < 640 ? "1" : "0.8"}
47
+ />
48
+ </motion.g>
49
+
50
+ {/* Open Folder */}
51
+ <motion.g
52
+ initial={false}
53
+ animate={{
54
+ opacity: isOpen ? 1 : 0,
55
+ scale: isOpen ? 1.15 : 1.15,
56
+ y: isOpen ? 20 : 20,
57
+ x: isOpen ? 23 : 23,
58
+ }}
59
+ transition={{
60
+ duration: 0.2,
61
+ ease: "easeInOut",
62
+ }}
63
+ >
64
+ <path
65
+ d="M3.72714 12.7562C3.28295 7.78306 7.20051 3.5 12.1934 3.5L67.2044 3.5C70.2086 3.5 73.1283 4.49474 75.5076 6.32893L89.3807 17.0235C91.76 18.8577 94.6797 19.8524 97.6839 19.8524H154.462C159.507 19.8524 163.442 24.2216 162.916 29.2393L154.874 105.887C154.42 110.214 150.772 113.5 146.421 113.5H20.5C16.0986 113.5 12.4253 110.14 12.0337 105.756L3.72714 12.7562Z"
66
+ fill={`url(#${gradientId})`}
67
+ stroke={strokeColor}
68
+ strokeOpacity="1"
69
+ strokeWidth="0.85"
70
+ fillOpacity="1"
71
+ />
72
+ </motion.g>
73
+ </g>
74
+ <defs>
75
+ <linearGradient id="red" x1="0" y1="0" x2="0" y2="1">
76
+ <stop offset="0%" stopColor="rgba(226, 141, 159, 1)" />
77
+ <stop offset="100%" stopColor="rgba(244, 193, 204, 1)" />
78
+ </linearGradient>
79
+ <linearGradient id="blue" x1="0" y1="0" x2="0" y2="1">
80
+ <stop offset="0%" stopColor="rgba(90, 149, 208, 1)" />
81
+ <stop offset="100%" stopColor="rgba(158, 202, 246, 1)" />
82
+ </linearGradient>
83
+ <linearGradient id="green" x1="0" y1="0" x2="0" y2="1">
84
+ <stop offset="0%" stopColor="rgba(127, 202, 132, 1)" />
85
+ <stop offset="100%" stopColor="rgba(185, 227, 188, 1)" />
86
+ </linearGradient>
87
+ <linearGradient id="purple" x1="0" y1="0" x2="0" y2="1">
88
+ <stop offset="0%" stopColor="rgba(143, 87, 173, 1)" />
89
+ <stop offset="100%" stopColor="rgba(209, 154, 238, 1)" />
90
+ </linearGradient>
91
+ <linearGradient id="orange" x1="0" y1="0" x2="0" y2="1">
92
+ <stop offset="0%" stopColor="rgba(233, 148, 111, 1)" />
93
+ <stop offset="100%" stopColor="rgba(246, 185, 158, 1)" />
94
+ </linearGradient>
95
+
96
+ <filter
97
+ id="folderShadow"
98
+ x="-10"
99
+ y="-10"
100
+ width="208"
101
+ height="163"
102
+ filterUnits="userSpaceOnUse"
103
+ >
104
+ <feDropShadow
105
+ dx="0"
106
+ dy="0"
107
+ stdDeviation="3"
108
+ floodColor="rgba(119, 115, 149, 0.35)"
109
+ />
110
+ </filter>
111
+ </defs>
112
+ </svg>
113
+ );
114
+ };
115
+
116
+ export default AnimatedFolderIcon;
@@ -0,0 +1,162 @@
1
+ import * as React from "react";
2
+ import Image from "next/image";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const buttonBase =
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-white font-figtree cursor-pointer";
9
+
10
+ const lift =
11
+ "-translate-y-[3px] group-hover:-translate-y-[4px] group-active:-translate-y-[1px] transition-all duration-100";
12
+
13
+ export const buttonVariants = cva(buttonBase, {
14
+ variants: {
15
+ variant: {
16
+ default: "",
17
+ primary: cn(
18
+ "bg-button-primary shadow-button-primary hover:bg-button-primary-hover active:bg-button-primary-active",
19
+ lift,
20
+ ),
21
+ secondary: cn(
22
+ "text-[#625679] bg-button-secondary hover:bg-button-secondary-hover active:bg-button-secondary-active active:shadow-button-secondary",
23
+ lift,
24
+ ),
25
+ tertiary: "bg-transparent text-[#625679] px-4 active:text-[#8F57AD]",
26
+ "tertiary-arrow":
27
+ "bg-transparent text-[#625679] px-4 active:text-[#8F57AD] flex items-center gap-2",
28
+ destructive:
29
+ "bg-destructive text-destructive-foreground hover:bg-destructive-dark",
30
+ outline: "bg-violet-100 hover:bg-muted border border-[1px] border-muted",
31
+ ghost: "hover:bg-accent hover:text-accent-foreground",
32
+ link: "text-primary underline-offset-4 hover:underline",
33
+ "apply-ghost":
34
+ "bg-[#ebdff7] bg-opacity-50 text-heavy font-semibold hover:bg-[#e6cdff] w-full justify-start",
35
+ apply: "text-medium hover:bg-[#ebdff7] hover:text-heavy",
36
+ },
37
+ size: {
38
+ default: "h-10 px-4 py-2",
39
+ sm: "h-8 rounded-md px-3",
40
+ lg: "h-11 rounded-md px-8",
41
+ icon: "h-10 w-10",
42
+ },
43
+ },
44
+ defaultVariants: { variant: "default", size: "default" },
45
+ });
46
+
47
+ export interface ButtonProps
48
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
49
+ VariantProps<typeof buttonVariants> {
50
+ asChild?: boolean;
51
+ isPending?: boolean;
52
+ full?: boolean;
53
+ secondClass?: string;
54
+ }
55
+
56
+ const overlays = {
57
+ primary: { bg: "bg-button-primary-back", border: "border border-white/30" },
58
+ secondary: {
59
+ bg: "bg-button-secondary-back",
60
+ border: "border border-white/50",
61
+ },
62
+ } as const;
63
+
64
+ const pressedByVariant: Record<"primary" | "secondary", string> = {
65
+ primary:
66
+ "!-translate-y-[1px] shadow-none bg-button-primary-active hover:!bg-button-primary-active",
67
+ secondary:
68
+ "!-translate-y-[1px] shadow-button-secondary bg-button-secondary-active hover:!bg-button-secondary-active",
69
+ };
70
+
71
+ const noLift =
72
+ "group-hover:!translate-y-[1px] group-active:!translate-y-[1px] transition-none";
73
+
74
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
75
+ (
76
+ {
77
+ className,
78
+ variant = "default",
79
+ size,
80
+ isPending = false,
81
+ full = false,
82
+ asChild = false,
83
+ disabled,
84
+ secondClass = "",
85
+ children,
86
+ ...props
87
+ },
88
+ ref,
89
+ ) => {
90
+ const Comp = asChild ? Slot : "button";
91
+ const lockPressed =
92
+ isPending && (variant === "primary" || variant === "secondary");
93
+
94
+ const btnClasses = cn(
95
+ buttonVariants({ variant, size, className }),
96
+ lockPressed && [pressedByVariant[variant], noLift],
97
+ full && "w-full",
98
+ );
99
+
100
+ const overlay = overlays[variant as keyof typeof overlays];
101
+
102
+ const wrapperClasses = cn(
103
+ "group relative inline-block w-max",
104
+ full && "block w-full",
105
+ lockPressed && ["pointer-events-none", noLift],
106
+ );
107
+
108
+ return (
109
+ <div className={wrapperClasses}>
110
+ {overlay && (
111
+ <>
112
+ <span
113
+ className={cn(
114
+ "pointer-events-none absolute inset-0 h-full w-full rounded-lg",
115
+ overlay.bg,
116
+ )}
117
+ />
118
+ <span
119
+ className={cn(
120
+ "pointer-events-none absolute inset-0 z-50 h-full w-full rounded-lg",
121
+ overlay.border,
122
+ lockPressed ? "-translate-y-[1px] " + noLift : lift,
123
+ )}
124
+ />
125
+ </>
126
+ )}
127
+
128
+ <Comp
129
+ ref={ref}
130
+ {...props}
131
+ className={cn("flex flex-row gap-2", btnClasses)}
132
+ disabled={disabled ?? isPending}
133
+ >
134
+ {variant === "tertiary-arrow" ? (
135
+ <div className="w-full">
136
+ <Image
137
+ src="/arrow-left.svg"
138
+ alt="Left Arrow"
139
+ width={10}
140
+ height={10}
141
+ />
142
+ {children}
143
+ </div>
144
+ ) : (
145
+ children
146
+ )}
147
+ </Comp>
148
+
149
+ {(variant === "tertiary" || variant === "tertiary-arrow") && (
150
+ <span
151
+ className={cn(
152
+ "block h-0 max-w-0 border-b-2 border-dashed border-[#625679] transition-all duration-200 group-hover:max-w-full group-active:border-[#8F57AD]",
153
+ secondClass,
154
+ )}
155
+ />
156
+ )}
157
+ </div>
158
+ );
159
+ },
160
+ );
161
+
162
+ Button.displayName = "Button";
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const labelVariants = cva(
8
+ "text-[#5f476c] text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
9
+ );
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14
+ VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root
17
+ ref={ref}
18
+ className={cn(labelVariants(), className)}
19
+ {...props}
20
+ />
21
+ ));
22
+ Label.displayName = LabelPrimitive.Root.displayName;
23
+
24
+ export { Label };
@@ -0,0 +1,136 @@
1
+ import * as React from "react";
2
+ import * as ToastPrimitives from "@radix-ui/react-toast";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { X } from "lucide-react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ const ToastProvider = ToastPrimitives.Provider;
9
+
10
+ const ToastViewport = React.forwardRef<
11
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
12
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
13
+ >(({ className, ...props }, ref) => (
14
+ <ToastPrimitives.Viewport
15
+ ref={ref}
16
+ className={cn(
17
+ "fixed left-1/2 top-4 z-[100] flex max-h-screen w-auto -translate-x-1/2 flex-col p-4 sm:max-w-fit",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24
+
25
+ const toastVariants = cva(
26
+ "group pointer-events-auto relative flex w-auto min-w-fit items-center justify-between space-x-4 overflow-hidden rounded-md border px-4 py-3 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-top-full data-[state=open]:slide-in-from-top-full",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "border bg-background text-foreground",
31
+ destructive:
32
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
33
+ success: "border-green-200 bg-green-50 text-green-900 shadow-lg",
34
+ cute: cn(
35
+ "border-purple-200 bg-gradient-to-r from-purple-50 to-pink-50 text-purple-900 shadow-lg rounded-lg", // Base styling
36
+ "w-[calc(100vw-4rem)]", // Mobile: take (100vw - 4rem) width for near full-bleed with margin
37
+ "mx-auto", // Center it (viewport already centers, but keep safety)
38
+ "min-w-0", // Prevent it from shrinking too small if content wraps
39
+ "sm:w-auto", // On larger screens revert to intrinsic sizing
40
+ "justify-center text-center space-x-0", // Center content & text horizontally
41
+ ),
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ variant: "default",
46
+ },
47
+ },
48
+ );
49
+
50
+ const Toast = React.forwardRef<
51
+ React.ElementRef<typeof ToastPrimitives.Root>,
52
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
53
+ VariantProps<typeof toastVariants>
54
+ >(({ className, variant, ...props }, ref) => {
55
+ return (
56
+ <ToastPrimitives.Root
57
+ ref={ref}
58
+ className={cn(toastVariants({ variant }), className)}
59
+ {...props}
60
+ />
61
+ );
62
+ });
63
+ Toast.displayName = ToastPrimitives.Root.displayName;
64
+
65
+ const ToastAction = React.forwardRef<
66
+ React.ElementRef<typeof ToastPrimitives.Action>,
67
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
68
+ >(({ className, ...props }, ref) => (
69
+ <ToastPrimitives.Action
70
+ ref={ref}
71
+ className={cn(
72
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ ));
78
+ ToastAction.displayName = ToastPrimitives.Action.displayName;
79
+
80
+ const ToastClose = React.forwardRef<
81
+ React.ElementRef<typeof ToastPrimitives.Close>,
82
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
83
+ >(({ className, ...props }, ref) => (
84
+ <ToastPrimitives.Close
85
+ ref={ref}
86
+ className={cn(
87
+ "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
88
+ className,
89
+ )}
90
+ toast-close=""
91
+ {...props}
92
+ >
93
+ <X className="h-3 w-3" />
94
+ </ToastPrimitives.Close>
95
+ ));
96
+ ToastClose.displayName = ToastPrimitives.Close.displayName;
97
+
98
+ const ToastTitle = React.forwardRef<
99
+ React.ElementRef<typeof ToastPrimitives.Title>,
100
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
101
+ >(({ className, ...props }, ref) => (
102
+ <ToastPrimitives.Title
103
+ ref={ref}
104
+ className={cn("text-sm font-semibold", className)}
105
+ {...props}
106
+ />
107
+ ));
108
+ ToastTitle.displayName = ToastPrimitives.Title.displayName;
109
+
110
+ const ToastDescription = React.forwardRef<
111
+ React.ElementRef<typeof ToastPrimitives.Description>,
112
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
113
+ >(({ className, ...props }, ref) => (
114
+ <ToastPrimitives.Description
115
+ ref={ref}
116
+ className={cn("text-sm opacity-90", className)}
117
+ {...props}
118
+ />
119
+ ));
120
+ ToastDescription.displayName = ToastPrimitives.Description.displayName;
121
+
122
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
123
+
124
+ type ToastActionElement = React.ReactElement<typeof ToastAction>;
125
+
126
+ export {
127
+ type ToastProps,
128
+ type ToastActionElement,
129
+ ToastProvider,
130
+ ToastViewport,
131
+ Toast,
132
+ ToastTitle,
133
+ ToastDescription,
134
+ ToastClose,
135
+ ToastAction,
136
+ };
@@ -0,0 +1,33 @@
1
+ import { useToast } from "../../hooks/use-toast";
2
+ import {
3
+ Toast,
4
+ ToastClose,
5
+ ToastDescription,
6
+ ToastProvider,
7
+ ToastTitle,
8
+ ToastViewport,
9
+ } from "./toast";
10
+
11
+ export function Toaster() {
12
+ const { toasts } = useToast();
13
+
14
+ return (
15
+ <ToastProvider>
16
+ {toasts.map(function ({ id, title, description, action, ...props }) {
17
+ return (
18
+ <Toast key={id} {...props}>
19
+ <div className="grid gap-1">
20
+ {title && <ToastTitle>{title}</ToastTitle>}
21
+ {description && (
22
+ <ToastDescription>{description}</ToastDescription>
23
+ )}
24
+ </div>
25
+ {action}
26
+ <ToastClose />
27
+ </Toast>
28
+ );
29
+ })}
30
+ <ToastViewport />
31
+ </ToastProvider>
32
+ );
33
+ }