@srcroot/ui 0.0.52 → 0.0.54
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/dist/index.js +9 -3
- package/package.json +1 -1
- package/src/registry/analytics/google-analytics.tsx +3 -0
- package/src/registry/analytics/google-tag-manager.tsx +3 -0
- package/src/registry/analytics/meta-pixel.tsx +4 -1
- package/src/registry/analytics/microsoft-clarity.tsx +3 -0
- package/src/registry/analytics/tiktok-pixel.tsx +4 -1
- package/src/registry/ui/accordion.tsx +3 -0
- package/src/registry/ui/alert-dialog.tsx +3 -0
- package/src/registry/ui/alert.tsx +3 -0
- package/src/registry/ui/aspect-ratio.tsx +3 -0
- package/src/registry/ui/avatar.tsx +3 -0
- package/src/registry/ui/badge.tsx +3 -0
- package/src/registry/ui/breadcrumb.tsx +3 -0
- package/src/registry/ui/button-group.tsx +3 -0
- package/src/registry/ui/button.tsx +3 -0
- package/src/registry/ui/calendar.tsx +3 -0
- package/src/registry/ui/card.tsx +3 -0
- package/src/registry/ui/chatbot.tsx +3 -0
- package/src/registry/ui/checkbox.tsx +3 -0
- package/src/registry/ui/collapsible.tsx +3 -0
- package/src/registry/ui/container.tsx +3 -0
- package/src/registry/ui/dialog.tsx +3 -0
- package/src/registry/ui/dropdown-menu.tsx +3 -0
- package/src/registry/ui/empty-state.tsx +40 -0
- package/src/registry/ui/file-upload.tsx +65 -24
- package/src/registry/ui/floating-dock.tsx +22 -0
- package/src/registry/ui/form-field.tsx +3 -0
- package/src/registry/ui/image.tsx +3 -0
- package/src/registry/ui/input-group.tsx +3 -0
- package/src/registry/ui/kbd.tsx +4 -1
- package/src/registry/ui/label.tsx +3 -0
- package/src/registry/ui/loading-spinner.tsx +3 -0
- package/src/registry/ui/map.tsx +99 -0
- package/src/registry/ui/marquee.tsx +53 -0
- package/src/registry/ui/native-select.tsx +3 -0
- package/src/registry/ui/otp-input.tsx +3 -0
- package/src/registry/ui/pagination.tsx +3 -0
- package/src/registry/ui/popover.tsx +3 -0
- package/src/registry/ui/progress.tsx +3 -0
- package/src/registry/ui/radio.tsx +3 -0
- package/src/registry/ui/scroll-area.tsx +3 -0
- package/src/registry/ui/scroll-to-top.tsx +99 -0
- package/src/registry/ui/search.tsx +3 -0
- package/src/registry/ui/select.tsx +3 -0
- package/src/registry/ui/separator.tsx +3 -0
- package/src/registry/ui/skeleton.tsx +3 -0
- package/src/registry/ui/slider.tsx +3 -0
- package/src/registry/ui/slot.tsx +3 -0
- package/src/registry/ui/star-rating.tsx +3 -0
- package/src/registry/ui/switch.tsx +3 -0
- package/src/registry/ui/table-of-contents.tsx +96 -0
- package/src/registry/ui/table.tsx +3 -0
- package/src/registry/ui/tabs.tsx +3 -0
- package/src/registry/ui/text.tsx +3 -0
- package/src/registry/ui/textarea.tsx +3 -0
- package/src/registry/ui/toast.tsx +3 -0
- package/src/registry/ui/tooltip.tsx +3 -0
- package/src/registry/ui/whatsapp.tsx +36 -0
package/dist/index.js
CHANGED
|
@@ -437,7 +437,7 @@ var REGISTRY = {
|
|
|
437
437
|
description: "Text input field",
|
|
438
438
|
category: "Forms",
|
|
439
439
|
dependencies: [],
|
|
440
|
-
registryDependencies: ["react-icons
|
|
440
|
+
registryDependencies: ["react-icons"]
|
|
441
441
|
},
|
|
442
442
|
textarea: {
|
|
443
443
|
file: "ui/textarea.tsx",
|
|
@@ -684,7 +684,7 @@ var REGISTRY = {
|
|
|
684
684
|
description: "Date picker with calendar",
|
|
685
685
|
category: "Forms",
|
|
686
686
|
dependencies: ["calendar", "popover", "button"],
|
|
687
|
-
registryDependencies: ["date-fns
|
|
687
|
+
registryDependencies: ["date-fns"]
|
|
688
688
|
},
|
|
689
689
|
drawer: {
|
|
690
690
|
file: "ui/drawer.tsx",
|
|
@@ -787,13 +787,19 @@ var REGISTRY = {
|
|
|
787
787
|
description: "Charts using Recharts",
|
|
788
788
|
category: "Data Display",
|
|
789
789
|
dependencies: [],
|
|
790
|
-
registryDependencies: ["recharts
|
|
790
|
+
registryDependencies: ["recharts"]
|
|
791
791
|
},
|
|
792
792
|
slot: {
|
|
793
793
|
file: "ui/slot.tsx",
|
|
794
794
|
description: "Slot utility for polymorphic components",
|
|
795
795
|
category: "Core",
|
|
796
796
|
dependencies: []
|
|
797
|
+
},
|
|
798
|
+
"table-of-contents": {
|
|
799
|
+
file: "ui/table-of-contents.tsx",
|
|
800
|
+
description: "Table of contents navigation",
|
|
801
|
+
category: "Navigation",
|
|
802
|
+
dependencies: []
|
|
797
803
|
}
|
|
798
804
|
};
|
|
799
805
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import Script from 'next/script';
|
|
2
4
|
import type { FC } from 'react';
|
|
3
5
|
|
|
4
6
|
interface MetaPixelProps {
|
|
5
|
-
pixelIds: string[]; //
|
|
7
|
+
pixelIds: string[]; // ↠array now
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
const MetaPixel: FC<MetaPixelProps> = ({ pixelIds }) => {
|
|
@@ -42,3 +44,4 @@ const MetaPixel: FC<MetaPixelProps> = ({ pixelIds }) => {
|
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export default MetaPixel;
|
|
47
|
+
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import Script from 'next/script';
|
|
2
4
|
import type { FC } from 'react';
|
|
3
5
|
|
|
4
6
|
interface TikTokPixelProps {
|
|
5
|
-
pixelIds: string[]; //
|
|
7
|
+
pixelIds: string[]; // ↠array
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
const TikTokPixel: FC<TikTokPixelProps> = ({ pixelIds }) => {
|
|
@@ -32,3 +34,4 @@ const TikTokPixel: FC<TikTokPixelProps> = ({ pixelIds }) => {
|
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export default TikTokPixel;
|
|
37
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -160,3 +162,4 @@ const AccordionContent = React.forwardRef<
|
|
|
160
162
|
AccordionContent.displayName = "AccordionContent"
|
|
161
163
|
|
|
162
164
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
165
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -71,3 +73,4 @@ const AlertDescription = React.forwardRef<
|
|
|
71
73
|
AlertDescription.displayName = "AlertDescription"
|
|
72
74
|
|
|
73
75
|
export { Alert, AlertTitle, AlertDescription }
|
|
76
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -92,3 +94,4 @@ const AvatarFallback = React.forwardRef<
|
|
|
92
94
|
AvatarFallback.displayName = "AvatarFallback"
|
|
93
95
|
|
|
94
96
|
export { Avatar, AvatarImage, AvatarFallback, avatarVariants }
|
|
97
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -50,3 +52,4 @@ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
|
50
52
|
Badge.displayName = "Badge"
|
|
51
53
|
|
|
52
54
|
export { Badge, badgeVariants }
|
|
55
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -62,3 +64,4 @@ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
|
62
64
|
ButtonGroup.displayName = "ButtonGroup"
|
|
63
65
|
|
|
64
66
|
export { ButtonGroup, buttonGroupVariants }
|
|
67
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -67,3 +69,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
67
69
|
Button.displayName = "Button"
|
|
68
70
|
|
|
69
71
|
export { Button, buttonVariants }
|
|
72
|
+
|
package/src/registry/ui/card.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -86,3 +88,4 @@ const CardFooter = React.forwardRef<
|
|
|
86
88
|
CardFooter.displayName = "CardFooter"
|
|
87
89
|
|
|
88
90
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
91
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
import { Slot } from "@/components/ui/slot"
|
|
@@ -103,3 +105,4 @@ const CollapsibleContent = React.forwardRef<
|
|
|
103
105
|
CollapsibleContent.displayName = "CollapsibleContent"
|
|
104
106
|
|
|
105
107
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
108
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -42,3 +44,4 @@ const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
|
|
|
42
44
|
Container.displayName = "Container"
|
|
43
45
|
|
|
44
46
|
export { Container, containerVariants }
|
|
47
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
icon?: React.ElementType
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
action?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EmptyState({
|
|
13
|
+
icon: Icon,
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
action,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: EmptyStateProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex flex-col items-center justify-center py-12 text-center border-2 border-dashed rounded-lg bg-muted/5",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{Icon && (
|
|
29
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted/50 mb-4">
|
|
30
|
+
<Icon className="h-10 w-10 text-muted-foreground" />
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
<h3 className="text-lg font-semibold tracking-tight">{title}</h3>
|
|
34
|
+
<p className="text-muted-foreground text-sm max-w-sm mt-2 mb-6">
|
|
35
|
+
{description}
|
|
36
|
+
</p>
|
|
37
|
+
{action && <div>{action}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -13,6 +13,7 @@ interface FileUploadProps {
|
|
|
13
13
|
maxFiles?: number
|
|
14
14
|
className?: string
|
|
15
15
|
disabled?: boolean
|
|
16
|
+
variant?: 'default' | 'compact'
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
interface UploadedFile {
|
|
@@ -44,6 +45,7 @@ export function FileUpload({
|
|
|
44
45
|
maxFiles = 5,
|
|
45
46
|
className,
|
|
46
47
|
disabled = false,
|
|
48
|
+
variant = 'default',
|
|
47
49
|
}: FileUploadProps) {
|
|
48
50
|
const [files, setFiles] = React.useState<UploadedFile[]>([])
|
|
49
51
|
const [isDragging, setIsDragging] = React.useState(false)
|
|
@@ -141,7 +143,9 @@ export function FileUpload({
|
|
|
141
143
|
onDragLeave={handleDragLeave}
|
|
142
144
|
onDrop={handleDrop}
|
|
143
145
|
className={cn(
|
|
144
|
-
"relative flex
|
|
146
|
+
"relative flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed transition-colors",
|
|
147
|
+
variant === 'default' && "flex-col gap-4 p-8",
|
|
148
|
+
variant === 'compact' && "flex-row gap-3 p-4",
|
|
145
149
|
isDragging
|
|
146
150
|
? "border-primary bg-primary/5"
|
|
147
151
|
: "border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50",
|
|
@@ -159,28 +163,55 @@ export function FileUpload({
|
|
|
159
163
|
disabled={disabled}
|
|
160
164
|
/>
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
{variant === 'default' && (
|
|
167
|
+
<>
|
|
168
|
+
<div className={cn(
|
|
169
|
+
"flex h-14 w-14 items-center justify-center rounded-full bg-muted transition-colors",
|
|
170
|
+
isDragging && "bg-primary/10"
|
|
171
|
+
)}>
|
|
172
|
+
<LuUpload className={cn(
|
|
173
|
+
"h-6 w-6 text-muted-foreground",
|
|
174
|
+
isDragging && "text-primary"
|
|
175
|
+
)} />
|
|
176
|
+
</div>
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
<div className="text-center">
|
|
179
|
+
<p className="text-sm font-medium">
|
|
180
|
+
{isDragging ? "Drop files here" : "Drag & drop files here"}
|
|
181
|
+
</p>
|
|
182
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
183
|
+
or click to browse
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<p className="text-xs text-muted-foreground">
|
|
188
|
+
{accept ? `Accepted: ${accept}` : "All file types accepted"} • Max {formatFileSize(maxSize)}
|
|
189
|
+
</p>
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{variant === 'compact' && (
|
|
194
|
+
<>
|
|
195
|
+
<div className={cn(
|
|
196
|
+
"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-muted transition-colors",
|
|
197
|
+
isDragging && "bg-primary/10"
|
|
198
|
+
)}>
|
|
199
|
+
<LuUpload className={cn(
|
|
200
|
+
"h-5 w-5 text-muted-foreground",
|
|
201
|
+
isDragging && "text-primary"
|
|
202
|
+
)} />
|
|
203
|
+
</div>
|
|
180
204
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
<div className="flex flex-col text-left">
|
|
206
|
+
<p className="text-sm font-medium">
|
|
207
|
+
{isDragging ? "Drop files" : "Upload files"}
|
|
208
|
+
</p>
|
|
209
|
+
<p className="text-xs text-muted-foreground">
|
|
210
|
+
{accept ? accept : `Max ${formatFileSize(maxSize)}`}
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
</>
|
|
214
|
+
)}
|
|
184
215
|
</div>
|
|
185
216
|
|
|
186
217
|
{/* Error Message */}
|
|
@@ -193,6 +224,11 @@ export function FileUpload({
|
|
|
193
224
|
<div className="space-y-2">
|
|
194
225
|
{files.map((uploadedFile, index) => {
|
|
195
226
|
const FileIcon = getFileIcon(uploadedFile.file.type)
|
|
227
|
+
const dotIndex = uploadedFile.file.name.lastIndexOf('.')
|
|
228
|
+
const hasExtension = dotIndex !== -1
|
|
229
|
+
const name = hasExtension ? uploadedFile.file.name.slice(0, dotIndex) : uploadedFile.file.name
|
|
230
|
+
const extension = hasExtension ? uploadedFile.file.name.slice(dotIndex) : ''
|
|
231
|
+
|
|
196
232
|
return (
|
|
197
233
|
<div
|
|
198
234
|
key={index}
|
|
@@ -211,9 +247,14 @@ export function FileUpload({
|
|
|
211
247
|
)}
|
|
212
248
|
|
|
213
249
|
<div className="flex-1 min-w-0">
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
<div className="flex items-center min-w-0">
|
|
251
|
+
<span className="truncate text-sm font-medium">
|
|
252
|
+
{name}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-sm font-medium whitespace-nowrap">
|
|
255
|
+
{extension}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
217
258
|
<p className="text-xs text-muted-foreground">
|
|
218
259
|
{formatFileSize(uploadedFile.file.size)}
|
|
219
260
|
</p>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface FloatingDockProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FloatingDock({ className, children, ...props }: FloatingDockProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={cn(
|
|
14
|
+
"fixed bottom-4 right-4 md:bottom-6 md:right-6 z-50 flex flex-col items-center gap-2 md:gap-3",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { Label } from "@/components/ui/label"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -89,3 +91,4 @@ const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
|
|
|
89
91
|
FormField.displayName = "FormField"
|
|
90
92
|
|
|
91
93
|
export { FormField }
|
|
94
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -142,3 +144,4 @@ const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
|
|
142
144
|
Image.displayName = "Image"
|
|
143
145
|
|
|
144
146
|
export { Image, imageVariants }
|
|
147
|
+
|
package/src/registry/ui/kbd.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -10,7 +12,7 @@ interface KbdProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
10
12
|
* Kbd - Keyboard key display component
|
|
11
13
|
*
|
|
12
14
|
* Usage:
|
|
13
|
-
* <Kbd
|
|
15
|
+
* <Kbd>⌘</Kbd>
|
|
14
16
|
* <Kbd keys={["Ctrl", "Shift", "P"]} />
|
|
15
17
|
*/
|
|
16
18
|
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
@@ -58,3 +60,4 @@ const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
|
58
60
|
Kbd.displayName = "Kbd"
|
|
59
61
|
|
|
60
62
|
export { Kbd }
|
|
63
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -106,3 +108,4 @@ const LoadingOverlay = React.forwardRef<HTMLDivElement, LoadingOverlayProps>(
|
|
|
106
108
|
LoadingOverlay.displayName = "LoadingOverlay"
|
|
107
109
|
|
|
108
110
|
export { LoadingSpinner, LoadingOverlay, spinnerVariants }
|
|
111
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
// import dynamic from "next/dynamic"
|
|
6
|
+
|
|
7
|
+
// Dynamically import Leaflet components only on client side
|
|
8
|
+
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"
|
|
9
|
+
import "leaflet/dist/leaflet.css"
|
|
10
|
+
import L from "leaflet"
|
|
11
|
+
|
|
12
|
+
// Fix for default marker icon in Next.js
|
|
13
|
+
const fixLeafletIcon = () => {
|
|
14
|
+
delete (L.Icon.Default.prototype as any)._getIconUrl
|
|
15
|
+
L.Icon.Default.mergeOptions({
|
|
16
|
+
iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
|
17
|
+
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
|
18
|
+
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MapProps {
|
|
23
|
+
lat: number
|
|
24
|
+
lng: number
|
|
25
|
+
zoom?: number
|
|
26
|
+
provider?: "osm" | "google"
|
|
27
|
+
className?: string
|
|
28
|
+
googleApiKey?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ChangeView({ center, zoom }: { center: [number, number]; zoom: number }) {
|
|
32
|
+
const map = useMap()
|
|
33
|
+
map.setView(center, zoom)
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function OSMMap({ lat, lng, zoom = 13, className }: MapProps) {
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fixLeafletIcon()
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<MapContainer center={[lat, lng]} zoom={zoom} scrollWheelZoom={false} className={cn("h-full w-full", className)}>
|
|
44
|
+
<ChangeView center={[lat, lng]} zoom={zoom} />
|
|
45
|
+
<TileLayer
|
|
46
|
+
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
47
|
+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
48
|
+
/>
|
|
49
|
+
<Marker position={[lat, lng]}>
|
|
50
|
+
<Popup>
|
|
51
|
+
We are here! <br /> Come visit us.
|
|
52
|
+
</Popup>
|
|
53
|
+
</Marker>
|
|
54
|
+
</MapContainer>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function GoogleMapEmbed({ lat, lng, zoom = 13, className, googleApiKey }: MapProps) {
|
|
59
|
+
// Falls back to simple no-key embed if no key provided (limited functionality)
|
|
60
|
+
// Or uses a proper specific place search if a key is provided.
|
|
61
|
+
// For agency demo without a key, generic iframe might be safer or just a placeholder message.
|
|
62
|
+
|
|
63
|
+
// Using unauthenticated iframe for location
|
|
64
|
+
const src = `https://maps.google.com/maps?q=${lat},${lng}&t=&z=${zoom}&ie=UTF8&iwloc=&output=embed`
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<iframe
|
|
68
|
+
width="100%"
|
|
69
|
+
height="100%"
|
|
70
|
+
className={cn("border-0", className)}
|
|
71
|
+
src={src}
|
|
72
|
+
allowFullScreen
|
|
73
|
+
loading="lazy"
|
|
74
|
+
referrerPolicy="no-referrer-when-downgrade"
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function Map(props: MapProps) {
|
|
80
|
+
const [isMounted, setIsMounted] = useState(false)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setIsMounted(true)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
if (!isMounted) {
|
|
87
|
+
return (
|
|
88
|
+
<div className={cn("flex items-center justify-center bg-muted animate-pulse", props.className)}>
|
|
89
|
+
<span className="text-muted-foreground">Loading Map...</span>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (props.provider === "google") {
|
|
95
|
+
return <GoogleMapEmbed {...props} />
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <OSMMap {...props} />
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
interface MarqueeProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
reverse?: boolean;
|
|
8
|
+
pauseOnHover?: boolean;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
vertical?: boolean;
|
|
11
|
+
repeat?: number;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Marquee({
|
|
16
|
+
className,
|
|
17
|
+
reverse,
|
|
18
|
+
pauseOnHover = false,
|
|
19
|
+
children,
|
|
20
|
+
vertical = false,
|
|
21
|
+
repeat = 4,
|
|
22
|
+
...props
|
|
23
|
+
}: MarqueeProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
{...props}
|
|
27
|
+
className={cn(
|
|
28
|
+
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
|
|
29
|
+
{
|
|
30
|
+
"flex-row": !vertical,
|
|
31
|
+
"flex-col": vertical,
|
|
32
|
+
},
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{Array(repeat)
|
|
37
|
+
.fill(0)
|
|
38
|
+
.map((_, i) => (
|
|
39
|
+
<div
|
|
40
|
+
key={i}
|
|
41
|
+
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
|
|
42
|
+
"animate-marquee flex-row": !vertical,
|
|
43
|
+
"animate-marquee-vertical flex-col": vertical,
|
|
44
|
+
"group-hover:[animation-play-state:paused]": pauseOnHover,
|
|
45
|
+
"[animation-direction:reverse]": reverse,
|
|
46
|
+
})}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { createPortal } from "react-dom"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -222,3 +224,4 @@ const PopoverContent = React.forwardRef<
|
|
|
222
224
|
PopoverContent.displayName = "PopoverContent"
|
|
223
225
|
|
|
224
226
|
export { Popover, PopoverTrigger, PopoverContent }
|
|
227
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -97,3 +99,4 @@ const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(
|
|
|
97
99
|
RadioGroupItem.displayName = "RadioGroupItem"
|
|
98
100
|
|
|
99
101
|
export { RadioGroup, RadioGroupItem }
|
|
102
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -117,3 +119,4 @@ const ScrollBar = React.forwardRef<HTMLDivElement, ScrollBarProps>(
|
|
|
117
119
|
ScrollBar.displayName = "ScrollBar"
|
|
118
120
|
|
|
119
121
|
export { ScrollArea, ScrollBar }
|
|
122
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { LuArrowUp } from "react-icons/lu"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { FaArrowUp } from "react-icons/fa"
|
|
8
|
+
|
|
9
|
+
export function ScrollToTop() {
|
|
10
|
+
const [isVisible, setIsVisible] = React.useState(false)
|
|
11
|
+
const [scrollProgress, setScrollProgress] = React.useState(0)
|
|
12
|
+
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const handleScroll = () => {
|
|
15
|
+
// Visibility toggle
|
|
16
|
+
if (window.scrollY > 300) {
|
|
17
|
+
setIsVisible(true)
|
|
18
|
+
} else {
|
|
19
|
+
setIsVisible(false)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Progress calculation
|
|
23
|
+
const winScroll = document.body.scrollTop || document.documentElement.scrollTop
|
|
24
|
+
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
|
|
25
|
+
const scrolled = (winScroll / height) * 100
|
|
26
|
+
setScrollProgress(scrolled)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
window.addEventListener("scroll", handleScroll)
|
|
30
|
+
return () => window.removeEventListener("scroll", handleScroll)
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const scrollToTop = () => {
|
|
34
|
+
window.scrollTo({
|
|
35
|
+
top: 0,
|
|
36
|
+
behavior: "smooth",
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isVisible) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Circumference of the circle (r=28) -> 2 * pi * 28 ≈ 175.929
|
|
45
|
+
const circumference = 175.929
|
|
46
|
+
const strokeDashoffset = circumference - (scrollProgress / 100) * circumference
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Button
|
|
50
|
+
variant="default"
|
|
51
|
+
size="icon"
|
|
52
|
+
onClick={scrollToTop}
|
|
53
|
+
aria-label="Scroll to top"
|
|
54
|
+
className={cn(
|
|
55
|
+
"relative group flex items-center justify-center cursor-pointer",
|
|
56
|
+
"h-10 w-10 md:h-16 md:w-16 rounded-full",
|
|
57
|
+
"bg-primary",
|
|
58
|
+
"shadow-lg transition-all duration-300 hover:scale-110 z-50",
|
|
59
|
+
"text-white"
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{/* Progress Ring SVG */}
|
|
63
|
+
<svg
|
|
64
|
+
className="absolute inset-0 -rotate-90 pointer-events-none !w-full !h-full"
|
|
65
|
+
width="100%"
|
|
66
|
+
height="100%"
|
|
67
|
+
viewBox="0 0 64 64"
|
|
68
|
+
>
|
|
69
|
+
{/* Background Circle Track */}
|
|
70
|
+
<circle
|
|
71
|
+
cx="32"
|
|
72
|
+
cy="32"
|
|
73
|
+
r="28"
|
|
74
|
+
stroke="rgba(255,255,255,0.2)"
|
|
75
|
+
strokeWidth="4"
|
|
76
|
+
fill="none"
|
|
77
|
+
/>
|
|
78
|
+
{/* Progress Circle */}
|
|
79
|
+
<circle
|
|
80
|
+
cx="32"
|
|
81
|
+
cy="32"
|
|
82
|
+
r="28"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth="4"
|
|
85
|
+
fill="none"
|
|
86
|
+
strokeDasharray={circumference}
|
|
87
|
+
strokeDashoffset={strokeDashoffset}
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
className="transition-all duration-150 ease-out text-white"
|
|
90
|
+
/>
|
|
91
|
+
</svg>
|
|
92
|
+
|
|
93
|
+
{/* Icon */}
|
|
94
|
+
<div className="relative z-10">
|
|
95
|
+
<LuArrowUp className="!h-5 !w-5 md:!h-8 md:!w-8 drop-shadow-md" />
|
|
96
|
+
</div>
|
|
97
|
+
</Button>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { createPortal } from "react-dom"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -288,3 +290,4 @@ const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
|
|
|
288
290
|
SelectItem.displayName = "SelectItem"
|
|
289
291
|
|
|
290
292
|
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }
|
|
293
|
+
|
package/src/registry/ui/slot.tsx
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export interface HeadingInfo {
|
|
7
|
+
id: string
|
|
8
|
+
text: string
|
|
9
|
+
level: number
|
|
10
|
+
children?: HeadingInfo[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TableOfContentsProps {
|
|
14
|
+
headings?: HeadingInfo[]
|
|
15
|
+
activeSection?: string | null
|
|
16
|
+
className?: string
|
|
17
|
+
onSectionClick?: (id: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TableOfContents({
|
|
21
|
+
headings = [],
|
|
22
|
+
activeSection,
|
|
23
|
+
className,
|
|
24
|
+
onSectionClick,
|
|
25
|
+
}: TableOfContentsProps) {
|
|
26
|
+
|
|
27
|
+
const scrollToHeading = React.useCallback((headingId: string) => {
|
|
28
|
+
if (onSectionClick) {
|
|
29
|
+
onSectionClick(headingId)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const element = document.getElementById(headingId)
|
|
34
|
+
if (element) {
|
|
35
|
+
const offset = 80
|
|
36
|
+
const elementPosition =
|
|
37
|
+
element.getBoundingClientRect().top + window.pageYOffset
|
|
38
|
+
window.scrollTo({
|
|
39
|
+
top: elementPosition - offset,
|
|
40
|
+
behavior: "smooth",
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}, [onSectionClick])
|
|
44
|
+
|
|
45
|
+
// Flatten all headings without hierarchy for simplicity
|
|
46
|
+
const flattenedHeadings = React.useMemo(() => {
|
|
47
|
+
const flatten = (headings: HeadingInfo[]): HeadingInfo[] => {
|
|
48
|
+
return headings.flatMap((heading) => [
|
|
49
|
+
heading,
|
|
50
|
+
...(heading.children ? flatten(heading.children) : []),
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
return flatten(headings)
|
|
54
|
+
}, [headings])
|
|
55
|
+
|
|
56
|
+
if (flattenedHeadings.length === 0) return null
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"hidden lg:block sticky top-16 right-0 h-[calc(100vh-4rem)] w-full bg-background justify-self-end lg:max-w-64",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div className="flex flex-col h-full">
|
|
66
|
+
<div className="flex items-center justify-between p-4">
|
|
67
|
+
<span className="font-medium text-foreground text-sm">
|
|
68
|
+
On this page
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<nav className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
{flattenedHeadings.map((heading) => (
|
|
75
|
+
<button
|
|
76
|
+
key={heading.id}
|
|
77
|
+
onClick={() => scrollToHeading(heading.id)}
|
|
78
|
+
className={cn(
|
|
79
|
+
"w-full text-left px-2 py-1.5 rounded text-xs transition-colors duration-200",
|
|
80
|
+
activeSection === heading.id
|
|
81
|
+
? "text-primary bg-accent font-medium"
|
|
82
|
+
: "text-muted-foreground hover:text-foreground",
|
|
83
|
+
)}
|
|
84
|
+
style={{
|
|
85
|
+
paddingLeft: `${(heading.level - 1) * 12 + 8}px`,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{heading.text}
|
|
89
|
+
</button>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</nav>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
package/src/registry/ui/tabs.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cn } from "@/lib/utils"
|
|
3
5
|
|
|
@@ -120,3 +122,4 @@ const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
|
|
|
120
122
|
TabsContent.displayName = "TabsContent"
|
|
121
123
|
|
|
122
124
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
125
|
+
|
package/src/registry/ui/text.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -59,3 +61,4 @@ const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
|
59
61
|
Text.displayName = "Text"
|
|
60
62
|
|
|
61
63
|
export { Text, textVariants }
|
|
64
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -93,3 +95,4 @@ const ToastAction = React.forwardRef<
|
|
|
93
95
|
ToastAction.displayName = "ToastAction"
|
|
94
96
|
|
|
95
97
|
export { Toast, ToastTitle, ToastDescription, ToastAction, toastVariants }
|
|
98
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { createPortal } from "react-dom"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
@@ -139,3 +141,4 @@ const TooltipContent = React.forwardRef<
|
|
|
139
141
|
TooltipContent.displayName = "TooltipContent"
|
|
140
142
|
|
|
141
143
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
144
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { FaWhatsapp } from "react-icons/fa"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
|
|
8
|
+
interface WhatsAppProps {
|
|
9
|
+
phoneNumber?: string
|
|
10
|
+
message?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function WhatsApp({
|
|
14
|
+
phoneNumber = "1234567890", // Default placeholder
|
|
15
|
+
message = "Hello! I would like to know more about your services."
|
|
16
|
+
}: WhatsAppProps) {
|
|
17
|
+
const handleClick = () => {
|
|
18
|
+
const url = `https://wa.me/${phoneNumber}?text=${encodeURIComponent(message)}`
|
|
19
|
+
window.open(url, "_blank")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
variant="default"
|
|
25
|
+
size="icon"
|
|
26
|
+
onClick={handleClick}
|
|
27
|
+
className={cn(
|
|
28
|
+
"rounded-full h-10 w-10 md:h-16 md:w-16 shadow-lg transition-all duration-300 hover:scale-110",
|
|
29
|
+
"bg-[#25D366] hover:bg-[#20bd5a] text-white"
|
|
30
|
+
)}
|
|
31
|
+
aria-label="Contact us on WhatsApp"
|
|
32
|
+
>
|
|
33
|
+
<FaWhatsapp className="!h-5 !w-5 md:!h-8 md:!w-8" />
|
|
34
|
+
</Button>
|
|
35
|
+
)
|
|
36
|
+
}
|