@m5kdev/web-ui 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 (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,120 @@
1
+ import { File, Upload, X } from "lucide-react";
2
+ import { useCallback, useState } from "react";
3
+ import { useDropzone } from "react-dropzone";
4
+ import { Button } from "#components/ui/button";
5
+ import { Progress } from "#components/ui/progress";
6
+ import { cn } from "#utils";
7
+
8
+ interface FileDropzoneProps {
9
+ onUploadComplete?: (filePath: string) => void;
10
+ className?: string;
11
+ }
12
+
13
+ export function FileDropzone({ onUploadComplete, className }: FileDropzoneProps) {
14
+ const [uploadProgress, setUploadProgress] = useState(0);
15
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
16
+ const [isUploading, setIsUploading] = useState(false);
17
+
18
+ const onDrop = useCallback((acceptedFiles: File[]) => {
19
+ if (acceptedFiles.length > 0) {
20
+ setSelectedFile(acceptedFiles[0]);
21
+ }
22
+ }, []);
23
+
24
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
25
+ onDrop,
26
+ accept: {
27
+ "image/jpeg": [],
28
+ "image/png": [],
29
+ "image/webp": [],
30
+ "image/jpg": [],
31
+ },
32
+ maxFiles: 1,
33
+ });
34
+
35
+ const uploadFile = async () => {
36
+ if (!selectedFile) return;
37
+
38
+ setIsUploading(true);
39
+ setUploadProgress(0);
40
+
41
+ const formData = new FormData();
42
+ formData.append("file", selectedFile);
43
+
44
+ try {
45
+ const response = await fetch("/api/upload/image", {
46
+ method: "POST",
47
+ body: formData,
48
+ // Note: Content-Type is automatically set for FormData
49
+ });
50
+
51
+ if (!response.ok) {
52
+ throw new Error(`HTTP error! status: ${response.status}`);
53
+ }
54
+
55
+ const data = await response.json();
56
+ onUploadComplete?.(data.filePath);
57
+ setSelectedFile(null);
58
+ setUploadProgress(0);
59
+ } catch (error) {
60
+ console.error("Upload error:", error);
61
+ } finally {
62
+ setIsUploading(false);
63
+ }
64
+ };
65
+
66
+ const removeFile = () => {
67
+ setSelectedFile(null);
68
+ setUploadProgress(0);
69
+ };
70
+
71
+ return (
72
+ <div className={cn("w-full max-w-xl", className)}>
73
+ <div
74
+ {...getRootProps()}
75
+ className={cn(
76
+ "border-2 border-dashed rounded-lg p-6 cursor-pointer transition-colors",
77
+ isDragActive ? "border-primary bg-primary/5" : "border-gray-300 hover:border-primary",
78
+ selectedFile && "border-primary"
79
+ )}
80
+ >
81
+ <input {...getInputProps()} />
82
+ <div className="flex flex-col items-center justify-center space-y-2 text-center">
83
+ {selectedFile ? (
84
+ <>
85
+ <File className="h-8 w-8 text-primary" />
86
+ <div className="flex items-center gap-2">
87
+ <span className="text-sm text-gray-600">{selectedFile.name}</span>
88
+ <button
89
+ type="button"
90
+ onClick={(e) => {
91
+ e.stopPropagation();
92
+ removeFile();
93
+ }}
94
+ className="p-1 hover:bg-gray-100 rounded-full"
95
+ >
96
+ <X className="h-4 w-4 text-gray-500" />
97
+ </button>
98
+ </div>
99
+ </>
100
+ ) : (
101
+ <>
102
+ <Upload className="h-8 w-8 text-gray-400" />
103
+ <p className="text-sm text-gray-600">Drag & drop an image here, or click to select</p>
104
+ <p className="text-xs text-gray-500">Supports JPG, PNG, and WebP</p>
105
+ </>
106
+ )}
107
+ </div>
108
+ </div>
109
+
110
+ {selectedFile && (
111
+ <div className="mt-4 space-y-4">
112
+ {isUploading && <Progress value={uploadProgress} className="h-2 w-full" />}
113
+ <Button onClick={uploadFile} disabled={isUploading} className="w-full">
114
+ {isUploading ? "Uploading..." : "Upload File"}
115
+ </Button>
116
+ </div>
117
+ )}
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,233 @@
1
+ import { ChevronDown, RotateCcw } from "lucide-react";
2
+ import * as React from "react";
3
+
4
+ import { Button } from "#components/ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuCheckboxItem,
8
+ DropdownMenuContent,
9
+ DropdownMenuGroup,
10
+ DropdownMenuItem,
11
+ DropdownMenuLabel,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from "#components/ui/dropdown-menu";
15
+ import { cn } from "#utils";
16
+
17
+ interface MultiSelectOption {
18
+ label: string;
19
+ icon?: React.ReactNode;
20
+ value: string;
21
+ group?: string;
22
+ }
23
+
24
+ interface MultiSelectDropdownProps {
25
+ options: MultiSelectOption[];
26
+ selectedValues: string[];
27
+ onValueChange: (selectedValues: string[]) => void;
28
+ placeholder?: React.ReactNode;
29
+ label?: string;
30
+ className?: string;
31
+ triggerClassName?: string;
32
+ disabled?: boolean;
33
+ maxDisplayCount?: number;
34
+ multiSelectLabel?: string;
35
+ trigger?: React.ReactNode;
36
+ resetButton?: "bottom" | "top";
37
+ resetLabel?: React.ReactNode;
38
+ separateGroups?: boolean;
39
+ }
40
+
41
+ const MultiSelectDropdown = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuTrigger>,
43
+ MultiSelectDropdownProps
44
+ >(
45
+ (
46
+ {
47
+ options,
48
+ selectedValues,
49
+ onValueChange,
50
+ placeholder = "Select items...",
51
+ label,
52
+ className,
53
+ triggerClassName,
54
+ trigger,
55
+ multiSelectLabel,
56
+ disabled = false,
57
+ maxDisplayCount = 3,
58
+ resetButton = "bottom",
59
+ resetLabel = "Reset",
60
+ separateGroups = false,
61
+ ...props
62
+ },
63
+ ref
64
+ ) => {
65
+ const handleValueChange = (value: string, checked: boolean) => {
66
+ if (checked) {
67
+ onValueChange([...selectedValues, value]);
68
+ } else {
69
+ onValueChange(selectedValues.filter((v) => v !== value));
70
+ }
71
+ };
72
+
73
+ const handleReset = () => {
74
+ onValueChange([]);
75
+ };
76
+
77
+ const getDisplayText = () => {
78
+ if (selectedValues.length === 0) {
79
+ return placeholder;
80
+ }
81
+
82
+ const selectedLabels = options
83
+ .filter((option) => selectedValues.includes(option.value))
84
+ .map((option) => option.label);
85
+
86
+ if (multiSelectLabel && selectedLabels.length > 1) {
87
+ return multiSelectLabel;
88
+ }
89
+
90
+ if (selectedLabels.length <= maxDisplayCount) {
91
+ return selectedLabels.join(", ");
92
+ }
93
+
94
+ return `${selectedLabels.slice(0, maxDisplayCount).join(", ")} (+${
95
+ selectedLabels.length - maxDisplayCount
96
+ } more)`;
97
+ };
98
+
99
+ const groupedOptions = React.useMemo(() => {
100
+ const groups: Record<string, MultiSelectOption[]> = {};
101
+ const ungrouped: MultiSelectOption[] = [];
102
+
103
+ options.forEach((option) => {
104
+ if (option.group) {
105
+ if (!groups[option.group]) {
106
+ groups[option.group] = [];
107
+ }
108
+ groups[option.group].push(option);
109
+ } else {
110
+ ungrouped.push(option);
111
+ }
112
+ });
113
+
114
+ return { groups, ungrouped };
115
+ }, [options]);
116
+
117
+ return (
118
+ <div className={cn("w-full", className)}>
119
+ <DropdownMenu>
120
+ <DropdownMenuTrigger asChild>
121
+ <Button
122
+ ref={ref}
123
+ variant="outline"
124
+ className={cn(
125
+ "w-full justify-between text-left font-normal",
126
+ selectedValues.length === 0 && "text-muted-foreground",
127
+ triggerClassName
128
+ )}
129
+ disabled={disabled}
130
+ {...props}
131
+ >
132
+ {trigger || (
133
+ <>
134
+ <span className="truncate">{getDisplayText()}</span>
135
+ <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
136
+ </>
137
+ )}
138
+ </Button>
139
+ </DropdownMenuTrigger>
140
+
141
+ <DropdownMenuContent
142
+ className="w-full min-w-[var(--radix-dropdown-menu-trigger-width)]"
143
+ align="start"
144
+ >
145
+ {resetButton && resetButton === "top" && (
146
+ <>
147
+ <DropdownMenuItem
148
+ onClick={handleReset}
149
+ disabled={selectedValues.length === 0}
150
+ onSelect={(event) => event.preventDefault()}
151
+ className="cursor-pointer text-muted-foreground hover:text-foreground"
152
+ >
153
+ {resetLabel || (
154
+ <>
155
+ <RotateCcw className="mr-2 h-4 w-4" />
156
+ Reset
157
+ </>
158
+ )}
159
+ </DropdownMenuItem>
160
+ <DropdownMenuSeparator />
161
+ </>
162
+ )}
163
+ {label && (
164
+ <>
165
+ <DropdownMenuLabel>{label}</DropdownMenuLabel>
166
+ <DropdownMenuSeparator />
167
+ </>
168
+ )}
169
+ {Object.entries(groupedOptions.groups).map(([groupName, groupOptions], groupIndex) => (
170
+ <DropdownMenuGroup key={groupName}>
171
+ {groupIndex > 0 && separateGroups && <DropdownMenuSeparator />}
172
+ <DropdownMenuLabel className="text-xs font-semibold text-muted-foreground px-2 py-1.5">
173
+ {groupName}
174
+ </DropdownMenuLabel>
175
+ {groupOptions.map((option) => (
176
+ <DropdownMenuCheckboxItem
177
+ key={option.value}
178
+ checked={selectedValues.includes(option.value)}
179
+ onCheckedChange={(checked) => handleValueChange(option.value, checked)}
180
+ onSelect={(event) => event.preventDefault()}
181
+ className="cursor-pointer"
182
+ >
183
+ {option.icon && <span className="mr-2">{option.icon}</span>}
184
+ {option.label}
185
+ </DropdownMenuCheckboxItem>
186
+ ))}
187
+ </DropdownMenuGroup>
188
+ ))}
189
+ {groupedOptions.ungrouped.length > 0 && (
190
+ <DropdownMenuGroup>
191
+ {Object.keys(groupedOptions.groups).length > 0 && <DropdownMenuSeparator />}
192
+ {groupedOptions.ungrouped.map((option) => (
193
+ <DropdownMenuCheckboxItem
194
+ key={option.value}
195
+ checked={selectedValues.includes(option.value)}
196
+ onCheckedChange={(checked) => handleValueChange(option.value, checked)}
197
+ onSelect={(event) => event.preventDefault()}
198
+ className="cursor-pointer"
199
+ >
200
+ {option.label}
201
+ </DropdownMenuCheckboxItem>
202
+ ))}
203
+ </DropdownMenuGroup>
204
+ )}
205
+ {resetButton && resetButton === "bottom" && (
206
+ <>
207
+ <DropdownMenuSeparator />
208
+ <DropdownMenuItem
209
+ onClick={handleReset}
210
+ disabled={selectedValues.length === 0}
211
+ onSelect={(event) => event.preventDefault()}
212
+ className="cursor-pointer text-muted-foreground hover:text-foreground"
213
+ >
214
+ {resetLabel || (
215
+ <>
216
+ <RotateCcw className="mr-2 h-4 w-4" />
217
+ Reset
218
+ </>
219
+ )}
220
+ </DropdownMenuItem>
221
+ </>
222
+ )}
223
+ </DropdownMenuContent>
224
+ </DropdownMenu>
225
+ </div>
226
+ );
227
+ }
228
+ );
229
+
230
+ MultiSelectDropdown.displayName = "MultiSelectDropdown";
231
+
232
+ export { MultiSelectDropdown };
233
+ export type { MultiSelectOption, MultiSelectDropdownProps };
@@ -0,0 +1,288 @@
1
+ import { Mesh, Program, Renderer, Triangle, Vec3 } from "ogl";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ export default function Orb({
5
+ hue = 0,
6
+ hoverIntensity = 0.2,
7
+ rotateOnHover = true,
8
+ forceHoverState = false,
9
+ }) {
10
+ const ctnDom = useRef(null);
11
+
12
+ const vert = /* glsl */ `
13
+ precision highp float;
14
+ attribute vec2 position;
15
+ attribute vec2 uv;
16
+ varying vec2 vUv;
17
+ void main() {
18
+ vUv = uv;
19
+ gl_Position = vec4(position, 0.0, 1.0);
20
+ }
21
+ `;
22
+
23
+ const frag = /* glsl */ `
24
+ precision highp float;
25
+
26
+ uniform float iTime;
27
+ uniform vec3 iResolution;
28
+ uniform float hue;
29
+ uniform float hover;
30
+ uniform float rot;
31
+ uniform float hoverIntensity;
32
+ varying vec2 vUv;
33
+
34
+ vec3 rgb2yiq(vec3 c) {
35
+ float y = dot(c, vec3(0.299, 0.587, 0.114));
36
+ float i = dot(c, vec3(0.596, -0.274, -0.322));
37
+ float q = dot(c, vec3(0.211, -0.523, 0.312));
38
+ return vec3(y, i, q);
39
+ }
40
+
41
+ vec3 yiq2rgb(vec3 c) {
42
+ float r = c.x + 0.956 * c.y + 0.621 * c.z;
43
+ float g = c.x - 0.272 * c.y - 0.647 * c.z;
44
+ float b = c.x - 1.106 * c.y + 1.703 * c.z;
45
+ return vec3(r, g, b);
46
+ }
47
+
48
+ vec3 adjustHue(vec3 color, float hueDeg) {
49
+ float hueRad = hueDeg * 3.14159265 / 180.0;
50
+ vec3 yiq = rgb2yiq(color);
51
+ float cosA = cos(hueRad);
52
+ float sinA = sin(hueRad);
53
+ float i = yiq.y * cosA - yiq.z * sinA;
54
+ float q = yiq.y * sinA + yiq.z * cosA;
55
+ yiq.y = i;
56
+ yiq.z = q;
57
+ return yiq2rgb(yiq);
58
+ }
59
+
60
+ vec3 hash33(vec3 p3) {
61
+ p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
62
+ p3 += dot(p3, p3.yxz + 19.19);
63
+ return -1.0 + 2.0 * fract(vec3(
64
+ p3.x + p3.y,
65
+ p3.x + p3.z,
66
+ p3.y + p3.z
67
+ ) * p3.zyx);
68
+ }
69
+
70
+ float snoise3(vec3 p) {
71
+ const float K1 = 0.333333333;
72
+ const float K2 = 0.166666667;
73
+ vec3 i = floor(p + (p.x + p.y + p.z) * K1);
74
+ vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
75
+ vec3 e = step(vec3(0.0), d0 - d0.yzx);
76
+ vec3 i1 = e * (1.0 - e.zxy);
77
+ vec3 i2 = 1.0 - e.zxy * (1.0 - e);
78
+ vec3 d1 = d0 - (i1 - K2);
79
+ vec3 d2 = d0 - (i2 - K1);
80
+ vec3 d3 = d0 - 0.5;
81
+ vec4 h = max(0.6 - vec4(
82
+ dot(d0, d0),
83
+ dot(d1, d1),
84
+ dot(d2, d2),
85
+ dot(d3, d3)
86
+ ), 0.0);
87
+ vec4 n = h * h * h * h * vec4(
88
+ dot(d0, hash33(i)),
89
+ dot(d1, hash33(i + i1)),
90
+ dot(d2, hash33(i + i2)),
91
+ dot(d3, hash33(i + 1.0))
92
+ );
93
+ return dot(vec4(31.316), n);
94
+ }
95
+
96
+ vec4 extractAlpha(vec3 colorIn) {
97
+ float a = max(max(colorIn.r, colorIn.g), colorIn.b);
98
+ return vec4(colorIn.rgb / (a + 1e-5), a);
99
+ }
100
+
101
+ const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
102
+ const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
103
+ const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
104
+ const float innerRadius = 0.6;
105
+ const float noiseScale = 0.65;
106
+
107
+ float light1(float intensity, float attenuation, float dist) {
108
+ return intensity / (1.0 + dist * attenuation);
109
+ }
110
+ float light2(float intensity, float attenuation, float dist) {
111
+ return intensity / (1.0 + dist * dist * attenuation);
112
+ }
113
+
114
+ vec4 draw(vec2 uv) {
115
+ vec3 color1 = adjustHue(baseColor1, hue);
116
+ vec3 color2 = adjustHue(baseColor2, hue);
117
+ vec3 color3 = adjustHue(baseColor3, hue);
118
+
119
+ float ang = atan(uv.y, uv.x);
120
+ float len = length(uv);
121
+ float invLen = len > 0.0 ? 1.0 / len : 0.0;
122
+
123
+ float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
124
+ float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
125
+ float d0 = distance(uv, (r0 * invLen) * uv);
126
+ float v0 = light1(1.0, 10.0, d0);
127
+ v0 *= smoothstep(r0 * 1.05, r0, len);
128
+ float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
129
+
130
+ float a = iTime * -1.0;
131
+ vec2 pos = vec2(cos(a), sin(a)) * r0;
132
+ float d = distance(uv, pos);
133
+ float v1 = light2(1.5, 5.0, d);
134
+ v1 *= light1(1.0, 50.0, d0);
135
+
136
+ float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
137
+ float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
138
+
139
+ vec3 col = mix(color1, color2, cl);
140
+ col = mix(color3, col, v0);
141
+ col = (col + v1) * v2 * v3;
142
+ col = clamp(col, 0.0, 1.0);
143
+
144
+ return extractAlpha(col);
145
+ }
146
+
147
+ vec4 mainImage(vec2 fragCoord) {
148
+ vec2 center = iResolution.xy * 0.5;
149
+ float size = min(iResolution.x, iResolution.y);
150
+ vec2 uv = (fragCoord - center) / size * 2.0;
151
+
152
+ float angle = rot;
153
+ float s = sin(angle);
154
+ float c = cos(angle);
155
+ uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
156
+
157
+ uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
158
+ uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
159
+
160
+ return draw(uv);
161
+ }
162
+
163
+ void main() {
164
+ vec2 fragCoord = vUv * iResolution.xy;
165
+ vec4 col = mainImage(fragCoord);
166
+ gl_FragColor = vec4(col.rgb * col.a, col.a);
167
+ }
168
+ `;
169
+
170
+ useEffect(() => {
171
+ const container = ctnDom.current as HTMLElement | null;
172
+ if (!container) return;
173
+
174
+ const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
175
+ const gl = renderer.gl;
176
+ gl.clearColor(0, 0, 0, 0);
177
+ container.appendChild(gl.canvas);
178
+
179
+ const geometry = new Triangle(gl);
180
+ const program = new Program(gl, {
181
+ vertex: vert,
182
+ fragment: frag,
183
+ uniforms: {
184
+ iTime: { value: 0 },
185
+ iResolution: {
186
+ value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height),
187
+ },
188
+ hue: { value: hue },
189
+ hover: { value: 0 },
190
+ rot: { value: 0 },
191
+ hoverIntensity: { value: hoverIntensity },
192
+ },
193
+ });
194
+
195
+ const mesh = new Mesh(gl, { geometry, program });
196
+
197
+ function resize() {
198
+ if (!container) return;
199
+ const dpr = window.devicePixelRatio || 1;
200
+ const width = container.clientWidth;
201
+ const height = container.clientHeight;
202
+ renderer.setSize(width * dpr, height * dpr);
203
+ gl.canvas.style.width = width + "px";
204
+ gl.canvas.style.height = height + "px";
205
+ program.uniforms.iResolution.value.set(
206
+ gl.canvas.width,
207
+ gl.canvas.height,
208
+ gl.canvas.width / gl.canvas.height
209
+ );
210
+ }
211
+ window.addEventListener("resize", resize);
212
+ resize();
213
+
214
+ let targetHover = 0;
215
+ let lastTime = 0;
216
+ let currentRot = 0;
217
+ const rotationSpeed = 0.3;
218
+
219
+ const handleMouseMove = (e: MouseEvent) => {
220
+ const rect = container.getBoundingClientRect();
221
+ const x = e.clientX - rect.left;
222
+ const y = e.clientY - rect.top;
223
+ const width = rect.width;
224
+ const height = rect.height;
225
+ const size = Math.min(width, height);
226
+ const centerX = width / 2;
227
+ const centerY = height / 2;
228
+ const uvX = ((x - centerX) / size) * 2.0;
229
+ const uvY = ((y - centerY) / size) * 2.0;
230
+
231
+ if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
232
+ targetHover = 1;
233
+ } else {
234
+ targetHover = 0;
235
+ }
236
+ };
237
+
238
+ const handleMouseLeave = () => {
239
+ targetHover = 0;
240
+ };
241
+
242
+ container.addEventListener("mousemove", handleMouseMove);
243
+ container.addEventListener("mouseleave", handleMouseLeave);
244
+
245
+ let rafId: number;
246
+ const update = (t: number) => {
247
+ rafId = requestAnimationFrame(update);
248
+ const dt = (t - lastTime) * 0.001;
249
+ lastTime = t;
250
+ program.uniforms.iTime.value = t * 0.001;
251
+ program.uniforms.hue.value = hue;
252
+ program.uniforms.hoverIntensity.value = hoverIntensity;
253
+
254
+ const effectiveHover = forceHoverState ? 1 : targetHover;
255
+ program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
256
+
257
+ if (rotateOnHover && effectiveHover > 0.5) {
258
+ currentRot += dt * rotationSpeed;
259
+ }
260
+ program.uniforms.rot.value = currentRot;
261
+
262
+ renderer.render({ scene: mesh });
263
+ };
264
+ rafId = requestAnimationFrame(update);
265
+
266
+ return () => {
267
+ cancelAnimationFrame(rafId);
268
+ window.removeEventListener("resize", resize);
269
+ container.removeEventListener("mousemove", handleMouseMove);
270
+ container.removeEventListener("mouseleave", handleMouseLeave);
271
+ container.removeChild(gl.canvas);
272
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
273
+ };
274
+ // eslint-disable-next-line react-hooks/exhaustive-deps
275
+ }, [hue, hoverIntensity, rotateOnHover, forceHoverState]);
276
+
277
+ return (
278
+ <div
279
+ ref={ctnDom}
280
+ style={{
281
+ position: "relative",
282
+ zIndex: 0,
283
+ width: "100%",
284
+ height: "100%",
285
+ }}
286
+ />
287
+ );
288
+ }