a
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/retroui";
|
||||
|
||||
interface ICodeBlock extends React.HTMLAttributes<HTMLPreElement> {}
|
||||
|
||||
export async function copyToClipboardWithMeta(value: string, event?: Event) {
|
||||
navigator.clipboard.writeText(value);
|
||||
if (event) {
|
||||
// trackEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
export function CodeBlock({ className, children, ...props }: ICodeBlock) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false);
|
||||
const preRef = React.useRef<HTMLPreElement>(null);
|
||||
|
||||
const handleClickCopy = async () => {
|
||||
const code = preRef.current?.textContent;
|
||||
if (code) {
|
||||
setHasCopied(true);
|
||||
await navigator.clipboard.writeText(code);
|
||||
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<pre
|
||||
className={cn(
|
||||
"overflow-x-auto rounded-sm bg-[#282A36] mt-3 mb-6 p-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
id="cody-copy-button"
|
||||
data-umami-event="copy-code-button"
|
||||
disabled={hasCopied}
|
||||
className="absolute top-4 right-4 z-10"
|
||||
size="sm"
|
||||
onClick={handleClickCopy}
|
||||
>
|
||||
{hasCopied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
<span ref={preRef}>{children}</span>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import React from "react";
|
||||
import { HTMLAttributes } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./retroui";
|
||||
|
||||
interface IComponentShowcase extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const CopyableCommand = ({ command }: { command: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 group">
|
||||
<code className="flex-1">{command}</code>
|
||||
<Button size="sm" onClick={copyToClipboard} className="hidden md:block" title="Copy to clipboard">
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
<Button className="md:hidden" size="icon" onClick={copyToClipboard} title="Copy to clipboard mobile">
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CliCommand({
|
||||
npmCommand,
|
||||
yarnCommand,
|
||||
pnpmCommand,
|
||||
bunCommand,
|
||||
}: {
|
||||
npmCommand: string;
|
||||
yarnCommand?: string;
|
||||
pnpmCommand?: string;
|
||||
bunCommand?: string;
|
||||
}) {
|
||||
const isNpx = npmCommand.includes("npx");
|
||||
if (isNpx) {
|
||||
pnpmCommand = pnpmCommand ?? npmCommand.replace("npx", "pnpm dlx");
|
||||
yarnCommand = yarnCommand ?? npmCommand.replace("npx", "yarn dlx");
|
||||
bunCommand = bunCommand ?? npmCommand.replace("npx", "bunx");
|
||||
} else {
|
||||
pnpmCommand = pnpmCommand ?? npmCommand.replace("npm", "pnpm");
|
||||
yarnCommand = yarnCommand ?? npmCommand.replace("npm install", "yarn add");
|
||||
bunCommand = bunCommand ?? npmCommand.replace("npm", "bun");
|
||||
}
|
||||
|
||||
return (
|
||||
<TabGroup className="p-4 my-2 bg-gray-800 rounded-md text-background/90">
|
||||
<TabList className="flex space-x-4 mb-6 text-sm">
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
pnpm
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
npm
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
yarn
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
bun
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="text-sm text-accent">
|
||||
<TabPanel>
|
||||
<CopyableCommand command={pnpmCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={npmCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={yarnCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={bunCommand} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentInstallCli({
|
||||
npmCommand,
|
||||
yarnCommand,
|
||||
pnpmCommand,
|
||||
bunCommand,
|
||||
}: {
|
||||
npmCommand: string;
|
||||
yarnCommand?: string;
|
||||
pnpmCommand?: string;
|
||||
bunCommand?: string;
|
||||
}) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<CliCommand
|
||||
npmCommand={npmCommand}
|
||||
yarnCommand={yarnCommand}
|
||||
pnpmCommand={pnpmCommand}
|
||||
bunCommand={bunCommand}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentInstallManual({ children }: { children: React.ReactNode }) {
|
||||
return <TabPanel>{children}</TabPanel>;
|
||||
}
|
||||
|
||||
function ComponentInstall({ children }: IComponentShowcase) {
|
||||
return (
|
||||
<TabGroup>
|
||||
<TabList className="border-b bg-transparent flex space-x-4 mb-6 text-sm">
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
CLI
|
||||
</Tab>
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Manual
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>{children}</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(ComponentInstall, {
|
||||
Cli: ComponentInstallCli,
|
||||
Manual: ComponentInstallManual,
|
||||
});
|
||||
|
||||
export { ComponentInstall };
|
||||
@@ -0,0 +1,64 @@
|
||||
import { componentConfig } from "@/config";
|
||||
import { TabGroup, TabList, TabPanels, TabPanel, Tab } from "@headlessui/react";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import { Button } from "./retroui";
|
||||
|
||||
interface IComponentShowcase extends HTMLAttributes<HTMLDivElement> {
|
||||
name: keyof typeof componentConfig.examples;
|
||||
}
|
||||
|
||||
export function ComponentShowcase({ name, children }: IComponentShowcase) {
|
||||
const { preview: Preview } = componentConfig.examples[name];
|
||||
const Code = React.Children.toArray(children)[0];
|
||||
|
||||
return (
|
||||
<TabGroup>
|
||||
<TabList className="space-x-4 border-b mb-6 text-sm">
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Preview
|
||||
</Tab>
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Code
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="border rounded-sm px-6 py-24 flex justify-center items-center min-h-80 mt-4 relative">
|
||||
<a
|
||||
href={`https://v0.dev/chat/api/open?url=https://retroui.dev/r/${name}.json`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="font-sans absolute top-4 right-4"
|
||||
>
|
||||
Open in
|
||||
<svg
|
||||
viewBox="0 0 40 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 ml-2 text-current"
|
||||
>
|
||||
<path
|
||||
d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<Preview />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="relative rounded-sm overflow-auto">{Code}</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ComponentSourceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function ComponentSource({ children, className }: ComponentSourceProps) {
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded-md", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Button } from "@/components/retroui/Button";
|
||||
import { Dialog } from "@/components/retroui/Dialog";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { Theme, themeConfig } from "@/config/theme";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
|
||||
export default function CopyTheme({ theme, hasRadius = false }: { theme: Theme, hasRadius?: boolean }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Trigger asChild>
|
||||
<Button>Copy Theme</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content className={`theme-${theme} ${hasRadius ? "with-radius" : ""}`}>
|
||||
<Dialog.Header>
|
||||
<Text as="h5">Copy Theme</Text>
|
||||
</Dialog.Header>
|
||||
<section className="w-full p-4">
|
||||
<Text className="text-lg mb-4 text-muted-foreground">Copy and paste the following theme code into your CSS file.</Text>
|
||||
|
||||
<CodeBlock className="w-full max-h-[480px] text-gray-300 text-sm overflow-y-auto">
|
||||
{themeConfig({ hasRadius })[theme].color}
|
||||
</CodeBlock>
|
||||
</section>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SideNav from "./SideNav";
|
||||
import { AlignJustify, X } from "lucide-react";
|
||||
import { Button } from "@/components/retroui";
|
||||
|
||||
export default function HamburgerMenu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="p-2"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="h-4 w-4" />
|
||||
) : (
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 w-full h-screen bg-black opacity-50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-0 bottom-0 h-screen left-0 z-10">
|
||||
<SideNav setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Text, Input } from "@/components/retroui";
|
||||
import { useState } from "react";
|
||||
|
||||
const INIT = "INIT";
|
||||
const SUBMITTING = "SUBMITTING";
|
||||
const ERROR = "ERROR";
|
||||
const SUCCESS = "SUCCESS";
|
||||
const formStates = [INIT, SUBMITTING, ERROR, SUCCESS] as const;
|
||||
const formStyles = {
|
||||
id: "cltl71xnw000ris85z92rosvh",
|
||||
name: "Default",
|
||||
formStyle: "inline",
|
||||
placeholderText: "you@example.com",
|
||||
formFont: "font-sans",
|
||||
formFontColor: "#000000",
|
||||
formFontSizePx: 14,
|
||||
buttonText: "Subscribe",
|
||||
buttonFont: "font-sans",
|
||||
buttonFontColor: "#ffffff",
|
||||
buttonColor: "#000000",
|
||||
buttonFontSizePx: 14,
|
||||
successMessage: "Thanks! We'll be in touch!",
|
||||
successFont: "Inter",
|
||||
successFontColor: "#000000",
|
||||
successFontSizePx: 14,
|
||||
userGroup: "",
|
||||
};
|
||||
const domain = "app.loops.so";
|
||||
|
||||
export function JoinNewsletter() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [formState, setFormState] = useState<(typeof formStates)[number]>(INIT);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const resetForm = () => {
|
||||
setEmail("");
|
||||
setFormState(INIT);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limit the number of submissions allowed
|
||||
* @returns {boolean} true if the form has been successfully submitted in the past minute
|
||||
*/
|
||||
const hasRecentSubmission = () => {
|
||||
const time = new Date();
|
||||
const timestamp = time.valueOf();
|
||||
const previousTimestamp = localStorage.getItem("loops-form-timestamp");
|
||||
|
||||
// Indicate if the last sign up was less than a minute ago
|
||||
if (
|
||||
previousTimestamp &&
|
||||
Number(previousTimestamp) + 60 * 1000 > timestamp
|
||||
) {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage("Too many signups, please try again in a little while");
|
||||
return true;
|
||||
}
|
||||
|
||||
localStorage.setItem("loops-form-timestamp", timestamp.toString());
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
// Prevent the default form submission
|
||||
event.preventDefault();
|
||||
|
||||
// boundary conditions for submission
|
||||
if (formState !== INIT) return;
|
||||
if (!isValidEmail(email)) {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage("Please enter a valid email");
|
||||
return;
|
||||
}
|
||||
if (hasRecentSubmission()) return;
|
||||
setFormState(SUBMITTING);
|
||||
|
||||
// build body
|
||||
const formBody = `userGroup=${encodeURIComponent(
|
||||
formStyles.userGroup,
|
||||
)}&email=${encodeURIComponent(email)}&mailingLists=`;
|
||||
|
||||
// API request to add user to newsletter
|
||||
fetch(`https://${domain}/api/newsletter-form/${formStyles.id}`, {
|
||||
method: "POST",
|
||||
body: formBody,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((res: any) => [res.ok, res.json(), res])
|
||||
.then(([ok, dataPromise, res]) => {
|
||||
if (ok) {
|
||||
resetForm();
|
||||
setFormState(SUCCESS);
|
||||
} else {
|
||||
dataPromise.then((data: any) => {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage(data.message || res.statusText);
|
||||
localStorage.setItem("loops-form-timestamp", "");
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setFormState(ERROR);
|
||||
// check for cloudflare error
|
||||
if (error.message === "Failed to fetch") {
|
||||
setErrorMessage(
|
||||
"Too many signups, please try again in a little while",
|
||||
);
|
||||
} else if (error.message) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
localStorage.setItem("loops-form-timestamp", "");
|
||||
});
|
||||
};
|
||||
|
||||
switch (formState) {
|
||||
case SUCCESS:
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: `'${formStyles.successFont}', sans-serif`,
|
||||
color: formStyles.successFontColor,
|
||||
fontSize: `${formStyles.successFontSizePx}px`,
|
||||
}}
|
||||
>
|
||||
{formStyles.successMessage}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
case ERROR:
|
||||
return (
|
||||
<>
|
||||
<SignUpFormError />
|
||||
<BackButton />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto py-16 px-4 border-2 border-black">
|
||||
<div className="flex flex-col justify-center items-center text-center">
|
||||
<Text as="h2">Join Our Newsletter</Text>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
Get notified about latest updates and new launches.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex space-x-2 w-full lg:w-1/2"
|
||||
>
|
||||
<Input
|
||||
placeholder={formStyles.placeholderText}
|
||||
value={email}
|
||||
type="text"
|
||||
name="email"
|
||||
required={true}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button>Subscribe</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignUpFormError() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "Inter, sans-serif",
|
||||
color: "rgb(185, 28, 28)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{errorMessage || "Oops! Something went wrong, please try again"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BackButton() {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
color: "#6b7280",
|
||||
font: "14px, Inter, sans-serif",
|
||||
margin: "10px auto",
|
||||
textAlign: "center",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
textDecoration: isHovered ? "underline" : "none",
|
||||
}}
|
||||
onMouseOut={() => setIsHovered(false)}
|
||||
onMouseOver={() => setIsHovered(true)}
|
||||
onClick={resetForm}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email: any) {
|
||||
return /.+@.+/.test(email);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Badge, Card, Text } from "@/components/retroui";
|
||||
import { useMDXComponent } from "next-contentlayer/hooks";
|
||||
import React, { AnchorHTMLAttributes, HTMLAttributes } from "react";
|
||||
import { ComponentShowcase } from "./ComponentShowcase";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ComponentSource } from "./ComponentSource";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import Link from "next/link";
|
||||
import { ComponentInstall, CliCommand } from "./ComponentInstall";
|
||||
import Image from "next/image";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Table } from "./retroui/Table";
|
||||
|
||||
const components = (type: "doc" | "blog") => ({
|
||||
h1: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h1" {...props} />
|
||||
),
|
||||
h2: (props: HTMLAttributes<HTMLHeadingElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text as="h2" className="mb-4 mt-8" {...props} />
|
||||
) : (
|
||||
<Text as="h2" className="border-b pb-1 mb-6" {...props} />
|
||||
),
|
||||
h3: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h3" className="mb-4" {...props} />
|
||||
),
|
||||
h4: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h4" className="mb-2" {...props} />
|
||||
),
|
||||
h5: (props: HTMLAttributes<HTMLHeadElement>) => <Text as="h5" {...props} />,
|
||||
h6: (props: HTMLAttributes<HTMLHeadElement>) => <Text as="h6" {...props} />,
|
||||
p: (props: HTMLAttributes<HTMLHeadElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text {...props} className="text-lg text-foreground" />
|
||||
) : (
|
||||
<Text {...props} />
|
||||
),
|
||||
li: (props: HTMLAttributes<HTMLHeadElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text
|
||||
as="li"
|
||||
{...props}
|
||||
className="text-lg text-foreground ml-4 lg:ml-8 mb-2"
|
||||
/>
|
||||
) : (
|
||||
<Text as="li" className="mb-2" {...props} />
|
||||
),
|
||||
img: (props: HTMLAttributes<HTMLImageElement>) => (
|
||||
<img className="mx-auto w-full max-w-[600px] my-8" {...props} />
|
||||
),
|
||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { href, target, rel, ...rest } = props;
|
||||
|
||||
if (!href) {
|
||||
return <a {...rest} />;
|
||||
}
|
||||
|
||||
const isExternal = href.startsWith("http");
|
||||
|
||||
return isExternal ? (
|
||||
<a
|
||||
href={href}
|
||||
target={target || "_blank"}
|
||||
rel={rel || "noopener noreferrer"}
|
||||
className="underline underline-offset-4 hover:decoration-primary"
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className="underline underline-offset-4 hover:decoration-primary"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
pre: CodeBlock,
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) => (
|
||||
<code
|
||||
className={cn(
|
||||
"relative rounded-sm bg-[#282A36] p-1 text-primary text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
TabGroup,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Table,
|
||||
Link,
|
||||
Badge,
|
||||
Image,
|
||||
Card,
|
||||
Alert,
|
||||
ComponentShowcase,
|
||||
ComponentSource,
|
||||
ComponentInstall,
|
||||
CliCommand,
|
||||
});
|
||||
|
||||
export default function MDX({
|
||||
code,
|
||||
type = "doc",
|
||||
}: {
|
||||
code: string;
|
||||
type?: "doc" | "blog";
|
||||
}) {
|
||||
const Content = useMDXComponent(code);
|
||||
|
||||
return <Content components={components(type)} />;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const RichTextCodeBlock: React.FC<{
|
||||
code: string
|
||||
language?: string
|
||||
title?: string
|
||||
}> = ({ code, language, title }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-6 rounded-lg overflow-hidden border-2 border-border bg-[#282A36]">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[#1e1f29] border-b-2 border-border">
|
||||
<span className="text-sm text-gray-400 font-mono">
|
||||
{title || language || 'code'}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon />
|
||||
<span>Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-4 overflow-x-auto">
|
||||
<code className="text-sm text-accent font-mono whitespace-pre">
|
||||
{code}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import Link from 'next/link'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedUploadNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import { Text } from '@/components/retroui'
|
||||
import { RichTextCodeBlock } from '@/components/RichTextCodeBlock'
|
||||
|
||||
const CustomUploadComponent: React.FC<{
|
||||
node: SerializedUploadNode
|
||||
}> = ({ node }) => {
|
||||
if (node.relationTo === 'media') {
|
||||
const uploadDoc = node.value
|
||||
if (typeof uploadDoc !== 'object') {
|
||||
return null
|
||||
}
|
||||
const { alt, height, url, width } = uploadDoc
|
||||
return (
|
||||
<img
|
||||
alt={alt}
|
||||
height={height}
|
||||
src={`https://cms.retroui.dev${url}`}
|
||||
width={width}
|
||||
className="mx-auto w-full max-w-[600px] my-8"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
type NodeTypes = DefaultNodeTypes
|
||||
export const JSXConverters: JSXConvertersFunction<NodeTypes> = ({
|
||||
defaultConverters,
|
||||
}) => {
|
||||
return {
|
||||
...defaultConverters,
|
||||
heading: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
const Tag = node.tag as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
const classNames: Record<string, string> = {
|
||||
h1: '',
|
||||
h2: 'mb-4 mt-8 lg:text-3xl',
|
||||
h3: 'mb-4',
|
||||
h4: 'mb-2',
|
||||
h5: '',
|
||||
h6: '',
|
||||
}
|
||||
return (
|
||||
<Text as={Tag} className={classNames[Tag]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
paragraph: ({ node, nodesToJSX }) => {
|
||||
// Handle empty paragraphs as spacing
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return <br />
|
||||
}
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return <Text className="text-lg text-foreground">{children}</Text>
|
||||
},
|
||||
text: ({ node }) => {
|
||||
const format = node.format ?? 0
|
||||
let text: React.ReactNode = node.text
|
||||
|
||||
// Format is a bitmask: 1=bold, 2=italic, 4=strikethrough, 8=underline, 16=code
|
||||
if (format & 16) {
|
||||
// Inline code
|
||||
text = (
|
||||
<code className="rounded-sm bg-[#282A36] px-1.5 py-0.5 text-primary text-sm">
|
||||
{text}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
if (format & 1) {
|
||||
text = <strong>{text}</strong>
|
||||
}
|
||||
if (format & 2) {
|
||||
text = <em>{text}</em>
|
||||
}
|
||||
if (format & 4) {
|
||||
text = <s>{text}</s>
|
||||
}
|
||||
if (format & 8) {
|
||||
text = <u>{text}</u>
|
||||
}
|
||||
|
||||
return <>{text}</>
|
||||
},
|
||||
list: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
if (node.listType === 'bullet') {
|
||||
return <ul className="list-disc pl-4 lg:pl-8 my-4">{children}</ul>
|
||||
}
|
||||
return <ol className="list-decimal pl-4 lg:pl-8 my-4">{children}</ol>
|
||||
},
|
||||
listitem: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return (
|
||||
<Text as="li" className="text-lg text-foreground ml-4 lg:ml-8 mb-2">
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
link: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
const href = node.fields?.url || ''
|
||||
const isExternal = href.startsWith('http')
|
||||
const linkClass = 'underline underline-offset-4 hover:decoration-primary'
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={linkClass}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href} className={linkClass}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
quote: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return (
|
||||
<blockquote className="border-l-4 border-primary pl-4 my-4 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
)
|
||||
},
|
||||
linebreak: () => {
|
||||
return <br />
|
||||
},
|
||||
upload: ({ node }) => {
|
||||
return <CustomUploadComponent node={node} />
|
||||
},
|
||||
// Handle autolink nodes
|
||||
autolink: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: (node as any).children })
|
||||
const href = (node as any).fields?.url || ''
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-4 hover:decoration-primary break-all"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
// Blocks converter - nested object with slug as key
|
||||
blocks: {
|
||||
Code: ({ node }: any) => {
|
||||
const fields = (node as any).fields
|
||||
return (
|
||||
<RichTextCodeBlock
|
||||
code={fields?.code || ''}
|
||||
language={fields?.language}
|
||||
title={fields?.blockName}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { navConfig } from "@/config/navigation";
|
||||
import { Badge, Text } from "@/components/retroui";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface SideNavProps {
|
||||
setIsOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export default function SideNav({ setIsOpen }: SideNavProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="sidebar-scroll border-r-2 z-10 overflow-y-scroll h-full max-h-[calc(100vh-6rem)] transition-transform transform md:translate-x-0 w-full bg-background flex flex-col justify-start md:justify-start py-8">
|
||||
<nav
|
||||
className="flex flex-col items-start max-lg:px-6 space-y-4 z-99"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{navConfig.sideNavItems.map((item) => (
|
||||
<div key={item.title} className="w-full">
|
||||
<Text as="h5">{item.title}</Text>
|
||||
<div className="flex flex-col w-full">
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.title}
|
||||
href={child.href}
|
||||
onClick={() => setIsOpen && setIsOpen(false)}
|
||||
target={child.href.startsWith("http") ? "_blank" : "_self"}
|
||||
className={`px-2 py-1 w-full border border-transparent text-muted-foreground flex items-center justify-between hover:text-foreground hover:bg-muted/50 transition-colors ${
|
||||
pathname === child.href &&
|
||||
"bg-primary text-primary-foreground"
|
||||
}`}
|
||||
>
|
||||
{child.title}
|
||||
{child.tag && (
|
||||
<Badge
|
||||
size="sm"
|
||||
className="py-0.5 px-1.5 border text-xs border-secondary text-muted-foreground bg-secondary/10"
|
||||
>
|
||||
{child.tag}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { TableOfContents as TOCType } from '@/lib/toc';
|
||||
|
||||
interface TableOfContentsProps {
|
||||
toc: TOCType;
|
||||
}
|
||||
|
||||
function renderTOCItems(items: any[], level = 0) {
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className={`space-y-1 ${level > 0 ? 'ml-4 mt-1' : ''}`}>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<a
|
||||
href={item.url}
|
||||
className="text-sm hover:text-foreground transition-colors block py-1 border-l-2 border-transparent hover:border-accent pl-2"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
{item.items && renderTOCItems(item.items, level + 1)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TableOfContents({ toc }: TableOfContentsProps) {
|
||||
if (!toc.items || toc.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border p-4 rounded-sm max-h-60 overflow-y-auto sidebar-scroll">
|
||||
<h3 className="mb-3 border-b border-black pb-2">
|
||||
On this Page
|
||||
</h3>
|
||||
{renderTOCItems(toc.items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Text, Card, Avatar } from "@/components/retroui";
|
||||
|
||||
interface TestimonialCardProps {
|
||||
quote: string;
|
||||
avatarSrc: string;
|
||||
name: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function TestimonialCard({
|
||||
quote,
|
||||
avatarSrc,
|
||||
name,
|
||||
title,
|
||||
}: TestimonialCardProps) {
|
||||
return (
|
||||
<Card className="w-full max-w-[400px] shadow-none hover:shadow-md">
|
||||
<Card.Content>
|
||||
<Text className="text-lg">{quote}</Text>
|
||||
|
||||
<div className="flex items-center space-x-2 mt-6">
|
||||
<Avatar className="h-10 w-10">
|
||||
<Avatar.Image alt={`${name} avatar`} src={avatarSrc} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{name}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function ThemeWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { colorTheme, variant } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper = document.querySelector(".theme-wrapper");
|
||||
if (wrapper) {
|
||||
wrapper.className = `theme-wrapper theme-${colorTheme} ${variant}`;
|
||||
}
|
||||
}, [colorTheme, variant]);
|
||||
|
||||
return <div className={`theme-wrapper theme-${colorTheme} ${variant}`}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowUpRight, GithubIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import HamburgerMenu from "./HamburgerMenu";
|
||||
import { Button, Text } from "@/components/retroui";
|
||||
import { navConfig } from "@/config/navigation";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
export default function TopNav() {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<nav className="sticky z-1 top-0 right-0 w-full border-b-2 bg-background">
|
||||
<div className="w-full bg-black text-white">
|
||||
<div className="container max-w-6xl mx-auto px-4 py-2 flex justify-center space-x-4 items-center">
|
||||
<Text className="text-sm lg:text-center">
|
||||
Ship faster with premium blocks and templates!
|
||||
</Text>
|
||||
<a href="https://dub.sh/retroui-pro" target="_blank">
|
||||
<Button
|
||||
size="sm"
|
||||
className="shadow-none font-sans font-bold px-2 py-1 w-30"
|
||||
>
|
||||
RetroUI Pro
|
||||
<ArrowUpRight className="ml-auto h-4 w-4 inline-block" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container max-w-6xl px-4 lg:px-0 mx-auto">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo Section */}
|
||||
<div className="shrink-0">
|
||||
<a
|
||||
href="/"
|
||||
className="text-black font-head text-2xl flex items-end"
|
||||
>
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="retro ui logo"
|
||||
className="mr-2"
|
||||
height={30}
|
||||
width={30}
|
||||
/>
|
||||
<div className="text-foreground">RetroUI</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="hidden md:flex space-x-4">
|
||||
{navConfig.topNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.href}
|
||||
className="hover:underline decoration-primary underline-offset-2 transition-all"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 lg:hidden">
|
||||
<Link
|
||||
href="https://github.com/logging-studio/retroui"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GithubIcon />
|
||||
</Link>
|
||||
<HamburgerMenu />
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-3">
|
||||
<Link
|
||||
href="https://github.com/logging-studio/retroui"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="secondary" size="icon">
|
||||
<GithubIcon size="14"/>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="secondary" size="icon" onClick={toggleDarkMode}>
|
||||
{isDarkMode ? <SunIcon size="14" /> : <MoonIcon size="14" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Text } from "@/components/retroui";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
export default function WorksWithSection() {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
name: "vite",
|
||||
lightSrc: "/images/logos/vite.png",
|
||||
darkSrc: "/images/logos/vite_white.png",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
name: "nextjs",
|
||||
lightSrc: "/images/logos/nextjs.png",
|
||||
darkSrc: "/images/logos/next_white.png",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
name: "remix",
|
||||
lightSrc: "/images/logos/remix.png",
|
||||
darkSrc: "/images/logos/remix_white.png",
|
||||
size: 55,
|
||||
},
|
||||
{
|
||||
name: "astro",
|
||||
lightSrc: "/images/logos/astro.png",
|
||||
darkSrc: "/images/logos/astro_white.png",
|
||||
size: 55,
|
||||
},
|
||||
{
|
||||
name: "gatsby",
|
||||
lightSrc: "/images/logos/gatsby.png",
|
||||
darkSrc: "/images/logos/gatsby_white.png",
|
||||
size: 60,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="container max-w-6xl mx-auto px-4 lg:px-0 my-48" id="works-with">
|
||||
<Text as="h2" className="text-center max-w-2xl mx-auto mb-28">
|
||||
Works With Both <span className="text-outlined">SSR</span> and{" "}
|
||||
<span className="text-outlined">SPA</span> Applications.
|
||||
</Text>
|
||||
|
||||
<div className="grid grid-cols-5 gap-8 lg:gap-6 max-w-2xl mx-auto items-center">
|
||||
{frameworks.map((framework) => (
|
||||
<div key={framework.name}>
|
||||
<Image
|
||||
src={isDarkMode ? framework.darkSrc : framework.lightSrc}
|
||||
alt={`${framework.name} ${isDarkMode ? 'dark' : 'light'}`}
|
||||
width={framework.size}
|
||||
height={framework.size}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/retroui/Button";
|
||||
import { Input } from "@/components/retroui/Input";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { Mail, TwitterIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { subscribeToNewsletter } from "@/app/actions/newsletter";
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Docs", href: "/docs" },
|
||||
{ name: "Blogs", href: "/blogs" },
|
||||
{ name: "Community", href: "https://discord.com/invite/Jum3NJxK6Q" },
|
||||
{ name: "Pro Blocks", href: "https://pro.retroui.dev" },
|
||||
];
|
||||
|
||||
const projectLinks = [
|
||||
{ name: "CommerCN", href: "https://commercn.com" },
|
||||
{ name: "Evendeals", href: "https://evendeals.com" },
|
||||
{ name: "Tanstack Starter Kit", href: "https://tanstackstarterkit.com" },
|
||||
];
|
||||
|
||||
function Footer() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
|
||||
const result = await subscribeToNewsletter(email);
|
||||
|
||||
if (result.success) {
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
} else {
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="border-t-2 mt-24">
|
||||
<div className="max-w-6xl mx-auto px-4 py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-12">
|
||||
<div className="lg:col-span-2 max-w-md">
|
||||
<Text as="h2" className="mb-6">
|
||||
STAY CONNECTED
|
||||
</Text>
|
||||
<Text className="text-muted-foreground leading-relaxed mb-2 text-sm">
|
||||
Join our community to stay updated with new component releases, updates, and special offers.
|
||||
</Text>
|
||||
<form onSubmit={handleSubscribe} className="mb-8">
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button className="w-54 justify-center" type="submit" disabled={status === "loading"}>
|
||||
{status === "loading" ? "..." : "SUBSCRIBE"}
|
||||
</Button>
|
||||
</div>
|
||||
{status === "success" && (
|
||||
<Text className="text-green-600 text-sm mt-2">Subscribed successfully!</Text>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<Text className="text-red-600 text-sm mt-2">Failed to subscribe. Try again.</Text>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="flex space-x-6">
|
||||
<a href="mailto:arif@retroui.dev" target="_blank" className="flex items-center gap-2">
|
||||
<div className="bg-black text-white p-1 shadow shadow-primary">
|
||||
<Mail className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="hover:underline underline-offset-4">
|
||||
arif@retroui.dev
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://x.com/ariflogs" target="_blank" className="flex items-center gap-2">
|
||||
<div className="bg-black text-white p-1 shadow shadow-primary">
|
||||
<TwitterIcon className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="hover:underline underline-offset-4">@ariflogs</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Text as="h4" className="mb-6">
|
||||
Quick Links
|
||||
</Text>
|
||||
<ul className="space-y-2">
|
||||
{quickLinks.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="hover:underline font-medium underline-offset-4 decoration-primary decoration-2 transition-all"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Text as="h4" className="mb-6">
|
||||
By Makers of RetroUI
|
||||
</Text>
|
||||
<ul className="space-y-2">
|
||||
{projectLinks.map((project) => (
|
||||
<li key={project.name}>
|
||||
<a
|
||||
href={project.href}
|
||||
target="_blank"
|
||||
className="hover:underline font-medium underline-offset-4 decoration-primary decoration-2 transition-all"
|
||||
>
|
||||
{project.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground">
|
||||
<div className="max-w-6xl mx-auto px-4 py-6 text-center">
|
||||
<Text className="text-sm text-background">
|
||||
© 2026 Logging Studio. Crafted with ❤️ and ☕.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./ComponentShowcase";
|
||||
export * from "./JoinNewsletter";
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-2 bg-background rounded text-foreground shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = AccordionPrimitive.Item.displayName;
|
||||
|
||||
const AccordionHeader = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionHeader.displayName = AccordionPrimitive.Header.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden font-body bg-white text-gray-700 data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("px-4 pt-2 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
const AccordionComponent = Object.assign(Accordion, {
|
||||
Item: AccordionItem,
|
||||
Header: AccordionHeader,
|
||||
Content: AccordionContent,
|
||||
});
|
||||
|
||||
export { AccordionComponent as Accordion };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HtmlHTMLAttributes } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
|
||||
const alertVariants = cva("relative w-full rounded border-2 p-4", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground [&_svg]:shrink-0",
|
||||
solid: "bg-black text-white",
|
||||
},
|
||||
status: {
|
||||
error: "bg-red-300 text-red-800 border-red-800",
|
||||
success: "bg-green-300 text-green-800 border-green-800",
|
||||
warning: "bg-yellow-300 text-yellow-800 border-yellow-800",
|
||||
info: "bg-blue-300 text-blue-800 border-blue-800",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAlertProps
|
||||
extends HtmlHTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof alertVariants> {}
|
||||
|
||||
const Alert = ({ className, variant, status, ...props }: IAlertProps) => (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant, status }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
interface IAlertTitleProps extends HtmlHTMLAttributes<HTMLHeadingElement> {}
|
||||
const AlertTitle = ({ className, ...props }: IAlertTitleProps) => (
|
||||
<Text as="h5" className={cn(className)} {...props} />
|
||||
);
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
interface IAlertDescriptionProps
|
||||
extends HtmlHTMLAttributes<HTMLParagraphElement> {}
|
||||
const AlertDescription = ({ className, ...props }: IAlertDescriptionProps) => (
|
||||
<div className={cn("text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
const AlertComponent = Object.assign(Alert, {
|
||||
Title: AlertTitle,
|
||||
Description: AlertDescription,
|
||||
});
|
||||
|
||||
export { AlertComponent as Alert };
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-14 w-14 border-2 rounded-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = "Avatar";
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = "Avatar.Image";
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-primary text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = "Avatar.Fallback";
|
||||
|
||||
const AvatarComponent = Object.assign(Avatar, {
|
||||
Image: AvatarImage,
|
||||
Fallback: AvatarFallback,
|
||||
});
|
||||
|
||||
export { AvatarComponent as Avatar };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
const badgeVariants = cva("font-semibold rounded inline-flex items-center", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted text-muted-foreground",
|
||||
outline: "outline-2 outline-foreground text-foreground",
|
||||
solid: "bg-foreground text-background",
|
||||
surface: "outline-2 bg-primary text-primary-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "px-2 py-1 text-xs",
|
||||
md: "px-2.5 py-1.5 text-sm",
|
||||
lg: "px-3 py-2 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface ButtonProps
|
||||
extends HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export function Badge({
|
||||
children,
|
||||
size = "md",
|
||||
variant = "default",
|
||||
className = "",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(badgeVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export function BasicCard() {
|
||||
return (
|
||||
<div className="inline-block border-2 p-4 shadow-md cursor-pointer transition-all hover:shadow-xs">
|
||||
<h4 className="font-head text-2xl font-medium mb-1">
|
||||
This is card Title
|
||||
</h4>
|
||||
<p>This is card description.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const BreadcrumbRoot = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<nav
|
||||
ref={ref}
|
||||
aria-label="breadcrumb"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbRoot.displayName = "Breadcrumb"
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 sm:gap-2.5 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("inline-flex items-center", className)} {...props} />
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-medium transition-colors hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring rounded-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
|
||||
|
||||
const Breadcrumb = Object.assign(BreadcrumbRoot, {
|
||||
List: BreadcrumbList,
|
||||
Item: BreadcrumbItem,
|
||||
Link: BreadcrumbLink,
|
||||
Page: BreadcrumbPage,
|
||||
Separator: BreadcrumbSeparator,
|
||||
Ellipsis: BreadcrumbEllipsis,
|
||||
})
|
||||
|
||||
export { Breadcrumb }
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"font-head transition-all rounded outline-hidden cursor-pointer duration-200 font-medium flex justify-center items-center disabled:opacity-60 disabled:cursor-not-allowed",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"shadow-md hover:shadow active:shadow-none bg-primary text-primary-foreground border-2 border-black transition hover:translate-y-1 active:translate-y-2 active:translate-x-1 hover:bg-primary-hover",
|
||||
secondary:
|
||||
"shadow-md hover:shadow active:shadow-none bg-secondary shadow-primary text-secondary-foreground border-2 border-black transition hover:translate-y-1 active:translate-y-2 active:translate-x-1 hover:bg-secondary-hover",
|
||||
outline:
|
||||
"shadow-md hover:shadow active:shadow-none bg-transparent border-2 transition hover:translate-y-1 active:translate-y-2 active:translate-x-1",
|
||||
link: "bg-transparent hover:underline",
|
||||
ghost: "bg-transparent hover:bg-accent"
|
||||
},
|
||||
size: {
|
||||
sm: "px-3 py-1 text-sm shadow hover:shadow-none",
|
||||
md: "px-4 py-1.5 text-base",
|
||||
lg: "px-6 lg:px-8 py-2 lg:py-3 text-md lg:text-lg",
|
||||
icon: "p-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface IButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
size = "md",
|
||||
className = "",
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: IButtonProps,
|
||||
forwardedRef,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
ref={forwardedRef}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
@@ -0,0 +1,210 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/retroui/Button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background w-full outline-2 shadow-md group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4 font-head", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-8 p-2 border-2 rounded select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-8 p-2 border-2 rounded select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:outline-ring outline outline-input has-focus:ring-ring/50 has-focus:ring-[3px] rounded",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-base"
|
||||
: "rounded-none pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full outline-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"flex-1 font-normal text-sm select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l",
|
||||
defaultClassNames.day
|
||||
),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground opacity-80",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"font-sans flex justify-center items-center data-[selected-single=true]:shadow-md data-[selected-single=true]:outline-2 outline-border data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-secondary data-[range-middle=true]:hover:text-secondary-foreground data-[range-middle=true]:text-secondary-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring-1 group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[2px] data-[range-end=true]:rounded-none data-[range-end=true]:rounded-none data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-none [&>span]:text-xs",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HTMLAttributes } from "react";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
|
||||
interface ICardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Card = ({ className, ...props }: ICardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-block border-2 rounded shadow-md transition-all hover:shadow-none bg-card",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardHeader = ({ className, ...props }: ICardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col justify-start p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardTitle = ({ className, ...props }: ICardProps) => {
|
||||
return <Text as="h3" className={cn("mb-2", className)} {...props} />;
|
||||
};
|
||||
|
||||
const CardDescription = ({ className, ...props }: ICardProps) => (
|
||||
<p className={cn("text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
|
||||
const CardContent = ({ className, ...props }: ICardProps) => {
|
||||
return <div className={cn("p-4", className)} {...props} />;
|
||||
};
|
||||
|
||||
const CardComponent = Object.assign(Card, {
|
||||
Header: CardHeader,
|
||||
Title: CardTitle,
|
||||
Description: CardDescription,
|
||||
Content: CardContent,
|
||||
});
|
||||
|
||||
export { CardComponent as Card };
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/retroui/Button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const CarouselObject = Object.assign(Carousel, {
|
||||
Content: CarouselContent,
|
||||
Item: CarouselItem,
|
||||
Previous: CarouselPrevious,
|
||||
Next: CarouselNext,
|
||||
})
|
||||
|
||||
export { CarouselObject as Carousel }
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
const checkboxVariants = cva("border-2 rounded", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground ",
|
||||
outline: "",
|
||||
solid:
|
||||
"data-[state=checked]:bg-foreground data-[state=checked]:text-background",
|
||||
},
|
||||
size: {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-5 w-5",
|
||||
lg: "h-6 w-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface CheckboxProps
|
||||
extends React.ComponentProps<typeof CheckboxPrimitive.Root>,
|
||||
VariantProps<typeof checkboxVariants> {}
|
||||
|
||||
export const Checkbox = ({
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: CheckboxProps) => (
|
||||
<CheckboxPrimitive.Root
|
||||
className={cn(
|
||||
checkboxVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="h-full w-full">
|
||||
<Check className="h-full w-full" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog } from "@/components/retroui/Dialog";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Check, LucideIcon, Search } from "lucide-react";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded bg-background text-foreground border-border shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type CommandDialogProps = DialogProps & { className?: string };
|
||||
|
||||
const CommandDialog = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<Dialog.Content
|
||||
className={cn(
|
||||
"overflow-hidden p-0 shadow-lg rounded w-full max-w-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center border-border border-b px-3"
|
||||
cmdk-input-wrapper=""
|
||||
data-slot="command-input"
|
||||
>
|
||||
<Search className="me-2 h-4 w-4 shrink-0 opacity-50 text-foreground" />
|
||||
<CommandPrimitive.Input
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded bg-transparent py-3 text-sm outline-hidden text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[400px] overflow-auto overscroll-contain transition-[height] h-[calc(min(300px,var(--cmdk-list-height)))] bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm text-muted-foreground font-body"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"overflow-hidden p-1.5 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"relative flex text-foreground cursor-pointer gap-2 select-none items-center rounded px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground transition-all font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"ms-auto text-xs tracking-widest text-muted-foreground font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface CommandCheckProps {
|
||||
icon?: LucideIcon;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CommandCheck({
|
||||
icon: Icon = Check,
|
||||
className,
|
||||
...props
|
||||
}: CommandCheckProps) {
|
||||
return (
|
||||
<Icon
|
||||
data-slot="command-check"
|
||||
data-check="true"
|
||||
className={cn("size-4 ms-auto text-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const CommandComponent = Object.assign(Command, {
|
||||
Check: CommandCheck,
|
||||
Dialog: CommandDialog,
|
||||
Empty: CommandEmpty,
|
||||
Group: CommandGroup,
|
||||
Input: CommandInput,
|
||||
Item: CommandItem,
|
||||
List: CommandList,
|
||||
Separator: CommandSeparator,
|
||||
Shortcut: CommandShortcut,
|
||||
});
|
||||
|
||||
export { CommandComponent as Command };
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { ClipboardIcon, CheckIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CommandDisplayProps {
|
||||
command: string;
|
||||
}
|
||||
|
||||
export function CommandDisplay({ command }: CommandDisplayProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultColors = [
|
||||
"text-blue-300",
|
||||
"text-yellow-300",
|
||||
"text-green-300",
|
||||
"text-purple-300",
|
||||
];
|
||||
|
||||
// Split the command into parts for syntax highlighting
|
||||
const parts = command.split(" ").map((part, index) => {
|
||||
const color = defaultColors[index % defaultColors.length];
|
||||
return (
|
||||
<span key={index} className={color}>
|
||||
{part}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative group flex items-center bg-black/90 pl-4 py-2 font-mono text-xs">
|
||||
<div className="flex-1 whitespace-nowrap overflow-hidden">
|
||||
<div className="overflow-hidden text-ellipsis">
|
||||
{parts.map((part, index) => (
|
||||
<span key={`part-${index}`}>
|
||||
{part}
|
||||
{index < parts.length - 1 && " "}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="mr-2 p-1 shrink-0 text-gray-400 hover:text-white transition-colors"
|
||||
aria-label="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-4 w-4 text-green-400" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground data-[state=open]:bg-primary data-[state=open]:text-primary-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-background text-foreground border-2 shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-hidden rounded-sm p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-background text-foreground border-2 shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-context-menu-content-available-height)] min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-x-hidden overflow-y-auto rounded-sm p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ContextMenuComponent = Object.assign(ContextMenu, {
|
||||
Trigger: ContextMenuTrigger,
|
||||
Content: ContextMenuContent,
|
||||
Item: ContextMenuItem,
|
||||
CheckboxItem: ContextMenuCheckboxItem,
|
||||
RadioItem: ContextMenuRadioItem,
|
||||
Label: ContextMenuLabel,
|
||||
Separator: ContextMenuSeparator,
|
||||
Shortcut: ContextMenuShortcut,
|
||||
Group: ContextMenuGroup,
|
||||
Portal: ContextMenuPortal,
|
||||
Sub: ContextMenuSub,
|
||||
SubContent: ContextMenuSubContent,
|
||||
SubTrigger: ContextMenuSubTrigger,
|
||||
RadioGroup: ContextMenuRadioGroup,
|
||||
});
|
||||
|
||||
export { ContextMenuComponent as ContextMenu };
|
||||
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import * as ReactDialog from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { HTMLAttributes, ReactNode } from "react";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const Dialog = ReactDialog.Root;
|
||||
const DialogTrigger = ReactDialog.Trigger;
|
||||
|
||||
const overlayVariants = cva(
|
||||
` fixed bg-black/80 font-head
|
||||
data-[state=open]:fade-in-0
|
||||
data-[state=open]:animate-in
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
`,
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "inset-0 z-50 bg-black/85",
|
||||
none: "fixed bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IDialogBackgroupProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof overlayVariants> {}
|
||||
|
||||
const DialogBackdrop = React.forwardRef<HTMLDivElement, IDialogBackgroupProps>(
|
||||
function DialogBackdrop(inputProps: IDialogBackgroupProps, forwardedRef) {
|
||||
const { variant = "default", className, ...props } = inputProps;
|
||||
|
||||
return (
|
||||
<ReactDialog.Overlay
|
||||
className={cn(overlayVariants({ variant }), className)}
|
||||
ref={forwardedRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogBackdrop.displayName = "DialogBackdrop";
|
||||
|
||||
const dialogVariants = cva(
|
||||
`fixed left-[50%] top-[50%] z-50 grid rounded overflow-hidden w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border-2 bg-background shadow-lg duration-200
|
||||
data-[state=open]:animate-in
|
||||
data-[state=open]:fade-in-0
|
||||
data-[state=open]:zoom-in-95
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
data-[state=closed]:zoom-out-95`,
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
auto: "max-w-fit",
|
||||
sm: "lg:max-w-[30%]",
|
||||
md: "lg:max-w-[40%]",
|
||||
lg: "lg:max-w-[50%]",
|
||||
xl: "lg:max-w-[60%]",
|
||||
"2xl": "lg:max-w-[70%]",
|
||||
"3xl": "lg:max-w-[80%]",
|
||||
"4xl": "lg:max-w-[90%]",
|
||||
screen: "max-w-[100%]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "auto",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IDialogContentProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogVariants> {
|
||||
overlay?: IDialogBackgroupProps;
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, IDialogContentProps>(
|
||||
function DialogContent(inputProps: IDialogContentProps, forwardedRef) {
|
||||
const {
|
||||
children,
|
||||
size = "auto",
|
||||
className,
|
||||
overlay,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
return (
|
||||
<ReactDialog.Portal>
|
||||
<DialogBackdrop {...overlay} />
|
||||
<ReactDialog.Content
|
||||
className={cn(dialogVariants({ size }), className)}
|
||||
ref={forwardedRef}
|
||||
{...props}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<ReactDialog.Title />
|
||||
</VisuallyHidden>
|
||||
<div className="flex flex-col relative">{children}</div>
|
||||
</ReactDialog.Content>
|
||||
</ReactDialog.Portal>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogContent.displayName = "DialogContent";
|
||||
|
||||
interface IDialogDescriptionProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
const DialogDescription = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: IDialogDescriptionProps) => {
|
||||
return (
|
||||
<ReactDialog.Description className={cn(className)} {...props}>
|
||||
{children}
|
||||
</ReactDialog.Description>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogFooterVariants = cva(
|
||||
"flex items-center justify-end border-t-2 min-h-12 gap-4 px-4 py-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
},
|
||||
position: {
|
||||
fixed: "sticky bottom-0",
|
||||
static: "static",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
position: "fixed",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface IDialogFooterProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogFooterVariants> {}
|
||||
|
||||
const DialogFooter = ({
|
||||
children,
|
||||
className,
|
||||
position,
|
||||
variant,
|
||||
...props
|
||||
}: IDialogFooterProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(dialogFooterVariants({ position, variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogHeaderVariants = cva(
|
||||
"flex items-center justify-between border-b-2 px-4 min-h-12",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
},
|
||||
position: {
|
||||
fixed: "sticky top-0",
|
||||
static: "static",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
position: "static",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const DialogHeaderDefaultLayout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<DialogTrigger title="Close pop-up" className="cursor-pointer" asChild>
|
||||
<X />
|
||||
</DialogTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IDialogHeaderProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogHeaderVariants>,
|
||||
ReactDialog.DialogTitleProps {}
|
||||
|
||||
const DialogHeader = ({
|
||||
children,
|
||||
className,
|
||||
position,
|
||||
variant,
|
||||
asChild,
|
||||
...props
|
||||
}: IDialogHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(dialogHeaderVariants({ position, variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{asChild ? (
|
||||
children
|
||||
) : (
|
||||
<DialogHeaderDefaultLayout>{children}</DialogHeaderDefaultLayout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DialogComponent = Object.assign(Dialog, {
|
||||
Trigger: DialogTrigger,
|
||||
Header: DialogHeader,
|
||||
Content: DialogContent,
|
||||
Description: DialogDescription,
|
||||
Footer: DialogFooter,
|
||||
});
|
||||
|
||||
export { DialogComponent as Dialog };
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"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/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b data-[vaul-drawer-direction=top]:border-b-2",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t data-[vaul-drawer-direction=bottom]:border-t-2",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l-2 data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r-2 data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-foreground mx-auto mt-4 hidden h-1 w-[60px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground text-xl font-head font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DrawerComponent = Object.assign(Drawer, {
|
||||
Trigger: DrawerTrigger,
|
||||
Portal: DrawerPortal,
|
||||
Overlay: DrawerOverlay,
|
||||
Close: DrawerClose,
|
||||
Content: DrawerContent,
|
||||
Header: DrawerHeader,
|
||||
Footer: DrawerFooter,
|
||||
Title: DrawerTitle,
|
||||
Description: DrawerDescription,
|
||||
});
|
||||
|
||||
export { DrawerComponent as Drawer };
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Ghost } from "lucide-react";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
interface IEmptyProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Empty = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center p-4 md:p-8 border-2 rounded shadow-md transition-all hover:shadow-none bg-card text-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Empty.displayName = "Empty";
|
||||
|
||||
const EmptyContent = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col items-center gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EmptyContent.displayName = "Empty.Content";
|
||||
|
||||
const EmptyIcon = ({ children, className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div className={cn(className)} {...props}>
|
||||
{children || <Ghost className="w-full h-full" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
EmptyIcon.displayName = "Empty.Icon";
|
||||
|
||||
const EmptyTitle = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<Text
|
||||
as="h3"
|
||||
className={cn("text-lg md:text-2xl font-bold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EmptyTitle.displayName = "Empty.Title";
|
||||
|
||||
const EmptySeparator = ({ className, ...props }: IEmptyProps) => {
|
||||
return <div role="separator" className={cn("w-full h-1 bg-primary", className)} {...props} />;
|
||||
};
|
||||
EmptySeparator.displayName = "Empty.Separator";
|
||||
|
||||
const EmptyDescription = ({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p
|
||||
className={cn("text-muted-foreground max-w-[320px]", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
EmptyDescription.displayName = "Empty.Description";
|
||||
|
||||
const EmptyComponent = Object.assign(Empty, {
|
||||
Content: EmptyContent,
|
||||
Icon: EmptyIcon,
|
||||
Title: EmptyTitle,
|
||||
Separator: EmptySeparator,
|
||||
Description: EmptyDescription,
|
||||
});
|
||||
|
||||
export { EmptyComponent as Empty };
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
variant?: "primary" | "outline" | "link";
|
||||
}
|
||||
|
||||
export function IconButton({
|
||||
children,
|
||||
size = "md",
|
||||
className = "",
|
||||
variant = "primary",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const sizeClasses = {
|
||||
sm: "p-2",
|
||||
md: "p-3",
|
||||
lg: "p-4",
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"bg-primary text-black border-2 border-black hover:bg-primary-hover",
|
||||
outline: "bg-transparent text-black border-2 border-black",
|
||||
link: "bg-transparent text-primary hover:underline",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`font-head border-2 border-black shadow-md hover:shadow-xs transition-all ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { InputHTMLAttributes } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({
|
||||
type = "text",
|
||||
placeholder = "Enter text",
|
||||
className = "",
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
className={`px-4 py-2 w-full rounded border-2 shadow-md transition focus:outline-hidden focus:shadow-xs ${
|
||||
props["aria-invalid"]
|
||||
? "border-destructive text-destructive shadow-xs shadow-destructive"
|
||||
: ""
|
||||
} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
export const Label = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) => (
|
||||
<LabelPrimitive.Root className={cn(labelVariants(), className)} {...props} />
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const loaderVariants = cva("flex gap-1", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "[&>div]:bg-primary [&>div]:border-border",
|
||||
secondary:
|
||||
"[&>div]:bg-secondary [&>div]:border-border",
|
||||
outline: "[&>div]:bg-transparent [&>div]:border-border",
|
||||
},
|
||||
size: {
|
||||
sm: "[&>div]:w-2 [&>div]:h-2",
|
||||
md: "[&>div]:w-3 [&>div]:h-3",
|
||||
lg: "[&>div]:w-4 [&>div]:h-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface LoaderProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
||||
VariantProps<typeof loaderVariants> {
|
||||
asChild?: boolean;
|
||||
count?: number; // number of bouncing dots
|
||||
duration?: number; // animation duration in seconds
|
||||
delayStep?: number; // delay in ms
|
||||
}
|
||||
|
||||
const Loader = React.forwardRef<HTMLDivElement, LoaderProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
count = 3,
|
||||
duration = 0.5,
|
||||
delayStep = 100,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(loaderVariants({ variant, size }), className)}
|
||||
ref={ref}
|
||||
role="status"
|
||||
aria-label="Loading..."
|
||||
{...props}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-2 animate-bounce"
|
||||
style={{
|
||||
animationDuration: `${duration}s`,
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${i * delayStep}ms`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Loader.displayName = "Loader";
|
||||
export { Loader };
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Text } from "./Text";
|
||||
import { Input } from "./Input";
|
||||
import { Button } from "./Button";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<div className="max-w-80 p-4 border-2 border-black shadow-md space-y-3">
|
||||
<div className="text-center">
|
||||
<Text as="h2">Login</Text>
|
||||
</div>
|
||||
<form className="flex flex-col gap-5 ">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<label htmlFor="email" className="w-11/12">
|
||||
Email
|
||||
</label>
|
||||
<Input id="email" type="email" placeholder="email" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<label htmlFor="password" className="w-11/12">
|
||||
Password
|
||||
</label>
|
||||
<Input id="password" type="password" placeholder="password" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-4 gap-1">
|
||||
<Button className="rounded-full active:shadow-none active:scale-95">
|
||||
Submit
|
||||
</Button>
|
||||
<div className="px-4 py-2 space-x-1">
|
||||
<span>Don't have an account?</span>
|
||||
<Link href="/" className="text-primary-700 hover:underline">
|
||||
sign-in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import React, { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
const Menu = DropdownMenu.Root;
|
||||
const Trigger = DropdownMenu.Trigger;
|
||||
|
||||
interface IMenuContent
|
||||
extends ComponentPropsWithoutRef<typeof DropdownMenu.Content> {}
|
||||
|
||||
const Content = ({ className, ...props }: IMenuContent) => (
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
className={cn(
|
||||
"bg-white border-2 shadow-md absolute top-2 min-w-20",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenu.Portal>
|
||||
);
|
||||
|
||||
const MenuItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenu.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenu.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative text-black flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors hover:bg-primary focus:bg-primary data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenuItem.displayName = "MenuItem";
|
||||
|
||||
const MenuComponent = Object.assign(Menu, {
|
||||
Trigger,
|
||||
Content,
|
||||
Item: MenuItem,
|
||||
});
|
||||
|
||||
export { MenuComponent as Menu };
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const popoverContentVariants = cva(
|
||||
"z-50 w-72 border-2 bg-background p-4 text-popover-foreground outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
);
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> &
|
||||
VariantProps<typeof popoverContentVariants>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
popoverContentVariants({
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
const PopoverObject = Object.assign(Popover, {
|
||||
Trigger: PopoverTrigger,
|
||||
Content: PopoverContent,
|
||||
Anchor: PopoverAnchor,
|
||||
});
|
||||
|
||||
export { PopoverObject as Popover };
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function ProductCard() {
|
||||
return (
|
||||
<div className="max-w-80 p-4 border-2 border-black">
|
||||
<img
|
||||
alt="product image"
|
||||
src="https://images.unsplash.com/photo-1596998791568-386ef5297c2e?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
/>
|
||||
<div className="mt-4 space-y-4">
|
||||
<h3 className="font-head text-3xl font-medium mb-1">Classic Gameboy</h3>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">$29.99</span>
|
||||
<Button>Add To Cart</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden bg-background border-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,82 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as RadioPrimitive from "@radix-ui/react-radio-group";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const radioVariants = cva("border-border border-2", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "",
|
||||
outline: "",
|
||||
solid: "",
|
||||
},
|
||||
size: {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-5 w-5",
|
||||
lg: "h-6 w-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const radioIndicatorVariants = cva("flex ", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary border-2 border-border",
|
||||
outline: "border-2 border-border",
|
||||
solid: "bg-border",
|
||||
},
|
||||
size: {
|
||||
sm: "h-2 w-2",
|
||||
md: "h-2.5 w-2.5",
|
||||
lg: "h-3.5 w-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface RadioGroupProps
|
||||
extends React.ComponentProps<typeof RadioPrimitive.Root> {}
|
||||
|
||||
export const RadioGroupRoot = ({ className, ...props }: RadioGroupProps) => (
|
||||
<RadioPrimitive.Root className={cn("grid gap-2", className)} {...props} />
|
||||
);
|
||||
|
||||
interface RadioProps
|
||||
extends React.ComponentProps<typeof RadioPrimitive.Item>,
|
||||
VariantProps<typeof radioVariants> {}
|
||||
|
||||
export const RadioItem = ({
|
||||
children,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: RadioProps) => (
|
||||
<RadioPrimitive.Item
|
||||
{...props}
|
||||
className={cn(
|
||||
radioVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RadioPrimitive.Indicator className="flex justify-center items-center">
|
||||
<span className={radioIndicatorVariants({ size, variant })}></span>
|
||||
</RadioPrimitive.Indicator>
|
||||
{children}
|
||||
</RadioPrimitive.Item>
|
||||
);
|
||||
|
||||
const RadioComponent = Object.assign(RadioGroupRoot, {
|
||||
Item: RadioItem,
|
||||
});
|
||||
|
||||
export { RadioComponent as RadioGroup };
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectTrigger = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.SelectTriggerProps) => {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex h-10 rounded min-w-40 items-center shadow-md focus:shadow-xs justify-between border-2 border-input border-border bg-transparent px-4 py-2 placeholder:text-muted-foreground outline-none focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectIcon = SelectPrimitive.Icon;
|
||||
|
||||
const SelectContent = ({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: SelectPrimitive.SelectContentProps) => {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden border border-border bg-background text-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1 text-muted-foreground">
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1 text-muted-foreground">
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectItem = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.SelectItemProps) => (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-none data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground focus:bg-primary focus:text-primary-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4 text-foreground" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
const SelectLabel = SelectPrimitive.Label;
|
||||
const SelectSeparator = SelectPrimitive.Separator;
|
||||
|
||||
const SelectObj = Object.assign(Select, {
|
||||
Trigger: SelectTrigger,
|
||||
Value: SelectValue,
|
||||
Icon: SelectIcon,
|
||||
Content: SelectContent,
|
||||
Group: SelectGroup,
|
||||
Item: SelectItem,
|
||||
Label: SelectLabel,
|
||||
Separator: SelectSeparator,
|
||||
});
|
||||
|
||||
export { SelectObj as Select };
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-3 w-full grow overflow-hidden bg-background border-2">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4.5 w-4.5 border-2 bg-background shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"h-auto w-full p-4 bg-background border group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border flex items-center relative",
|
||||
description:
|
||||
"group-[.toast]:text-muted-foreground ml-2 text-sm font-sans",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground py-1 px-2 bg-background border-border shadow hover:shadow-xs hover:translate-[2px] duration-200 transition-all focus:shadow-none border-2 ml-auto h-fit min-w-fit",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-foreground py-1 px-2 text-sm bg-background border-border shadow hover:shadow-xs hover:translate-[2px] duration-200 transition-all focus:shadow-none border-2 ml-auto h-fit min-w-fit",
|
||||
title: "ml-2 font-sans",
|
||||
closeButton:
|
||||
"absolute bg-background -top-1 -left-1 rounded-full p-0.5",
|
||||
},
|
||||
unstyled: true,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = ({ className, ...props }: SwitchPrimitives.SwitchProps) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer border-2 border-foreground items-center disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 bg-primary border-2 mx-0.5 border-foreground ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-background",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
);
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabListProps,
|
||||
TabPanel,
|
||||
TabPanelProps,
|
||||
TabPanels,
|
||||
TabProps,
|
||||
} from "@headlessui/react";
|
||||
|
||||
const Tabs = TabGroup;
|
||||
const TabsPanels = TabPanels;
|
||||
|
||||
interface ITabsTriggerList extends TabListProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsTriggerList = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ITabsTriggerList) => {
|
||||
return (
|
||||
<TabList className={cn("flex flex-row space-x-2 w-full", className)} {...props}>
|
||||
{children}
|
||||
</TabList>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITabsTrigger extends TabProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsTrigger = ({ children, className, ...props }: ITabsTrigger) => {
|
||||
return (
|
||||
<Tab
|
||||
className={cn(
|
||||
"px-4 py-1 border-2 border-transparent data-selected:border-border data-selected:bg-primary data-selected:text-primary-foreground data-selected:font-semibold focus:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Tab>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITabsContent extends TabPanelProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsContent = ({ children, className, ...props }: ITabsContent) => {
|
||||
return (
|
||||
<TabPanel
|
||||
className={cn("border-2 border-border mt-2 p-4 w-full", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export { Tabs, TabsPanels, TabsTrigger, TabsContent, TabsTriggerList };
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm border-2 shadow-lg", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b bg-primary text-primary-foreground font-head", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-accent text-accent-foreground font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-primary/50 hover:text-primary-foreground data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 md:h-12 px-4 text-left align-middle font-medium text-primary-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-2 md:p-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("my-2 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
const TableObj = Object.assign(Table, {
|
||||
Header: TableHeader,
|
||||
Body: TableBody,
|
||||
Footer: TableFooter,
|
||||
Row: TableRow,
|
||||
Head: TableHead,
|
||||
Cell: TableCell,
|
||||
Caption: TableCaption,
|
||||
})
|
||||
|
||||
export {
|
||||
TableObj as Table,
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface TOCItem {
|
||||
title: string;
|
||||
url: string;
|
||||
level: number;
|
||||
items?: TOCItem[];
|
||||
}
|
||||
|
||||
interface ManualTOCItem {
|
||||
id: string;
|
||||
title: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface TableOfContentsProps {
|
||||
depth?: number;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
data?: ManualTOCItem[];
|
||||
}
|
||||
|
||||
function generateTOCFromDOM(depth: number = 6): TOCItem[] {
|
||||
const headings: NodeListOf<HTMLHeadingElement> = document.querySelectorAll(
|
||||
Array.from({ length: depth }, (_, i) => `h${i + 1}`).join(', ')
|
||||
);
|
||||
|
||||
const items: TOCItem[] = [];
|
||||
const stack: TOCItem[] = [];
|
||||
|
||||
headings.forEach((heading) => {
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
const id = heading.id || heading.textContent?.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '') || '';
|
||||
|
||||
if (!heading.id && id) {
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
const item: TOCItem = {
|
||||
title: heading.textContent || '',
|
||||
url: `#${id}`,
|
||||
level,
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
items.push(item);
|
||||
} else {
|
||||
const parent = stack[stack.length - 1];
|
||||
if (!parent.items) parent.items = [];
|
||||
parent.items.push(item);
|
||||
}
|
||||
|
||||
stack.push(item);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function convertManualDataToTOC(data: ManualTOCItem[]): TOCItem[] {
|
||||
const items: TOCItem[] = [];
|
||||
const stack: TOCItem[] = [];
|
||||
|
||||
data.forEach((item) => {
|
||||
const tocItem: TOCItem = {
|
||||
title: item.title,
|
||||
url: `#${item.id}`,
|
||||
level: item.depth,
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= item.depth) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
items.push(tocItem);
|
||||
} else {
|
||||
const parent = stack[stack.length - 1];
|
||||
if (!parent.items) parent.items = [];
|
||||
parent.items.push(tocItem);
|
||||
}
|
||||
|
||||
stack.push(tocItem);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function renderTOCItems(items: TOCItem[], level = 0, activeId: string | null) {
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className={`space-y-1 ${level > 0 ? 'ml-4 mt-1' : ''}`}>
|
||||
{items.map((item, index) => {
|
||||
const isActive = activeId === item.url.substring(1);
|
||||
return (
|
||||
<li key={index}>
|
||||
<a
|
||||
href={item.url}
|
||||
className={`text-sm max-w-full truncate transition-colors block py-1 border-l-2 pl-2 ${
|
||||
isActive
|
||||
? 'text-accent-foreground border-border bg-accent'
|
||||
: 'border-transparent hover:border-border hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
{item.items && renderTOCItems(item.items, level + 1, activeId)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableOfContents({
|
||||
depth = 2,
|
||||
className = "",
|
||||
children,
|
||||
data,
|
||||
}: TableOfContentsProps) {
|
||||
const [tocItems, setTocItems] = useState<TOCItem[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const items = convertManualDataToTOC(data);
|
||||
setTocItems(items);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = generateTOCFromDOM(depth);
|
||||
setTocItems(items);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const updatedItems = generateTOCFromDOM(depth);
|
||||
setTocItems(updatedItems);
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['id']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [depth, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-20% 0% -35% 0%' }
|
||||
);
|
||||
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
headings.forEach((heading) => observer.observe(heading));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if (tocItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("border-2 shadow-md p-4 rounded w-52 h-60 overflow-y-auto sidebar-scroll", className)}>
|
||||
{children}
|
||||
{renderTOCItems(tocItems, 0, activeId)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ElementType, HTMLAttributes } from "react";
|
||||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const textVariants = cva("font-head", {
|
||||
variants: {
|
||||
as: {
|
||||
p: "font-sans text-base",
|
||||
li: "font-sans text-base",
|
||||
a: "font-sans text-base hover:underline underline-offset-2 decoration-primary",
|
||||
h1: "text-4xl lg:text-5xl font-bold",
|
||||
h2: "text-3xl lg:text-4xl font-semibold",
|
||||
h3: "text-2xl font-medium",
|
||||
h4: "text-xl font-normal",
|
||||
h5: "text-lg font-normal",
|
||||
h6: "text-base font-normal",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
as: "p",
|
||||
},
|
||||
});
|
||||
|
||||
interface TextProps
|
||||
extends Omit<HTMLAttributes<HTMLElement>, "className">,
|
||||
VariantProps<typeof textVariants> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Text = (props: TextProps) => {
|
||||
const { className, as, ...otherProps } = props;
|
||||
const Tag: ElementType = as || "p";
|
||||
|
||||
return (
|
||||
<Tag className={cn(textVariants({ as }), className)} {...otherProps} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Textarea({
|
||||
type = "text",
|
||||
placeholder = "Enter text...",
|
||||
className = "",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<textarea
|
||||
placeholder={placeholder}
|
||||
rows={4}
|
||||
className={cn(
|
||||
"px-4 py-2 w-full border-2 rounded border-input shadow-md transition focus:outline-hidden focus:shadow-xs placeholder:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-transparent hover:bg-muted/70 hover:text-muted-foreground data-[state=on]:bg-muted",
|
||||
outlined:
|
||||
"border-2 border-input bg-transparent hover:bg-accent hover:text-accent-foreground/80 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
solid: "border-2 border-input bg-transparent hover:bg-secondary hover:text-secondary-foreground hover:border-secondary data-[state=on]:bg-secondary data-[state=on]:text-secondary-foreground data-[state=on]:border-secondary",
|
||||
"outline-muted":
|
||||
"border-2 border-input bg-transparent hover:hover:bg-muted/70 hover:hover:text-muted-foreground data-[state=on]:bg-muted",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3 min-w-10",
|
||||
sm: "h-9 px-2.5 min-w-9",
|
||||
lg: "h-11 px-5 min-w-11",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toggleVariants } from "./Toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const tooltipContentVariants = cva(
|
||||
"z-50 overflow-hidden border-2 border-border bg-background px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
primary: "bg-primary text-primary-foreground",
|
||||
solid: "bg-black text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> &
|
||||
VariantProps<typeof tooltipContentVariants>
|
||||
>(({ className, sideOffset = 4, variant, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
tooltipContentVariants({
|
||||
variant,
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
const TooltipObject = Object.assign(Tooltip, {
|
||||
Trigger: TooltipTrigger,
|
||||
Content: TooltipContent,
|
||||
Provider: TooltipProvider,
|
||||
});
|
||||
|
||||
export { TooltipObject as Tooltip };
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Area,
|
||||
AreaChart as RechartsAreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
fillColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
fill?: "gradient" | "solid"
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
fillColors = ["var(--primary)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
fill = "gradient",
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const chartId = React.useId()
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsAreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
{categories.map((category, index) => {
|
||||
const fillColor = fillColors[index] || fillColors[0]
|
||||
const gradientId = `gradient-${chartId}-${category}`
|
||||
return (
|
||||
<linearGradient key={category} id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
{fill === "gradient" ? (
|
||||
<>
|
||||
<stop offset="5%" stopColor={fillColor} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={fillColor} stopOpacity={0} />
|
||||
</>
|
||||
) : (
|
||||
<stop stopColor={fillColor} stopOpacity={0.6} />
|
||||
)}
|
||||
</linearGradient>
|
||||
)
|
||||
})}
|
||||
</defs>
|
||||
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: strokeColors[0] }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
const gradientId = `gradient-${chartId}-${category}`
|
||||
|
||||
return (
|
||||
<Area
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stroke={strokeColor}
|
||||
fill={`url(#${gradientId})`}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</RechartsAreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AreaChart.displayName = "AreaChart"
|
||||
|
||||
export { AreaChart, type AreaChartProps }
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Bar,
|
||||
BarChart as RechartsBarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
fillColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
stacked?: boolean
|
||||
alignment?: "vertical" | "horizontal"
|
||||
className?: string
|
||||
}
|
||||
|
||||
const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
fillColors = ["var(--primary)", "var(--secondary)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
stacked = false,
|
||||
alignment = "vertical",
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsBarChart
|
||||
data={data}
|
||||
layout={alignment === "horizontal" ? "vertical" : undefined}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
{alignment === "horizontal" ? (
|
||||
<>
|
||||
<XAxis
|
||||
type="number"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
width={80}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: strokeColors[0] }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const fillColor = fillColors[index] || fillColors[0]
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={category}
|
||||
dataKey={category}
|
||||
fill={fillColor}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1}
|
||||
stackId={stacked ? "strokeId" : undefined} />
|
||||
)
|
||||
})}
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarChart.displayName = "BarChart"
|
||||
|
||||
export { BarChart, type BarChartProps }
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart as RechartsLineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
strokeWidth?: number
|
||||
dotSize?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const LineChart = React.forwardRef<HTMLDivElement, LineChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
strokeWidth = 2,
|
||||
dotSize = 4,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsLineChart data={data} margin={{ top: 0, right: 30, left: 0, bottom: 0 }}>
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: entry.color }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
|
||||
return (
|
||||
<Line
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
dot={{ r: dotSize, fill: strokeColor }}
|
||||
activeDot={{ r: dotSize + 2, fill: strokeColor }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
LineChart.displayName = "LineChart"
|
||||
|
||||
export { LineChart, type LineChartProps }
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart as RechartsPieChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
} from "recharts"
|
||||
|
||||
interface PieChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
dataKey: string
|
||||
nameKey: string
|
||||
colors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showTooltip?: boolean
|
||||
innerRadius?: number
|
||||
outerRadius?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PieChart = React.forwardRef<HTMLDivElement, PieChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
dataKey,
|
||||
nameKey,
|
||||
colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showTooltip = true,
|
||||
innerRadius = 0,
|
||||
outerRadius = 100,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsPieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey={dataKey}
|
||||
nameKey={nameKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
isAnimationActive={false}
|
||||
className="w-full h-full"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
const data = payload[0]
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{data.name}
|
||||
</span>
|
||||
<span className="font-bold text-foreground">
|
||||
{valueFormatter(data.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PieChart.displayName = "PieChart"
|
||||
|
||||
export { PieChart, type PieChartProps }
|
||||
@@ -0,0 +1,29 @@
|
||||
export * from "./Button";
|
||||
export * from "./Input";
|
||||
export * from "./Textarea";
|
||||
export * from "./Checkbox";
|
||||
export * from "./Radio";
|
||||
export * from "./Select";
|
||||
export * from "./Switch";
|
||||
export * from "./Label";
|
||||
export * from "./Text";
|
||||
export * from "./Accordion";
|
||||
export * from "./Alert";
|
||||
export * from "./Card";
|
||||
export * from "./Avatar";
|
||||
export * from "./Badge";
|
||||
export * from "./Tab";
|
||||
export * from "./Dialog";
|
||||
export * from "./Menu";
|
||||
export * from "./Progress";
|
||||
export * from "./Popover";
|
||||
export * from "./Toggle";
|
||||
export * from "./ToggleGroup";
|
||||
export * from "./Sonner";
|
||||
export * from "./Tooltip";
|
||||
export * from "./Breadcrumb";
|
||||
export * from "./CommandDisplay";
|
||||
export * from "./Command";
|
||||
export * from "./Loader";
|
||||
export * from "./ContextMenu";
|
||||
export * from "./Empty";
|
||||
Reference in New Issue
Block a user