@shipsite.dev/components 0.2.53 → 0.2.60
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/components.json +102 -0
- package/dist/context/ShipSiteProvider.d.ts +13 -0
- package/dist/context/ShipSiteProvider.d.ts.map +1 -1
- package/dist/context/ShipSiteProvider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +22 -2
- package/dist/layout/Header.js.map +1 -1
- package/dist/lib/use-form-submit.d.ts +7 -0
- package/dist/lib/use-form-submit.d.ts.map +1 -0
- package/dist/lib/use-form-submit.js +47 -0
- package/dist/lib/use-form-submit.js.map +1 -0
- package/dist/marketing/ContactForm.d.ts +16 -0
- package/dist/marketing/ContactForm.d.ts.map +1 -0
- package/dist/marketing/ContactForm.js +29 -0
- package/dist/marketing/ContactForm.js.map +1 -0
- package/dist/marketing/Form.d.ts +25 -0
- package/dist/marketing/Form.d.ts.map +1 -0
- package/dist/marketing/Form.js +18 -0
- package/dist/marketing/Form.js.map +1 -0
- package/dist/marketing/FormClient.d.ts +23 -0
- package/dist/marketing/FormClient.d.ts.map +1 -0
- package/dist/marketing/FormClient.js +41 -0
- package/dist/marketing/FormClient.js.map +1 -0
- package/dist/marketing/FormEmbed.d.ts +13 -0
- package/dist/marketing/FormEmbed.d.ts.map +1 -0
- package/dist/marketing/FormEmbed.js +27 -0
- package/dist/marketing/FormEmbed.js.map +1 -0
- package/dist/marketing/NewsletterForm.d.ts +13 -0
- package/dist/marketing/NewsletterForm.d.ts.map +1 -0
- package/dist/marketing/NewsletterForm.js +23 -0
- package/dist/marketing/NewsletterForm.js.map +1 -0
- package/dist/marketing/WaitlistForm.d.ts +16 -0
- package/dist/marketing/WaitlistForm.d.ts.map +1 -0
- package/dist/marketing/WaitlistForm.js +27 -0
- package/dist/marketing/WaitlistForm.js.map +1 -0
- package/dist/ui/input.d.ts +5 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +7 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/label.d.ts +5 -0
- package/dist/ui/label.d.ts.map +1 -0
- package/dist/ui/label.js +9 -0
- package/dist/ui/label.js.map +1 -0
- package/dist/ui/navigation-menu.d.ts +15 -0
- package/dist/ui/navigation-menu.d.ts.map +1 -0
- package/dist/ui/navigation-menu.js +32 -0
- package/dist/ui/navigation-menu.js.map +1 -0
- package/dist/ui/select.d.ts +14 -0
- package/dist/ui/select.d.ts.map +1 -0
- package/dist/ui/select.js +28 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/textarea.d.ts +5 -0
- package/dist/ui/textarea.d.ts.map +1 -0
- package/dist/ui/textarea.js +7 -0
- package/dist/ui/textarea.js.map +1 -0
- package/package.json +4 -1
- package/src/context/ShipSiteProvider.tsx +13 -1
- package/src/index.ts +7 -0
- package/src/layout/Header.tsx +155 -19
- package/src/lib/use-form-submit.ts +53 -0
- package/src/marketing/ContactForm.tsx +157 -0
- package/src/marketing/Form.tsx +41 -0
- package/src/marketing/FormClient.tsx +194 -0
- package/src/marketing/FormEmbed.tsx +118 -0
- package/src/marketing/NewsletterForm.tsx +102 -0
- package/src/marketing/WaitlistForm.tsx +145 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/navigation-menu.tsx +168 -0
- package/src/ui/select.tsx +160 -0
- package/src/ui/textarea.tsx +20 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { XIcon } from "lucide-react";
|
|
6
|
+
import { Section } from "../ui/section";
|
|
7
|
+
import { Button } from "../ui/button";
|
|
8
|
+
|
|
9
|
+
function resolveUrl(src: string, provider: "tally" | "typeform" | "custom"): string {
|
|
10
|
+
switch (provider) {
|
|
11
|
+
case "tally":
|
|
12
|
+
return `https://tally.so/embed/${src}?transparentBackground=1`;
|
|
13
|
+
case "typeform":
|
|
14
|
+
return `https://form.typeform.com/to/${src}`;
|
|
15
|
+
case "custom":
|
|
16
|
+
return src;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FormEmbedProps {
|
|
21
|
+
id?: string;
|
|
22
|
+
src: string;
|
|
23
|
+
provider?: "tally" | "typeform" | "custom";
|
|
24
|
+
title?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
height?: number;
|
|
27
|
+
mode?: "iframe" | "popup";
|
|
28
|
+
buttonLabel?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function FormEmbed({
|
|
32
|
+
id,
|
|
33
|
+
src,
|
|
34
|
+
provider = "custom",
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
height = 500,
|
|
38
|
+
mode = "iframe",
|
|
39
|
+
buttonLabel = "Open Form",
|
|
40
|
+
}: FormEmbedProps) {
|
|
41
|
+
const resolvedUrl = resolveUrl(src, provider);
|
|
42
|
+
|
|
43
|
+
if (mode === "popup") {
|
|
44
|
+
return <FormEmbedPopup url={resolvedUrl} buttonLabel={buttonLabel} height={height} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Section id={id}>
|
|
49
|
+
<div className="container-main max-w-3xl">
|
|
50
|
+
{(title || description) && (
|
|
51
|
+
<div className="text-center mb-12">
|
|
52
|
+
{title && (
|
|
53
|
+
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
|
54
|
+
{title}
|
|
55
|
+
</h2>
|
|
56
|
+
)}
|
|
57
|
+
{description && (
|
|
58
|
+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
59
|
+
{description}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
<div className="glass-1 rounded-2xl overflow-hidden">
|
|
65
|
+
<iframe
|
|
66
|
+
src={resolvedUrl}
|
|
67
|
+
height={height}
|
|
68
|
+
className="w-full border-0"
|
|
69
|
+
loading="lazy"
|
|
70
|
+
title={title || "Embedded form"}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</Section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function FormEmbedPopup({
|
|
79
|
+
url,
|
|
80
|
+
buttonLabel,
|
|
81
|
+
height,
|
|
82
|
+
}: {
|
|
83
|
+
url: string;
|
|
84
|
+
buttonLabel: string;
|
|
85
|
+
height: number;
|
|
86
|
+
}) {
|
|
87
|
+
return (
|
|
88
|
+
<DialogPrimitive.Root>
|
|
89
|
+
<DialogPrimitive.Trigger asChild>
|
|
90
|
+
<Button variant="default" size="lg">
|
|
91
|
+
{buttonLabel}
|
|
92
|
+
</Button>
|
|
93
|
+
</DialogPrimitive.Trigger>
|
|
94
|
+
<DialogPrimitive.Portal>
|
|
95
|
+
<DialogPrimitive.Overlay className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" />
|
|
96
|
+
<DialogPrimitive.Content className="bg-background border-border dark:border-border/15 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-50 w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-2xl border p-0 shadow-lg overflow-hidden">
|
|
97
|
+
<DialogPrimitive.Title className="sr-only">
|
|
98
|
+
{buttonLabel}
|
|
99
|
+
</DialogPrimitive.Title>
|
|
100
|
+
<DialogPrimitive.Description className="sr-only">
|
|
101
|
+
Embedded form
|
|
102
|
+
</DialogPrimitive.Description>
|
|
103
|
+
<iframe
|
|
104
|
+
src={url}
|
|
105
|
+
height={height}
|
|
106
|
+
className="w-full border-0"
|
|
107
|
+
loading="lazy"
|
|
108
|
+
title={buttonLabel}
|
|
109
|
+
/>
|
|
110
|
+
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring absolute top-4 right-4 z-[100] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden">
|
|
111
|
+
<XIcon className="size-5" />
|
|
112
|
+
<span className="sr-only">Close</span>
|
|
113
|
+
</DialogPrimitive.Close>
|
|
114
|
+
</DialogPrimitive.Content>
|
|
115
|
+
</DialogPrimitive.Portal>
|
|
116
|
+
</DialogPrimitive.Root>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { FormEvent, useState } from "react";
|
|
4
|
+
import { CheckCircle, Loader2 } from "lucide-react";
|
|
5
|
+
import { Section } from "../ui/section";
|
|
6
|
+
import { Input } from "../ui/input";
|
|
7
|
+
import { Button } from "../ui/button";
|
|
8
|
+
import { useFormSubmit } from "../lib/use-form-submit";
|
|
9
|
+
|
|
10
|
+
interface NewsletterFormProps {
|
|
11
|
+
id?: string;
|
|
12
|
+
action: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
submitLabel?: string;
|
|
17
|
+
successMessage?: string;
|
|
18
|
+
variant?: "section" | "inline";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NewsletterForm({
|
|
22
|
+
id,
|
|
23
|
+
action,
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
placeholder = "Enter your email",
|
|
27
|
+
submitLabel = "Subscribe",
|
|
28
|
+
successMessage = "You're subscribed!",
|
|
29
|
+
variant = "section",
|
|
30
|
+
}: NewsletterFormProps) {
|
|
31
|
+
const { status, errorMsg, submit } = useFormSubmit(action);
|
|
32
|
+
const [email, setEmail] = useState("");
|
|
33
|
+
|
|
34
|
+
function handleSubmit(e: FormEvent) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
submit({ email });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const successContent = (
|
|
40
|
+
<div className="flex items-center justify-center gap-2 text-sm text-primary">
|
|
41
|
+
<CheckCircle className="size-4" />
|
|
42
|
+
<span>{successMessage}</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const formContent =
|
|
47
|
+
status === "success" ? (
|
|
48
|
+
successContent
|
|
49
|
+
) : (
|
|
50
|
+
<>
|
|
51
|
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-2 max-w-md mx-auto">
|
|
52
|
+
<Input
|
|
53
|
+
type="email"
|
|
54
|
+
placeholder={placeholder}
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
57
|
+
required
|
|
58
|
+
className="flex-1"
|
|
59
|
+
disabled={status === "loading"}
|
|
60
|
+
/>
|
|
61
|
+
<Button type="submit" variant="default" disabled={status === "loading"}>
|
|
62
|
+
{status === "loading" ? (
|
|
63
|
+
<Loader2 className="size-4 animate-spin" />
|
|
64
|
+
) : (
|
|
65
|
+
submitLabel
|
|
66
|
+
)}
|
|
67
|
+
</Button>
|
|
68
|
+
</form>
|
|
69
|
+
{status === "error" && (
|
|
70
|
+
<p className="text-sm text-destructive text-center mt-2" aria-live="polite">
|
|
71
|
+
{errorMsg}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (variant === "inline") {
|
|
78
|
+
return formContent;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Section id={id}>
|
|
83
|
+
<div className="container-main max-w-2xl">
|
|
84
|
+
{(title || description) && (
|
|
85
|
+
<div className="text-center mb-12">
|
|
86
|
+
{title && (
|
|
87
|
+
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
|
88
|
+
{title}
|
|
89
|
+
</h2>
|
|
90
|
+
)}
|
|
91
|
+
{description && (
|
|
92
|
+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
93
|
+
{description}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
{formContent}
|
|
99
|
+
</div>
|
|
100
|
+
</Section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { FormEvent, useId, useState } from "react";
|
|
4
|
+
import { CheckCircle, Loader2 } from "lucide-react";
|
|
5
|
+
import { Section } from "../ui/section";
|
|
6
|
+
import { Input } from "../ui/input";
|
|
7
|
+
import { Label } from "../ui/label";
|
|
8
|
+
import { Button } from "../ui/button";
|
|
9
|
+
import { Badge } from "../ui/badge";
|
|
10
|
+
import { useFormSubmit } from "../lib/use-form-submit";
|
|
11
|
+
|
|
12
|
+
interface WaitlistFormProps {
|
|
13
|
+
id?: string;
|
|
14
|
+
action: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
badge?: string;
|
|
18
|
+
showName?: boolean;
|
|
19
|
+
nameLabel?: string;
|
|
20
|
+
emailLabel?: string;
|
|
21
|
+
submitLabel?: string;
|
|
22
|
+
successTitle?: string;
|
|
23
|
+
successMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function WaitlistForm({
|
|
27
|
+
id,
|
|
28
|
+
action,
|
|
29
|
+
title = "Join the Waitlist",
|
|
30
|
+
description,
|
|
31
|
+
badge,
|
|
32
|
+
showName = false,
|
|
33
|
+
nameLabel = "Name",
|
|
34
|
+
emailLabel = "Email",
|
|
35
|
+
submitLabel = "Join Waitlist",
|
|
36
|
+
successTitle = "You're on the list!",
|
|
37
|
+
successMessage = "We'll notify you when it's your turn.",
|
|
38
|
+
}: WaitlistFormProps) {
|
|
39
|
+
const uid = useId();
|
|
40
|
+
const { status, errorMsg, submit } = useFormSubmit(action);
|
|
41
|
+
const [fields, setFields] = useState({ name: "", email: "" });
|
|
42
|
+
|
|
43
|
+
function update(key: string, value: string) {
|
|
44
|
+
setFields((prev) => ({ ...prev, [key]: value }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleSubmit(e: FormEvent) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
const data: Record<string, string> = { email: fields.email };
|
|
50
|
+
if (showName) data.name = fields.name;
|
|
51
|
+
submit(data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Section id={id}>
|
|
56
|
+
<div className="container-main max-w-2xl text-center">
|
|
57
|
+
{badge && (
|
|
58
|
+
<div className="mb-4">
|
|
59
|
+
<Badge variant="outline">{badge}</Badge>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{(title || description) && (
|
|
63
|
+
<div className="mb-12">
|
|
64
|
+
{title && (
|
|
65
|
+
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
|
66
|
+
{title}
|
|
67
|
+
</h2>
|
|
68
|
+
)}
|
|
69
|
+
{description && (
|
|
70
|
+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
71
|
+
{description}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{status === "success" ? (
|
|
78
|
+
<div className="text-center py-12">
|
|
79
|
+
<div className="w-16 h-16 rounded-full bg-primary/10 mx-auto mb-4 flex items-center justify-center">
|
|
80
|
+
<CheckCircle className="w-8 h-8 text-primary" />
|
|
81
|
+
</div>
|
|
82
|
+
<h3 className="text-xl font-semibold text-foreground mb-2">
|
|
83
|
+
{successTitle}
|
|
84
|
+
</h3>
|
|
85
|
+
<p className="text-muted-foreground">{successMessage}</p>
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<form
|
|
89
|
+
onSubmit={handleSubmit}
|
|
90
|
+
className="glass-1 rounded-2xl p-8 max-w-md mx-auto space-y-4"
|
|
91
|
+
>
|
|
92
|
+
{showName && (
|
|
93
|
+
<div className="space-y-2 text-left">
|
|
94
|
+
<Label htmlFor={`${uid}-name`}>{nameLabel}</Label>
|
|
95
|
+
<Input
|
|
96
|
+
id={`${uid}-name`}
|
|
97
|
+
name="name"
|
|
98
|
+
placeholder="Your name"
|
|
99
|
+
value={fields.name}
|
|
100
|
+
onChange={(e) => update("name", e.target.value)}
|
|
101
|
+
required
|
|
102
|
+
disabled={status === "loading"}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
<div className="space-y-2 text-left">
|
|
107
|
+
<Label htmlFor={`${uid}-email`}>{emailLabel}</Label>
|
|
108
|
+
<Input
|
|
109
|
+
id={`${uid}-email`}
|
|
110
|
+
name="email"
|
|
111
|
+
type="email"
|
|
112
|
+
placeholder="you@example.com"
|
|
113
|
+
value={fields.email}
|
|
114
|
+
onChange={(e) => update("email", e.target.value)}
|
|
115
|
+
required
|
|
116
|
+
disabled={status === "loading"}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<Button
|
|
120
|
+
type="submit"
|
|
121
|
+
variant="default"
|
|
122
|
+
size="lg"
|
|
123
|
+
className="w-full"
|
|
124
|
+
disabled={status === "loading"}
|
|
125
|
+
>
|
|
126
|
+
{status === "loading" ? (
|
|
127
|
+
<>
|
|
128
|
+
<Loader2 className="size-4 animate-spin mr-2" />
|
|
129
|
+
Joining...
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
submitLabel
|
|
133
|
+
)}
|
|
134
|
+
</Button>
|
|
135
|
+
{status === "error" && (
|
|
136
|
+
<p className="text-sm text-destructive" aria-live="polite">
|
|
137
|
+
{errorMsg}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
</form>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
</Section>
|
|
144
|
+
);
|
|
145
|
+
}
|
package/src/ui/input.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
6
|
+
|
|
7
|
+
function Input({ className, type, ...props }: InputProps) {
|
|
8
|
+
return (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
data-slot="input"
|
|
12
|
+
className={cn(
|
|
13
|
+
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input };
|
package/src/ui/label.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
function Label({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<LabelPrimitive.Root
|
|
14
|
+
data-slot="label"
|
|
15
|
+
className={cn(
|
|
16
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Label };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
function NavigationMenu({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
viewport = true,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
|
14
|
+
viewport?: boolean;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<NavigationMenuPrimitive.Root
|
|
18
|
+
data-slot="navigation-menu"
|
|
19
|
+
data-viewport={viewport}
|
|
20
|
+
className={cn(
|
|
21
|
+
"group/navigation-menu relative z-10 flex max-w-max flex-1 items-center justify-center",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
{viewport && <NavigationMenuViewport />}
|
|
28
|
+
</NavigationMenuPrimitive.Root>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function NavigationMenuList({
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
|
36
|
+
return (
|
|
37
|
+
<NavigationMenuPrimitive.List
|
|
38
|
+
data-slot="navigation-menu-list"
|
|
39
|
+
className={cn(
|
|
40
|
+
"group flex flex-1 list-none items-center justify-center gap-1",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function NavigationMenuItem({
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
|
52
|
+
return (
|
|
53
|
+
<NavigationMenuPrimitive.Item
|
|
54
|
+
data-slot="navigation-menu-item"
|
|
55
|
+
className={cn("relative", className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const navigationMenuTriggerStyle = cva(
|
|
62
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium hover:bg-foreground/5 hover:text-accent-foreground focus:bg-foreground/10 focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-foreground/10 data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-foreground/10 data-[state=open]:bg-foreground/5 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function NavigationMenuTrigger({
|
|
66
|
+
className,
|
|
67
|
+
children,
|
|
68
|
+
...props
|
|
69
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
|
70
|
+
return (
|
|
71
|
+
<NavigationMenuPrimitive.Trigger
|
|
72
|
+
data-slot="navigation-menu-trigger"
|
|
73
|
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{children}{" "}
|
|
77
|
+
<ChevronDownIcon
|
|
78
|
+
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
/>
|
|
81
|
+
</NavigationMenuPrimitive.Trigger>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function NavigationMenuContent({
|
|
86
|
+
className,
|
|
87
|
+
...props
|
|
88
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
|
89
|
+
return (
|
|
90
|
+
<NavigationMenuPrimitive.Content
|
|
91
|
+
data-slot="navigation-menu-content"
|
|
92
|
+
className={cn(
|
|
93
|
+
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
|
94
|
+
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function NavigationMenuLink({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
|
106
|
+
return (
|
|
107
|
+
<NavigationMenuPrimitive.Link
|
|
108
|
+
data-slot="navigation-menu-link"
|
|
109
|
+
className={cn(
|
|
110
|
+
"data-[active=true]:focus:bg-foreground/10 data-[active=true]:hover:bg-foreground/10 data-[active=true]:bg-foreground/10 data-[active=true]:text-accent-foreground hover:bg-foreground/10 hover:text-accent-foreground focus:bg-foreground/10 focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function NavigationMenuViewport({
|
|
119
|
+
className,
|
|
120
|
+
...props
|
|
121
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
className={cn(
|
|
125
|
+
"absolute top-full left-0 isolate z-50 flex justify-center",
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<NavigationMenuPrimitive.Viewport
|
|
129
|
+
data-slot="navigation-menu-viewport"
|
|
130
|
+
className={cn(
|
|
131
|
+
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 border-border dark:border-border/15 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow-sm md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function NavigationMenuIndicator({
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
|
144
|
+
return (
|
|
145
|
+
<NavigationMenuPrimitive.Indicator
|
|
146
|
+
data-slot="navigation-menu-indicator"
|
|
147
|
+
className={cn(
|
|
148
|
+
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
|
154
|
+
</NavigationMenuPrimitive.Indicator>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
NavigationMenu,
|
|
160
|
+
NavigationMenuContent,
|
|
161
|
+
NavigationMenuIndicator,
|
|
162
|
+
NavigationMenuItem,
|
|
163
|
+
NavigationMenuLink,
|
|
164
|
+
NavigationMenuList,
|
|
165
|
+
NavigationMenuTrigger,
|
|
166
|
+
navigationMenuTriggerStyle,
|
|
167
|
+
NavigationMenuViewport,
|
|
168
|
+
};
|