@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.
- package/LICENSE +621 -0
- package/README.md +17 -0
- package/package.json +169 -0
- package/src/animations/card.motion.ts +9 -0
- package/src/components/AvatarUpload.tsx +133 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/Calendar.css +684 -0
- package/src/components/Calendar.tsx +32 -0
- package/src/components/CardsSelect.tsx +155 -0
- package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
- package/src/components/ColorPicker.tsx +56 -0
- package/src/components/CopyButton.tsx +45 -0
- package/src/components/CropDialog.tsx +154 -0
- package/src/components/DialogProvider.tsx +105 -0
- package/src/components/ErrorFallback.tsx +17 -0
- package/src/components/FileDropzone.tsx +120 -0
- package/src/components/MultiSelectDropdown.tsx +233 -0
- package/src/components/Orb.tsx +288 -0
- package/src/components/PageAlert.tsx +121 -0
- package/src/components/SelectChips.tsx +40 -0
- package/src/components/SidebarItem.tsx +26 -0
- package/src/components/Steps.tsx +340 -0
- package/src/components/TablerIconPicker.tsx +4260 -0
- package/src/components/app-header.tsx +40 -0
- package/src/components/blur-card.tsx +132 -0
- package/src/components/features-section-demo-1.tsx +127 -0
- package/src/components/features-section-demo-2.tsx +102 -0
- package/src/components/features-section-demo-3.tsx +272 -0
- package/src/components/mode-toggle.tsx +31 -0
- package/src/components/nav-main.tsx +69 -0
- package/src/components/pricing-cards.tsx +133 -0
- package/src/components/shared/ButtonCopy.tsx +50 -0
- package/src/components/team-switcher.tsx +83 -0
- package/src/components/theme-provider.tsx +74 -0
- package/src/components/typewriter.tsx +90 -0
- package/src/components/ui/alert-dialog.tsx +133 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/bento-grid.tsx +54 -0
- package/src/components/ui/bento-grid2.tsx +66 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +186 -0
- package/src/components/ui/floating-navbar.tsx +78 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/image.tsx +55 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/pagination.tsx +105 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/resizable-navbar.tsx +260 -0
- package/src/components/ui/segment-control.tsx +143 -0
- package/src/components/ui/select.tsx +153 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +121 -0
- package/src/components/ui/sidebar.tsx +736 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +45 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +90 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +95 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/ui/typewriter-effect.tsx +181 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/useDialog.ts +25 -0
- package/src/icons/GoogleIcon.tsx +32 -0
- package/src/icons/LinkedInIcon.tsx +30 -0
- package/src/icons/MicrosoftIcon.tsx +21 -0
- package/src/lib/chatwoot.ts +51 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/app/components/AppLoader.tsx +9 -0
- package/src/modules/app/components/AppShell.tsx +21 -0
- package/src/modules/app/components/AppSidebar.tsx +26 -0
- package/src/modules/app/components/AppSidebarContent.tsx +73 -0
- package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
- package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
- package/src/modules/app/components/AppSidebarUser.tsx +128 -0
- package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
- package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
- package/src/modules/auth/components/AuthLayout.tsx +13 -0
- package/src/modules/auth/components/AuthProviders.tsx +105 -0
- package/src/modules/auth/components/AuthRouter.tsx +29 -0
- package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
- package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
- package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
- package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/InviteFriends.tsx +273 -0
- package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
- package/src/modules/auth/components/LoginForm.tsx +104 -0
- package/src/modules/auth/components/LoginRoute.tsx +31 -0
- package/src/modules/auth/components/LogoutRoute.tsx +21 -0
- package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
- package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
- package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
- package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
- package/src/modules/auth/components/ProfileRoute.tsx +104 -0
- package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
- package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
- package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
- package/src/modules/auth/components/SignupRoute.tsx +53 -0
- package/src/modules/auth/components/UserPreferences.tsx +144 -0
- package/src/modules/auth/components/WaitlistCard.tsx +78 -0
- package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
- package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
- package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
- package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
- package/src/modules/billing/components/BillingRouter.tsx +20 -0
- package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
- package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
- package/src/modules/table/components/NuqsTable.tsx +396 -0
- package/src/modules/table/components/TableFiltering.tsx +520 -0
- package/src/modules/table/components/TablePagination.tsx +59 -0
- package/src/modules/table/components/table.types.ts +11 -0
- package/src/modules/table/filterTransformers.ts +323 -0
- package/src/types.ts +4 -0
- 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
|
+
}
|