= ({
- category,
- name,
- description,
- selected,
- children,
-}) => {
- const components = {
- a: ({ href, children, ...rest }: JSX.IntrinsicElements['a']) => {
- if (href && href.startsWith('/')) {
- return (
-
- {children}
-
- );
- }
-
- return (
-
- {children}
-
- );
- },
- blockquote: ({ children }: JSX.IntrinsicElements['blockquote']) => (
-
- {children}
-
- ),
- code: ({ children }: JSX.IntrinsicElements['code']) => (
-
- {children}
-
- ),
- h1: ({ children }: JSX.IntrinsicElements['h1']) => (
-
- {children}
-
- ),
- h2: ({ children }: JSX.IntrinsicElements['h2']) => (
-
- {children}
-
- ),
- h3: ({ children }: JSX.IntrinsicElements['h3']) => (
-
- {children}
-
- ),
- p: ({ children }: JSX.IntrinsicElements['p']) => (
-
- {children}
-
- ),
- pre: ({
- children,
- ...rest
- }: JSX.IntrinsicElements['pre'] & Omit
) => {
- const childrenProps = isValidElement(children) ? children.props : undefined;
- const language = childrenProps?.className ? childrenProps.className.substring(9) : undefined;
- const code = typeof childrenProps?.children === 'string' ? childrenProps.children.trim() : '';
-
- return (
-
- {code}
-
- );
- },
- };
-
- return (
- <>
-
-
-
- {/* Note: `pb-6` overrides `pb-4` on small devices. ` `
- has a `mb-6` when displayed on small screens, so the ``margin''
- above/below the article content is symmetrical. We do this instead
- of `py-6` to correctly position ` `, as well as the
- article content on small screens. This positioning issue is not
- present on larger screens so we have a breakpoint to reset it. For
- similar reasons, the `x` padding is set to 0 on small devices is set
- to 0. */}
-
-
-
-
- {children}
-
-
-
-
-
-
- >
- );
-};
-
-export default DesignLayout;
diff --git a/components/layouts/error.tsx b/components/layouts/error.tsx
index 9396b891..195a8dc9 100644
--- a/components/layouts/error.tsx
+++ b/components/layouts/error.tsx
@@ -1,6 +1,3 @@
-import type { FC, ReactNode } from 'react';
-
-import BaseLayout from '@/components/layouts/base';
import ContainerLayout from '@/components/layouts/container';
import { Button } from '@/components/ui';
@@ -10,39 +7,37 @@ import { Button } from '@/components/ui';
type ErrorLayoutProps = {
statusCode?: number;
- children?: ReactNode;
+ children?: React.ReactNode;
};
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
-const ErrorLayout: FC = ({ statusCode, children }) => {
+const ErrorLayout: React.FC = ({ statusCode, children }) => {
const header = statusCode === 404 ? 'PageNotFound()' : `InternalServerError(${statusCode})`;
const message = statusCode === 404 ? '0x924C0FAD' : '0x2145B8F0';
return (
-
-
-
-
- {header}
-
-
-
- FAIL. REASON: {message}
-
-
- Return home
-
- {children}
-
-
+
+
+
+ {header}
+
+
+
+ FAIL. REASON: {message}
+
+
+ Return home
+
+ {children}
+
);
};
diff --git a/components/pages/design/page-nav.tsx b/components/pages/design/page-nav.tsx
deleted file mode 100644
index 4a2d17f6..00000000
--- a/components/pages/design/page-nav.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import Link from 'next/link';
-import { type FC, useMemo } from 'react';
-
-import { ArrowLeft, ArrowRight } from 'lucide-react';
-
-import { DESIGN_COMPONENT_PAGES, DESIGN_PAGES } from '@/lib/constants/site';
-import type { PageSlug } from '@/lib/types/site';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-type DesignPageNavProps = {
- pageSlug?: PageSlug;
-};
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const DesignPageNav: FC = ({ pageSlug }) => {
- const pages = useMemo(() => DESIGN_PAGES.concat(DESIGN_COMPONENT_PAGES), []);
- const prevPage = useMemo(() => {
- const index = pages.findIndex((page) => page.slug === pageSlug);
-
- return index > 0 ? pages[index - 1] : null;
- }, [pages, pageSlug]);
-
- const nextPage = useMemo(() => {
- const index = pages.findIndex((page) => page.slug === pageSlug);
-
- if (index === -1) {
- return null;
- }
-
- return index !== -1 && index < pages.length - 1 ? pages[index + 1] : null;
- }, [pages, pageSlug]);
-
- return (
-
- {prevPage ? (
-
-
-
{prevPage.name}
-
- ) : (
-
- )}
- {nextPage ? (
-
-
{nextPage.name}
-
-
- ) : (
-
- )}
-
- );
-};
-
-export default DesignPageNav;
diff --git a/components/pages/design/toast-button.tsx b/components/pages/design/toast-button.tsx
deleted file mode 100644
index 2f906f0a..00000000
--- a/components/pages/design/toast-button.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { FC, ReactElement } from 'react';
-
-import { Button, useToast } from '@/components/ui';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-type ToastButtonProps = {
- title?: string;
- description: string;
- intent?: 'none' | 'primary' | 'success' | 'fail' | 'warning';
- action?: ReactElement;
-};
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const ToastButton: FC = ({
- title = 'Title',
- description = 'Swipe right to remove.',
- intent,
- action,
- ...rest
-}) => {
- const { toast } = useToast();
- return (
- toast({ title, description, intent, action })}
- {...rest}
- />
- );
-};
-
-ToastButton.displayName = 'ToastButton';
-
-export default ToastButton;
diff --git a/components/pages/home/featured-works/index.tsx b/components/pages/home/featured-works/index.tsx
deleted file mode 100644
index f04855bb..00000000
--- a/components/pages/home/featured-works/index.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { FC } from 'react';
-
-import BitTwiddlingFeature from './works/bit-twiddling';
-import ChessFeature from './works/chess';
-import ColormapRegistryFeature from './works/colormap-registry';
-import CoolContractsFeature from './works/cool-contracts';
-import RunningFeature from './works/running';
-import TxDotCoolFeature from './works/txdotcool';
-import TypingFeature from './works/typing';
-
-import type { MileageLog } from '@/lib/types/running';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-type FeaturedWorksProps = {
- mileageLogs: MileageLog[];
- runningLogs: MileageLog[];
-};
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const FeaturedWorks: FC = ({ mileageLogs, runningLogs }) => {
- return (
-
-
-
-
-
-
-
-
-
- );
-};
-
-FeaturedWorks.displayName = 'FeaturedWorks';
-
-export default FeaturedWorks;
diff --git a/components/pages/home/featured-works/works/running/index.tsx b/components/pages/home/featured-works/works/running/index.tsx
deleted file mode 100644
index d2dede98..00000000
--- a/components/pages/home/featured-works/works/running/index.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { FC, useState } from 'react';
-
-import RunningFeatureDetailBarChart from './bar-chart';
-import RunningFeatureDetailHeatmap from './heatmap';
-import * as Tabs from '@radix-ui/react-tabs';
-import clsx from 'clsx';
-import { ArrowLeftRight, BarChart, Footprints, Grid } from 'lucide-react';
-
-import { LENGTH_UNITS } from '@/lib/constants/units';
-import type { MileageLog } from '@/lib/types/running';
-
-import FeatureDisplay from '@/components/templates/feature-display';
-import { IconButton, Tooltip } from '@/components/ui';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-type RunningFeatureProps = {
- mileageLogs: MileageLog[];
- runningLogs: MileageLog[];
-};
-
-type RunningFeatureDetailProps = RunningFeatureProps;
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const RunningFeature: FC = ({ mileageLogs, runningLogs }) => {
- return (
- }
- >
-
-
- );
-};
-
-const RunningFeatureDetail: FC = ({ mileageLogs, runningLogs }) => {
- const [unitIndex, setUnitIndex] = useState(0);
-
- const tabContentStyles = 'h-full grow overflow-hidden bg-gray-3';
- const tabTriggerStyles = 'data-[state=active]:bg-gray-5 data-[state=active]:hover:bg-gray-5';
-
- const handleUnitChange = () => {
- setUnitIndex((unitIndex + 1) % LENGTH_UNITS.length);
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default RunningFeature;
diff --git a/components/pages/home/featured-works/works/txdotcool.tsx b/components/pages/home/featured-works/works/txdotcool.tsx
deleted file mode 100644
index 1b09697c..00000000
--- a/components/pages/home/featured-works/works/txdotcool.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import { type FC, useEffect, useRef, useState } from 'react';
-
-import { ArrowUp, ExternalLink, MessageCircle } from 'lucide-react';
-import { usePrepareSendTransaction, useSendTransaction, useWaitForTransaction } from 'wagmi';
-
-import { FIVEOUTOFNINE_MESSAGES } from '@/lib/constants/on-chain-messages';
-
-import CategoryTag from '@/components/templates/category-tag';
-import FeatureDisplay from '@/components/templates/feature-display';
-import { useToast } from '@/components/ui';
-import { Button, IconButton } from '@/components/ui';
-
-const TxDotCoolFeature: FC = () => {
- return (
-
- tx.cool
- tx.cool's logo
-
-
- }
- button={
- } newTab>
- Visit
-
- }
- tags={[ ]}
- >
-
-
- );
-};
-
-const TxDotCoolFeatureDetail: FC = () => {
- const [userMessage, setUserInput] = useState('');
-
- const messagesEndRef = useRef(null);
-
- const { toast } = useToast();
-
- const { config } = usePrepareSendTransaction({
- request: {
- chainId: 1,
- to: process.env.NEXT_PUBLIC_FIVEOUTOFNINE_ADDRESS,
- data: `0x${userMessage
- .split('')
- .map((_, i) => userMessage.charCodeAt(i).toString(16))
- .join('')}`,
- },
- });
- const { data, sendTransaction } = useSendTransaction({
- ...config,
- onError(error) {
- toast({
- title: 'Transaction fail',
- description: error.message,
- intent: 'fail',
- });
- },
- onSuccess(data) {
- toast({
- title: 'Transaction sent',
- description: 'Your message has been sent to fiveoutofnine.eth.',
- intent: 'primary',
- action: (
- }
- intent="primary"
- newTab
- >
- View
-
- ),
- });
- },
- });
- const { isLoading } = useWaitForTransaction({
- hash: data?.hash,
- onError(error) {
- toast({
- title: 'Transaction fail',
- description: error.message,
- intent: 'fail',
- action: data ? (
- }
- intent="fail"
- newTab
- >
- View
-
- ) : undefined,
- });
- },
- onSuccess(data) {
- toast({
- title: 'Message sent',
- description: 'Message sent to fiveoutonine.eth!',
- intent: 'success',
- action: data ? (
- }
- intent="success"
- newTab
- >
- View
-
- ) : undefined,
- });
- },
- });
-
- // Scroll messages into view on load.
- useEffect(
- () => messagesEndRef.current?.scrollIntoView({ block: 'nearest', inline: 'nearest' }),
- [],
- );
-
- return (
-
-
- {/* Header */}
-
- {/* Chat */}
-
-
-
- {/* Message input */}
-
-
- );
-};
-
-export default TxDotCoolFeature;
diff --git a/components/pages/home/featured-works/works/typing/timer.tsx b/components/pages/home/featured-works/works/typing/timer.tsx
deleted file mode 100644
index e1ed9483..00000000
--- a/components/pages/home/featured-works/works/typing/timer.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { type FC, useEffect, useState } from 'react';
-
-import clsx from 'clsx';
-
-import FiveoutofnineAvatar from '@/components/common/fiveoutofnine-avatar';
-import { Tooltip } from '@/components/ui';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-type TypingFeatureDetailTimerProps = {
- startTime?: Date;
- endTime?: Date;
- fiveoutofnineTime: number;
-};
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const TypingFeatureDetailTimer: FC = ({
- startTime,
- endTime,
- fiveoutofnineTime,
-}) => {
- const [timePassed, setTimePassed] = useState();
-
- // Timer to update the time passed.
- useEffect(() => {
- if (!startTime) {
- setTimePassed(undefined);
- return;
- }
-
- const interval = setInterval(() => {
- const timePassed = (Date.now() - startTime.getTime()) / 1000;
-
- setTimePassed(timePassed);
- }, 50);
-
- if (endTime) {
- const timePassed = (Date.now() - startTime.getTime()) / 1000;
-
- setTimePassed(timePassed);
- clearInterval(interval);
- return;
- }
-
- return () => clearInterval(interval);
- }, [startTime, endTime]);
-
- return (
-
-
Time
-
{timePassed ? `${timePassed}s` : '–'}
-
-
-
-
- {fiveoutofnineTime}s
-
-
-
-
- );
-};
-
-TypingFeatureDetailTimer.displayName = 'TypingFeatureDetailTimer';
-
-export default TypingFeatureDetailTimer;
diff --git a/components/templates/category-tag.tsx b/components/templates/category-tag.tsx
index 913299e4..789c3b69 100644
--- a/components/templates/category-tag.tsx
+++ b/components/templates/category-tag.tsx
@@ -1,5 +1,3 @@
-import type { FC } from 'react';
-
import { Badge } from '@/components/ui';
import type { BadgeProps } from '@/components/ui/badge/types';
@@ -15,12 +13,12 @@ type CategoryTagProps = Omit & {
// Component
// -----------------------------------------------------------------------------
-const CategoryTag: FC = ({ size, category, ...rest }) => {
+const CategoryTag: React.FC = ({ size, category, ...rest }) => {
const CATEGORY_TO_COLORS: Record = {
NFT: 'orange',
'On-chain': 'warning',
Writing: 'success',
- Web: 'primary',
+ Web: 'info',
};
return (
@@ -32,6 +30,4 @@ const CategoryTag: FC = ({ size, category, ...rest }) => {
);
};
-CategoryTag.displayName = 'CategoryTag';
-
export default CategoryTag;
diff --git a/components/templates/feature-display-minimal.tsx b/components/templates/feature-display-minimal.tsx
index e924d23a..8fd15783 100644
--- a/components/templates/feature-display-minimal.tsx
+++ b/components/templates/feature-display-minimal.tsx
@@ -1,5 +1,3 @@
-import type { FC, ReactNode } from 'react';
-
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
@@ -11,15 +9,15 @@ type FeatureDisplayMinimalProps = {
className?: string;
name: string;
description: string;
- symbol: ReactNode;
- button: ReactNode;
+ symbol: React.ReactNode;
+ button: React.ReactNode;
};
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
-const FeatureDisplayMinimal: FC = ({
+const FeatureDisplayMinimal: React.FC = ({
className,
name,
description,
diff --git a/components/templates/feature-display.tsx b/components/templates/feature-display.tsx
index 0f3dbc59..2e41ceb9 100644
--- a/components/templates/feature-display.tsx
+++ b/components/templates/feature-display.tsx
@@ -1,5 +1,3 @@
-import type { FC, ReactNode } from 'react';
-
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
@@ -11,17 +9,17 @@ type FeatureDisplayProps = {
className?: string;
name: string;
description: string;
- symbol: ReactNode;
- tags?: ReactNode[];
- button?: ReactNode;
- children: ReactNode;
+ symbol: React.ReactNode;
+ tags?: React.ReactNode[];
+ button?: React.ReactNode;
+ children: React.ReactNode;
};
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
-const FeatureDisplay: FC = ({
+const FeatureDisplay: React.FC = ({
className,
name,
description,
diff --git a/components/templates/link-preview.tsx b/components/templates/link-preview.tsx
index 27b7bd87..a8838379 100644
--- a/components/templates/link-preview.tsx
+++ b/components/templates/link-preview.tsx
@@ -1,6 +1,5 @@
import Image from 'next/image';
import Link from 'next/link';
-import type { ComponentPropsWithoutRef, FC } from 'react';
import { HoverCard } from '@/components/ui';
@@ -8,7 +7,7 @@ import { HoverCard } from '@/components/ui';
// Props
// -----------------------------------------------------------------------------
-type LinkPreviewProps = ComponentPropsWithoutRef & {
+type LinkPreviewProps = React.ComponentPropsWithoutRef & {
src: string;
width?: number;
height?: number;
@@ -18,7 +17,7 @@ type LinkPreviewProps = ComponentPropsWithoutRef & {
// Component
// -----------------------------------------------------------------------------
-const LinkPreview: FC = ({
+const LinkPreview: React.FC = ({
src,
width = 256,
height = 128,
diff --git a/components/templates/mdx/a.tsx b/components/templates/mdx/a.tsx
new file mode 100644
index 00000000..bddc2768
--- /dev/null
+++ b/components/templates/mdx/a.tsx
@@ -0,0 +1,28 @@
+import Link from 'next/link';
+
+const A: React.FC = ({ href, children, ...rest }) => {
+ if (href && href.startsWith('/')) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default A;
diff --git a/components/templates/mdx/article.tsx b/components/templates/mdx/article.tsx
new file mode 100644
index 00000000..2d4819bb
--- /dev/null
+++ b/components/templates/mdx/article.tsx
@@ -0,0 +1,21 @@
+import clsx from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+const MDXArticle: React.FC = ({ className, ...rest }) => {
+ return (
+
+ );
+};
+
+export default MDXArticle;
diff --git a/components/templates/mdx/callout/index.tsx b/components/templates/mdx/callout/index.tsx
new file mode 100644
index 00000000..f5d0695b
--- /dev/null
+++ b/components/templates/mdx/callout/index.tsx
@@ -0,0 +1,46 @@
+import { calloutIconContainerVariants, calloutIconVariants, calloutVariants } from './styles';
+import type { CalloutProps } from './types';
+import clsx from 'clsx';
+import { AlertCircle, CheckCircle2, Info, Lightbulb, XCircle } from 'lucide-react';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const Callout: React.FC = ({
+ className,
+ size = 'md',
+ intent = 'info',
+ icon,
+ children,
+}) => {
+ const Icon = icon
+ ? icon
+ : intent === 'info'
+ ? Info
+ : intent === 'success'
+ ? CheckCircle2
+ : intent === 'fail'
+ ? XCircle
+ : intent === 'warning'
+ ? AlertCircle
+ : Lightbulb;
+
+ return (
+
+
+
+
+ {children}
+
+ );
+};
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+Callout.displayName = 'Callout';
+
+export default Callout;
diff --git a/components/templates/mdx/callout/styles.tsx b/components/templates/mdx/callout/styles.tsx
new file mode 100644
index 00000000..b3c2e4af
--- /dev/null
+++ b/components/templates/mdx/callout/styles.tsx
@@ -0,0 +1,58 @@
+import { cva } from 'class-variance-authority';
+
+export const calloutIconContainerVariants = cva([], {
+ variants: {
+ size: {
+ sm: [],
+ md: ['pt-0.5'],
+ },
+ },
+});
+
+export const calloutIconVariants = cva([], {
+ variants: {
+ size: {
+ sm: ['size-4'],
+ md: ['size-5'],
+ },
+ intent: {
+ none: ['text-gray-11'],
+ info: ['text-blue-11'],
+ success: ['text-green-11'],
+ fail: ['text-red-11'],
+ warning: ['text-yellow-11'],
+ orange: ['text-orange-11'],
+ },
+ },
+});
+
+export const calloutVariants = cva(
+ [
+ 'group',
+ 'mdx--callout',
+ 'my-4',
+ 'not-prose',
+ 'border',
+ 'relative',
+ 'flex',
+ 'w-full',
+ 'overflow-hidden',
+ 'p-3',
+ ],
+ {
+ variants: {
+ size: {
+ sm: ['text-xs', 'gap-1.5', 'rounded-md', 'leading-4'],
+ md: ['text-sm', 'gap-2', 'rounded-lg', 'leading-6'],
+ },
+ intent: {
+ none: ['bg-gray-3', 'text-gray-12', 'border-gray-6'],
+ info: ['bg-blue-3', 'text-blue-12', 'border-blue-6'],
+ success: ['bg-green-3', 'text-green-12', 'border-green-6'],
+ fail: ['bg-red-3', 'text-red-12', 'border-red-6'],
+ warning: ['bg-yellow-3', 'text-yellow-12', 'border-yellow-6'],
+ orange: ['bg-orange-3', 'text-orange-12', 'border-orange-6'],
+ },
+ },
+ },
+);
diff --git a/components/templates/mdx/callout/types.tsx b/components/templates/mdx/callout/types.tsx
new file mode 100644
index 00000000..9d8f9d13
--- /dev/null
+++ b/components/templates/mdx/callout/types.tsx
@@ -0,0 +1,17 @@
+import { calloutVariants } from './styles';
+import type { VariantProps } from 'class-variance-authority';
+
+// ---------------------------------------–-------------------------------------
+// Variant props
+// ---------------------------------------–-------------------------------------
+
+export type CalloutVariantProps = VariantProps;
+
+// ---------------------------------------–-------------------------------------
+// Component props
+// ---------------------------------------–-------------------------------------
+
+export type CalloutProps = CalloutVariantProps &
+ JSX.IntrinsicElements['div'] & {
+ icon?: React.FC>;
+ };
diff --git a/components/templates/mdx/code.tsx b/components/templates/mdx/code.tsx
new file mode 100644
index 00000000..da4c9449
--- /dev/null
+++ b/components/templates/mdx/code.tsx
@@ -0,0 +1,10 @@
+const Code: React.FC = (props) => {
+ return (
+
+ );
+};
+
+export default Code;
diff --git a/components/templates/mdx/h1.tsx b/components/templates/mdx/h1.tsx
new file mode 100644
index 00000000..9917e92b
--- /dev/null
+++ b/components/templates/mdx/h1.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+
+import { Link } from 'lucide-react';
+
+import { toast } from '@/components/ui';
+
+const H1: React.FC = ({ children, ...rest }) => {
+ const pathname = usePathname();
+
+ if (typeof children === 'string') {
+ const id = children
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '') // Remove special characters
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
+ .replace(/-+/g, '-') // Replace multiple hyphens with a single hyphen
+ .trim();
+
+ return (
+
+ );
+ }
+
+ return {children} ;
+};
+
+export default H1;
diff --git a/components/templates/mdx/h2.tsx b/components/templates/mdx/h2.tsx
new file mode 100644
index 00000000..d7eb3160
--- /dev/null
+++ b/components/templates/mdx/h2.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+
+import { Link } from 'lucide-react';
+
+import { toast } from '@/components/ui';
+
+const H2: React.FC = ({ children, ...rest }) => {
+ const pathname = usePathname();
+
+ if (typeof children === 'string') {
+ const id = children
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '') // Remove special characters
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
+ .replace(/-+/g, '-') // Replace multiple hyphens with a single hyphen
+ .trim();
+
+ return (
+
+ );
+ }
+
+ return {children} ;
+};
+
+export default H2;
diff --git a/components/templates/mdx/h3.tsx b/components/templates/mdx/h3.tsx
new file mode 100644
index 00000000..89d29afd
--- /dev/null
+++ b/components/templates/mdx/h3.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+
+import { Link } from 'lucide-react';
+
+import { toast } from '@/components/ui';
+
+const H3: React.FC = ({ children, ...rest }) => {
+ const pathname = usePathname();
+
+ if (typeof children === 'string') {
+ const id = children
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '') // Remove special characters
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
+ .replace(/-+/g, '-') // Replace multiple hyphens with a single hyphen
+ .trim();
+
+ return (
+
+ );
+ }
+
+ return {children} ;
+};
+
+export default H3;
diff --git a/components/templates/mdx/index.tsx b/components/templates/mdx/index.tsx
new file mode 100644
index 00000000..4d78bea7
--- /dev/null
+++ b/components/templates/mdx/index.tsx
@@ -0,0 +1,9 @@
+export { default as A } from './a';
+export { default as MDXArticle } from './article';
+export { default as Callout } from './callout';
+export { default as Code } from './code';
+export { default as H1 } from './h1';
+export { default as H2 } from './h2';
+export { default as H3 } from './h3';
+export { default as P } from './p';
+export { default as ToastButton } from './toast-button';
diff --git a/components/templates/mdx/p.tsx b/components/templates/mdx/p.tsx
new file mode 100644
index 00000000..5562d0d2
--- /dev/null
+++ b/components/templates/mdx/p.tsx
@@ -0,0 +1,10 @@
+const P: React.FC = (props) => {
+ return (
+
+ );
+};
+
+export default P;
diff --git a/components/templates/mdx/toast-button.tsx b/components/templates/mdx/toast-button.tsx
new file mode 100644
index 00000000..2917ea95
--- /dev/null
+++ b/components/templates/mdx/toast-button.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { Button, toast } from '@/components/ui';
+import type { ToastProps } from '@/components/ui/toast/types';
+
+// -----------------------------------------------------------------------------
+// Props
+// -----------------------------------------------------------------------------
+
+type ToastButtonProps = ToastProps & {
+ children?: React.ReactNode;
+};
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const ToastButton: React.FC = ({ children, intent, ...toastPropsRest }) => {
+ const toastProps: ToastProps = {
+ intent: intent || 'info',
+ title: toastPropsRest.title || 'Title',
+ description: toastPropsRest.description || 'Some short description.',
+ hasCloseButton: toastPropsRest.hasCloseButton || true,
+ ...toastPropsRest,
+ };
+
+ return (
+ toast({ ...toastProps })}>
+ {children}
+
+ );
+};
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+ToastButton.displayName = 'ToastButton';
+
+export default ToastButton;
diff --git a/components/templates/seo-base.tsx b/components/templates/seo-base.tsx
deleted file mode 100644
index ab32070a..00000000
--- a/components/templates/seo-base.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import Head from 'next/head';
-import type { FC } from 'react';
-
-// -----------------------------------------------------------------------------
-// Props
-// -----------------------------------------------------------------------------
-
-export type SeoBaseProps = {
- title?: string;
- subtitle?: string;
-};
-
-// -----------------------------------------------------------------------------
-// Component
-// -----------------------------------------------------------------------------
-
-const SeoBase: FC = ({ title = '5/9', subtitle }) => {
- const fullTitle = subtitle && subtitle.length > 0 ? `${title} | ${subtitle}` : title;
-
- return (
-
-
-
-
-
-
- {fullTitle}
-
-
-
- );
-};
-
-export default SeoBase;
diff --git a/components/ui/accordion/index.tsx b/components/ui/accordion/index.tsx
new file mode 100644
index 00000000..54d813d2
--- /dev/null
+++ b/components/ui/accordion/index.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import {
+ accordionContentContainerStyles,
+ accordionContentVariants,
+ accordionItemVariants,
+ accordionTriggerChevronStyles,
+ accordionTriggerVariants,
+} from './styles';
+import type {
+ AccordionComposition,
+ AccordionContentProps,
+ AccordionItemProps,
+ AccordionTriggerProps,
+} from './types';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import clsx from 'clsx';
+import { ChevronRight } from 'lucide-react';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const AccordionContent = forwardRef<
+ React.ElementRef,
+ AccordionContentProps
+>(({ className, variant = 'normal', children, ...props }, ref) => (
+
+
+ {children}
+
+
+));
+
+const AccordionItem = forwardRef<
+ React.ElementRef,
+ AccordionItemProps
+>(({ className, variant = 'normal', ...props }, ref) => (
+
+));
+
+const AccordionRoot = AccordionPrimitive.Root;
+
+const AccordionTrigger = forwardRef<
+ React.ElementRef,
+ AccordionTriggerProps
+>(({ className, variant = 'normal', children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+));
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+AccordionItem.displayName = AccordionPrimitive.Item.displayName;
+AccordionRoot.displayName = AccordionPrimitive.Root.displayName;
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const Accordion: AccordionComposition = {
+ Content: AccordionContent,
+ Item: AccordionItem,
+ Root: AccordionRoot,
+ Trigger: AccordionTrigger,
+};
+
+export default Accordion;
diff --git a/components/ui/accordion/styles.tsx b/components/ui/accordion/styles.tsx
new file mode 100644
index 00000000..5426fe20
--- /dev/null
+++ b/components/ui/accordion/styles.tsx
@@ -0,0 +1,88 @@
+import { cva } from 'class-variance-authority';
+
+export const accordionContentContainerStyles = [
+ 'text-sm',
+ 'text-gray-11',
+ 'overflow-hidden',
+ 'data-[state=closed]:animate-accordion-up',
+ 'data-[state=open]:animate-accordion-down',
+];
+
+export const accordionContentVariants = cva([], {
+ variants: {
+ variant: {
+ container: [
+ 'p-3',
+ 'border',
+ 'border-b-0',
+ 'border-gray-7',
+ 'group-last:border-b',
+ 'group-last:rounded-b',
+ ],
+ normal: ['pb-3', 'pt-0'],
+ },
+ },
+});
+
+export const accordionItemVariants = cva(['group'], {
+ variants: {
+ variant: {
+ container: ['-space-y-[1px]'],
+ normal: ['border-b', 'border-gray-6'],
+ },
+ },
+});
+
+export const accordionTriggerChevronStyles = [
+ 'h-4',
+ 'w-4',
+ 'shrink-0',
+ 'text-gray-11',
+ 'transition-transform',
+ 'duration-200',
+ 'group-data-[state=open]:rotate-90',
+];
+
+export const accordionTriggerVariants = cva(
+ [
+ 'group',
+ 'flex',
+ 'flex-1',
+ 'items-center',
+ 'justify-between',
+ 'text-sm',
+ 'font-medium',
+ 'focus-visible:ring-blue-9',
+ 'focus-visible:rounded-sm',
+ ],
+ {
+ variants: {
+ variant: {
+ container: [
+ 'text-gray-11',
+ 'h-8',
+ 'pl-3',
+ 'pr-1.5',
+ 'border',
+ 'border-b-0',
+ 'border-gray-7',
+ 'group-first:rounded-t',
+ 'group-last:border-b',
+ 'group-last:rounded-b',
+ 'group-only:border-y',
+ 'transition-colors',
+ 'active:bg-gray-3',
+ 'hover:text-gray-12',
+ 'hover:border-gray-8',
+ 'hover:z-10',
+ 'focus:z-20',
+ 'data-[state=open]:text-gray-12',
+ 'group-last:data-[state=open]:rounded-b-none',
+ 'group-last:data-[state=open]:focus-visible:rounded-b-sm',
+ 'data-[state=open]:focus-visible:rounded-b-sm',
+ ],
+ normal: ['text-gray-12', 'py-3', 'hover:underline'],
+ },
+ },
+ },
+);
diff --git a/components/ui/accordion/types.tsx b/components/ui/accordion/types.tsx
new file mode 100644
index 00000000..1cbb1cc6
--- /dev/null
+++ b/components/ui/accordion/types.tsx
@@ -0,0 +1,47 @@
+import {
+ accordionContentVariants,
+ accordionItemVariants,
+ accordionTriggerVariants,
+} from './styles';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import type { VariantProps } from 'class-variance-authority';
+
+// ---------------------------------------–-------------------------------------
+// Component props
+// ---------------------------------------–-------------------------------------
+
+type AccordionContentVariantProps = VariantProps;
+
+type AccordionItemVariantProps = VariantProps;
+
+type AccordionTriggerVariantProps = VariantProps;
+
+// ---------------------------------------–-------------------------------------
+// Component props
+// ---------------------------------------–-------------------------------------
+
+export type AccordionContentProps = React.ComponentPropsWithoutRef<
+ typeof AccordionPrimitive.Content
+> &
+ AccordionContentVariantProps;
+
+export type AccordionItemProps = React.ComponentPropsWithoutRef &
+ AccordionItemVariantProps;
+
+export type AccordionRootProps = React.ComponentPropsWithoutRef;
+
+export type AccordionTriggerProps = React.ComponentPropsWithoutRef<
+ typeof AccordionPrimitive.Trigger
+> &
+ AccordionTriggerVariantProps;
+
+// ---------------------------------------–-------------------------------------
+// Composition
+// ---------------------------------------–-------------------------------------
+
+export type AccordionComposition = {
+ Content: React.FC;
+ Item: React.FC;
+ Root: React.FC;
+ Trigger: React.FC;
+};
diff --git a/components/ui/avatar/index.tsx b/components/ui/avatar/index.tsx
new file mode 100644
index 00000000..65a69552
--- /dev/null
+++ b/components/ui/avatar/index.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import { avatarFallbackStyles, avatarImageStyles, avatarStyles } from './styles';
+import type {
+ AvatarComposition,
+ AvatarFallbackProps,
+ AvatarImageProps,
+ AvatarRootProps,
+} from './types';
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+import clsx from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const AvatarFallback = forwardRef<
+ React.ElementRef,
+ AvatarFallbackProps
+>(({ className, ...props }, ref) => (
+
+));
+
+const AvatarImage = forwardRef, AvatarImageProps>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+
+const AvatarRoot = forwardRef, AvatarRootProps>(
+ ({ className, size = 40, style, ...props }, ref) => (
+
+ ),
+);
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+AvatarRoot.displayName = AvatarPrimitive.Root.displayName;
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+const Avatar: AvatarComposition = {
+ Fallback: AvatarFallback,
+ Image: AvatarImage,
+ Root: AvatarRoot,
+};
+
+export default Avatar;
diff --git a/components/ui/avatar/styles.tsx b/components/ui/avatar/styles.tsx
new file mode 100644
index 00000000..e59f2ed0
--- /dev/null
+++ b/components/ui/avatar/styles.tsx
@@ -0,0 +1,23 @@
+export const avatarFallbackStyles = [
+ 'flex',
+ 'h-full',
+ 'w-full',
+ 'items-center',
+ 'justify-center',
+ 'rounded-full',
+ 'bg-gray-5',
+ 'text-gray-11',
+ 'text-sm',
+];
+
+export const avatarImageStyles = ['aspect-square', 'h-full', 'w-full', 'bg-gray-9', 'object-cover'];
+
+export const avatarStyles = [
+ 'relative',
+ 'flex',
+ 'shrink-0',
+ 'overflow-hidden',
+ 'rounded-full',
+ 'border',
+ 'border-gray-6',
+];
diff --git a/components/ui/avatar/types.tsx b/components/ui/avatar/types.tsx
new file mode 100644
index 00000000..75756439
--- /dev/null
+++ b/components/ui/avatar/types.tsx
@@ -0,0 +1,23 @@
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+
+// ---------------------------------------–-------------------------------------
+// Component props
+// ---------------------------------------–-------------------------------------
+
+export type AvatarFallbackProps = React.ComponentPropsWithoutRef;
+
+export type AvatarImageProps = React.ComponentPropsWithoutRef;
+
+export type AvatarRootProps = React.ComponentPropsWithoutRef & {
+ size?: number;
+};
+
+// ---------------------------------------–-------------------------------------
+// Composition
+// ---------------------------------------–-------------------------------------
+
+export type AvatarComposition = {
+ Fallback: React.FC;
+ Image: React.FC;
+ Root: React.FC;
+};
diff --git a/components/ui/badge/index.tsx b/components/ui/badge/index.tsx
index 5b4135b4..dc0bb2d0 100644
--- a/components/ui/badge/index.tsx
+++ b/components/ui/badge/index.tsx
@@ -1,28 +1,34 @@
-import type { FC } from 'react';
-
import { badgeVariants } from './styles';
import type { BadgeProps } from './types';
-import { cx } from 'class-variance-authority';
+import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
-const Badge: FC = ({
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const Badge: React.FC = ({
className,
size = 'md',
variant = 'primary',
intent = 'none',
+ type = 'text',
children,
...rest
-}) => {
- return (
-
- {children}
-
- );
-};
+}) => (
+
+ {children}
+
+);
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
Badge.displayName = 'Badge';
diff --git a/components/ui/badge/styles.tsx b/components/ui/badge/styles.tsx
index fb337c46..b5b48630 100644
--- a/components/ui/badge/styles.tsx
+++ b/components/ui/badge/styles.tsx
@@ -1,61 +1,108 @@
import { cva } from 'class-variance-authority';
export const badgeVariants = cva(
- ['rounded-full', 'w-fit', 'flex', 'justify-center', 'items-center', 'font-medium'],
+ [
+ 'rounded-full',
+ 'justify-center',
+ 'items-center',
+ 'font-medium',
+ 'flex',
+ 'min-w-fit',
+ 'w-fit',
+ 'text-nowrap',
+ 'whitespace-nowrap',
+ 'overflow-hidden',
+ 'text-ellipsis',
+ ],
{
variants: {
size: {
- sm: ['px-2', 'h-4', 'text-xs'],
- md: ['px-2.5', 'h-5', 'text-sm'],
- lg: ['px-3', 'h-6', 'text-base'],
+ sm: ['min-w-[1rem]', 'h-4', 'text-xs'],
+ md: ['min-w-[1.25rem]', 'h-5', 'text-sm'],
+ lg: ['min-w-[1.5rem]', 'h-6', 'text-base'],
},
variant: {
primary: [],
secondary: [],
+ outline: ['border'],
},
intent: {
none: [
'data-[variant=primary]:bg-gray-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=secondary]:bg-gray-3',
'data-[variant=secondary]:text-gray-11',
+ 'data-[variant=outline]:bg-gray-3',
+ 'data-[variant=outline]:text-gray-11',
+ 'data-[variant=outline]:border-gray-6',
],
- primary: [
+ info: [
'data-[variant=primary]:bg-blue-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=secondary]:bg-blue-3',
- 'data-[variant=secondary]:text-blue-9',
+ 'data-[variant=secondary]:text-blue-11',
+ 'data-[variant=outline]:bg-blue-3',
+ 'data-[variant=outline]:text-blue-11',
+ 'data-[variant=outline]:border-blue-6',
],
success: [
'data-[variant=primary]:bg-green-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=secondary]:bg-green-3',
- 'data-[variant=secondary]:text-green-9',
+ 'data-[variant=secondary]:text-green-11',
+ 'data-[variant=outline]:bg-green-3',
+ 'data-[variant=outline]:text-green-11',
+ 'data-[variant=outline]:border-green-6',
],
fail: [
'data-[variant=primary]:bg-red-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=secondary]:bg-red-3',
- 'data-[variant=secondary]:text-red-9',
+ 'data-[variant=secondary]:text-red-11',
+ 'data-[variant=outline]:bg-red-3',
+ 'data-[variant=outline]:text-red-11',
+ 'data-[variant=outline]:border-red-6',
],
warning: [
'data-[variant=primary]:bg-yellow-9',
- 'data-[variant=primary]:dark:text-gray-1',
- 'data-[variant=primary]:text-gray-12',
+ 'data-[variant=primary]:text-black',
'data-[variant=secondary]:bg-yellow-3',
- 'data-[variant=secondary]:text-yellow-9',
+ 'data-[variant=secondary]:text-yellow-11',
+ 'data-[variant=outline]:bg-yellow-3',
+ 'data-[variant=outline]:text-yellow-11',
+ 'data-[variant=outline]:border-yellow-6',
],
orange: [
'data-[variant=primary]:bg-orange-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=secondary]:bg-orange-3',
- 'data-[variant=secondary]:text-orange-9',
+ 'data-[variant=secondary]:text-orange-11',
+ 'data-[variant=outline]:bg-orange-3',
+ 'data-[variant=outline]:text-orange-11',
+ 'data-[variant=outline]:border-orange-6',
],
+ black: [
+ 'data-[variant=primary]:bg-black',
+ 'data-[variant=primary]:text-white',
+ 'data-[variant=outline]:bg-black',
+ 'data-[variant=outline]:text-white',
+ 'data-[variant=outline]:border-white',
+ ],
+ white: [
+ 'data-[variant=primary]:bg-white',
+ 'data-[variant=primary]:text-black',
+ 'data-[variant=outline]:bg-white',
+ 'data-[variant=outline]:text-black',
+ 'data-[variant=outline]:border-black',
+ ],
+ },
+ type: {
+ number: [
+ 'data-[length-one=false]:data-[size=sm]:px-1.5',
+ 'data-[length-one=false]:data-[size=md]:px-2',
+ 'data-[length-one=false]:data-[size=lg]:px-2.5',
+ ],
+ text: ['data-[size=sm]:px-2', 'data-[size=md]:px-2.5', 'data-[size=lg]:px-3'],
},
},
},
diff --git a/components/ui/badge/types.tsx b/components/ui/badge/types.tsx
index 83754521..32c654c8 100644
--- a/components/ui/badge/types.tsx
+++ b/components/ui/badge/types.tsx
@@ -11,4 +11,15 @@ export type BadgeVariantProps = VariantProps;
// Component props
// -----------------------------------------------------------------------------
-export type BadgeProps = JSX.IntrinsicElements['div'] & BadgeVariantProps;
+export type BadgeProps = JSX.IntrinsicElements['span'] &
+ Omit &
+ (
+ | {
+ variant?: 'primary' | 'outline';
+ intent: BadgeVariantProps['intent'];
+ }
+ | {
+ variant?: 'secondary';
+ intent?: Exclude;
+ }
+ );
diff --git a/components/ui/button/index.tsx b/components/ui/button/index.tsx
index dec24897..bd5a44f4 100644
--- a/components/ui/button/index.tsx
+++ b/components/ui/button/index.tsx
@@ -1,10 +1,10 @@
import Link from 'next/link';
-import { type ForwardedRef, forwardRef } from 'react';
+import { forwardRef } from 'react';
-import { buttonIconVariants, buttonVariants } from './styles';
-import type { ButtonProps } from './types';
+import { buttonGroupStyles, buttonIconVariants, buttonVariants } from './styles';
+import type { ButtonGroupProps, ButtonProps } from './types';
import { Slot } from '@radix-ui/react-slot';
-import { cx } from 'class-variance-authority';
+import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
const Button = forwardRef(
@@ -18,15 +18,15 @@ const Button = forwardRef(
href,
leftIcon,
rightIcon,
- newTab,
+ newTab = false,
children,
...rest
}: ButtonProps,
- ref: ForwardedRef,
+ ref: React.ForwardedRef,
) => {
const props = {
className: twMerge(
- cx(
+ clsx(
buttonVariants({ size, variant, intent: !disabled ? intent : undefined, disabled }),
className,
),
@@ -50,11 +50,15 @@ const Button = forwardRef(
{leftIcon && variant !== 'text' ? (
- {leftIcon}
+
+ {leftIcon}
+
) : null}
- {children}
+ {children}
{rightIcon && variant !== 'text' ? (
- {rightIcon}
+
+ {rightIcon}
+
) : null}
@@ -64,17 +68,35 @@ const Button = forwardRef(
return (
{leftIcon && variant !== 'text' ? (
- {leftIcon}
+
+ {leftIcon}
+
) : null}
- {children}
+ {children}
{rightIcon && variant !== 'text' ? (
- {rightIcon}
+
+ {rightIcon}
+
) : null}
);
},
);
+const ButtonGroup: React.FC = ({ className, children, ...rest }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
Button.displayName = 'Button';
+ButtonGroup.displayName = 'ButtonGroup';
+export { ButtonGroup };
export default Button;
diff --git a/components/ui/button/styles.tsx b/components/ui/button/styles.tsx
index 1177b2a2..4ef17569 100644
--- a/components/ui/button/styles.tsx
+++ b/components/ui/button/styles.tsx
@@ -1,5 +1,9 @@
import { cva } from 'class-variance-authority';
+// -----------------------------------------------------------------------------
+// Button
+// -----------------------------------------------------------------------------
+
export const buttonIconVariants = cva(['flex', 'items-center', 'justify-center'], {
variants: {
size: {
@@ -13,38 +17,90 @@ export const buttonIconVariants = cva(['flex', 'items-center', 'justify-center']
export const buttonVariants = cva(
[
- 'rounded',
'w-fit',
+ 'whitespace-nowrap',
+ 'font-medium',
+ 'transition-colors',
'flex',
'justify-center',
'items-center',
- 'font-medium',
'no-underline',
- 'transition-colors',
- 'focus-visible:outline-none',
+ 'outline-none',
+ 'hover:z-10',
+ 'focus:z-20',
+ 'focus:outline-none',
'focus-visible:ring-2',
'focus-visible:ring-blue-9',
],
{
variants: {
size: {
- sm: ['px-2', 'h-6', 'text-xs', 'space-x-1'],
- md: ['px-3', 'h-8', 'text-sm', 'space-x-1.5'],
- lg: ['px-3', 'h-10', 'text-sm', 'space-x-1.5'],
- xl: ['px-4', 'h-12', 'text-base', 'space-x-2'],
+ sm: [
+ 'px-2',
+ 'h-6',
+ 'text-xs',
+ 'space-x-1',
+ 'rounded',
+ 'group-[.button--group]:rounded-none',
+ 'group-[.button--group]:first:rounded-l',
+ 'group-[.button--group]:last:rounded-r',
+ 'group-[.button--group]:focus-visible:rounded-sm',
+ 'group-[.button--group]:first:focus-visible:rounded-l',
+ 'group-[.button--group]:last:focus-visible:rounded-r',
+ ],
+ md: [
+ 'px-3',
+ 'h-8',
+ 'text-sm',
+ 'space-x-1.5',
+ 'rounded',
+ 'group-[.button--group]:rounded-none',
+ 'group-[.button--group]:first:rounded-l',
+ 'group-[.button--group]:last:rounded-r',
+ 'group-[.button--group]:focus-visible:rounded-sm',
+ 'group-[.button--group]:first:focus-visible:rounded-l',
+ 'group-[.button--group]:last:focus-visible:rounded-r',
+ ],
+ lg: [
+ 'px-3',
+ 'h-10',
+ 'text-base',
+ 'space-x-1.5',
+ 'rounded',
+ 'group-[.button--group]:rounded-none',
+ 'group-[.button--group]:first:rounded-l',
+ 'group-[.button--group]:last:rounded-r',
+ 'group-[.button--group]:focus-visible:rounded-sm',
+ 'group-[.button--group]:first:focus-visible:rounded-l',
+ 'group-[.button--group]:last:focus-visible:rounded-r',
+ ],
+ xl: [
+ 'px-4',
+ 'h-12',
+ 'text-lg',
+ 'space-x-2',
+ 'rounded-md',
+ 'group-[.button--group]:rounded-none',
+ 'group-[.button--group]:first:rounded-l-md',
+ 'group-[.button--group]:last:rounded-r-md',
+ 'group-[.button--group]:focus-visible:rounded-sm',
+ 'group-[.button--group]:first:focus-visible:rounded-l-md',
+ 'group-[.button--group]:last:focus-visible:rounded-r-md',
+ ],
},
variant: {
primary: ['border'],
secondary: [],
- outline: ['border'],
- ghost: [],
- text: ['hover:underline'],
+ outline: ['border', 'bg-transparent'],
+ ghost: ['bg-transparent'],
+ text: ['bg-transparent', 'hover:underline'],
+ solid: ['border'],
},
intent: {
none: [
// Primary
'data-[variant=primary]:bg-gray-4',
- 'data-[variant=primary]:text-gray-12',
+ 'data-[variant=primary]:text-white',
'data-[variant=primary]:border-gray-7',
'data-[variant=primary]:hover:border-gray-8',
'data-[variant=primary]:active:bg-gray-5',
@@ -65,11 +121,10 @@ export const buttonVariants = cva(
// Text
'data-[variant=text]:text-gray-11',
],
- primary: [
+ info: [
// Primary
'data-[variant=primary]:bg-blue-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=primary]:border-blue-7',
'data-[variant=primary]:hover:border-blue-8',
'data-[variant=primary]:active:bg-blue-10',
@@ -79,22 +134,21 @@ export const buttonVariants = cva(
'data-[variant=secondary]:hover:bg-blue-5',
'data-[variant=secondary]:active:bg-blue-6',
// Outline
- 'data-[variant=outline]:text-blue-9',
+ 'data-[variant=outline]:text-blue-11',
'data-[variant=outline]:border-blue-7',
'data-[variant=outline]:hover:border-blue-8',
'data-[variant=outline]:active:bg-blue-3',
// Ghost
- 'data-[variant=ghost]:text-blue-9',
+ 'data-[variant=ghost]:text-blue-11',
'data-[variant=ghost]:hover:bg-blue-4',
'data-[variant=ghost]:active:bg-blue-5',
// Text
- 'data-[variant=text]:text-blue-9',
+ 'data-[variant=text]:text-blue-11',
],
success: [
// Primary
'data-[variant=primary]:bg-green-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=primary]:border-green-7',
'data-[variant=primary]:hover:border-green-8',
'data-[variant=primary]:active:bg-green-10',
@@ -104,22 +158,21 @@ export const buttonVariants = cva(
'data-[variant=secondary]:hover:bg-green-5',
'data-[variant=secondary]:active:bg-green-6',
// Outline
- 'data-[variant=outline]:text-green-9',
+ 'data-[variant=outline]:text-green-11',
'data-[variant=outline]:border-green-7',
'data-[variant=outline]:hover:border-green-8',
'data-[variant=outline]:active:bg-green-3',
// Ghost
- 'data-[variant=ghost]:text-green-9',
+ 'data-[variant=ghost]:text-green-11',
'data-[variant=ghost]:hover:bg-green-4',
'data-[variant=ghost]:active:bg-green-5',
// Text
- 'data-[variant=text]:text-green-9',
+ 'data-[variant=text]:text-green-11',
],
fail: [
// Primary
'data-[variant=primary]:bg-red-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=primary]:border-red-7',
'data-[variant=primary]:hover:border-red-8',
'data-[variant=primary]:active:bg-red-10',
@@ -129,22 +182,21 @@ export const buttonVariants = cva(
'data-[variant=secondary]:hover:bg-red-5',
'data-[variant=secondary]:active:bg-red-6',
// Outline
- 'data-[variant=outline]:text-red-9',
+ 'data-[variant=outline]:text-red-11',
'data-[variant=outline]:border-red-7',
'data-[variant=outline]:hover:border-red-8',
'data-[variant=outline]:active:bg-red-3',
// Ghost
- 'data-[variant=ghost]:text-red-9',
+ 'data-[variant=ghost]:text-red-11',
'data-[variant=ghost]:hover:bg-red-4',
'data-[variant=ghost]:active:bg-red-5',
// Text
- 'data-[variant=text]:text-red-9',
+ 'data-[variant=text]:text-red-11',
],
warning: [
// Primary
'data-[variant=primary]:bg-yellow-9',
- 'data-[variant=primary]:dark:text-gray-1',
- 'data-[variant=primary]:text-gray-12',
+ 'data-[variant=primary]:text-black',
'data-[variant=primary]:border-yellow-7',
'data-[variant=primary]:hover:border-yellow-8',
'data-[variant=primary]:active:bg-yellow-10',
@@ -154,22 +206,21 @@ export const buttonVariants = cva(
'data-[variant=secondary]:hover:bg-yellow-5',
'data-[variant=secondary]:active:bg-yellow-6',
// Outline
- 'data-[variant=outline]:text-yellow-9',
+ 'data-[variant=outline]:text-yellow-11',
'data-[variant=outline]:border-yellow-7',
'data-[variant=outline]:hover:border-yellow-8',
'data-[variant=outline]:active:bg-yellow-3',
// Ghost
- 'data-[variant=ghost]:text-yellow-9',
+ 'data-[variant=ghost]:text-yellow-11',
'data-[variant=ghost]:hover:bg-yellow-4',
'data-[variant=ghost]:active:bg-yellow-5',
// Text
- 'data-[variant=text]:text-yellow-9',
+ 'data-[variant=text]:text-yellow-11',
],
orange: [
// Primary
'data-[variant=primary]:bg-orange-9',
- 'data-[variant=primary]:dark:text-gray-12',
- 'data-[variant=primary]:text-gray-1',
+ 'data-[variant=primary]:text-white',
'data-[variant=primary]:border-orange-7',
'data-[variant=primary]:hover:border-orange-8',
'data-[variant=primary]:active:bg-orange-10',
@@ -179,20 +230,36 @@ export const buttonVariants = cva(
'data-[variant=secondary]:hover:bg-orange-5',
'data-[variant=secondary]:active:bg-orange-6',
// Outline
- 'data-[variant=outline]:text-orange-9',
+ 'data-[variant=outline]:text-orange-11',
'data-[variant=outline]:border-orange-7',
'data-[variant=outline]:hover:border-orange-8',
'data-[variant=outline]:active:bg-orange-3',
// Ghost
- 'data-[variant=ghost]:text-orange-9',
+ 'data-[variant=ghost]:text-orange-11',
'data-[variant=ghost]:hover:bg-orange-4',
'data-[variant=ghost]:active:bg-orange-5',
// Text
- 'data-[variant=text]:text-orange-9',
+ 'data-[variant=text]:text-orange-11',
+ ],
+ black: [
+ // Solid
+ 'data-[variant=solid]:bg-black',
+ 'data-[variant=solid]:text-white',
+ 'data-[variant=solid]:border-black',
+ 'data-[variant=solid]:hover:text-black',
+ 'data-[variant=solid]:hover:bg-transparent',
+ ],
+ white: [
+ // Solid
+ 'data-[variant=solid]:bg-white',
+ 'data-[variant=solid]:text-black',
+ 'data-[variant=solid]:border-white',
+ 'data-[variant=solid]:hover:text-white',
+ 'data-[variant=solid]:hover:bg-transparent',
],
},
disabled: {
- true: 'aria-disabled cursor-not-allowed',
+ true: 'aria-disabled pointer-events-none',
false: '',
},
},
@@ -202,6 +269,13 @@ export const buttonVariants = cva(
{ variant: 'outline', disabled: true, className: 'text-gray-7 border-gray-7' },
{ variant: 'ghost', disabled: true, className: 'bg-gray-9 text-gray-11' },
{ variant: 'text', disabled: true, className: 'bg-gray-9 text-gray-11' },
+ { variant: 'solid', disabled: true, className: 'border-gray-9 bg-gray-9 text-gray-11' },
],
},
);
+
+// -----------------------------------------------------------------------------
+// Button Group
+// -----------------------------------------------------------------------------
+
+export const buttonGroupStyles = ['button--group', 'group', 'flex', '-space-x-[1px]'];
diff --git a/components/ui/button/types.tsx b/components/ui/button/types.tsx
index 665a3d6f..90497ed1 100644
--- a/components/ui/button/types.tsx
+++ b/components/ui/button/types.tsx
@@ -1,5 +1,3 @@
-import type { ReactNode } from 'react';
-
import { buttonVariants } from './styles';
import type { VariantProps } from 'class-variance-authority';
@@ -13,10 +11,22 @@ export type ButtonVariantProps = VariantProps;
// Component props
// -----------------------------------------------------------------------------
+export type ButtonGroupProps = JSX.IntrinsicElements['div'];
+
export type ButtonProps = JSX.IntrinsicElements['button'] &
- ButtonVariantProps & {
+ Omit &
+ (
+ | {
+ variant?: 'solid';
+ intent?: 'black' | 'white';
+ }
+ | {
+ variant?: Exclude;
+ intent?: Exclude;
+ }
+ ) & {
href?: string;
- leftIcon?: ReactNode;
- rightIcon?: ReactNode;
+ leftIcon?: React.ReactNode;
+ rightIcon?: React.ReactNode;
newTab?: boolean;
};
diff --git a/components/ui/checkbox/index.tsx b/components/ui/checkbox/index.tsx
new file mode 100644
index 00000000..53575e20
--- /dev/null
+++ b/components/ui/checkbox/index.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import { checkboxIndicatorIconVariants, checkboxIndicatorStyles, checkboxVariants } from './styles';
+import type { CheckboxProps } from './types';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import clsx from 'clsx';
+import { Check } from 'lucide-react';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const Checkbox = forwardRef, CheckboxProps>(
+ ({ className, size = 'md', ...props }, ref) => (
+
+
+
+
+
+ ),
+);
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export default Checkbox;
diff --git a/components/ui/checkbox/styles.tsx b/components/ui/checkbox/styles.tsx
new file mode 100644
index 00000000..77d147d7
--- /dev/null
+++ b/components/ui/checkbox/styles.tsx
@@ -0,0 +1,51 @@
+import { cva } from 'class-variance-authority';
+
+export const checkboxIndicatorIconVariants = cva([], {
+ variants: {
+ size: {
+ sm: ['size-2'],
+ md: ['size-2.5'],
+ lg: ['size-3'],
+ },
+ },
+});
+
+export const checkboxIndicatorStyles = [
+ 'flex',
+ 'items-center',
+ 'justify-center',
+ 'animate-in',
+ 'zoom-in',
+];
+
+export const checkboxVariants = cva(
+ [
+ 'border-gray-7',
+ 'shrink-0',
+ 'border',
+ 'transition-colors',
+ 'rounded-sm',
+ 'active:bg-gray-3',
+ 'disabled:cursor-not-allowed',
+ 'disabled:text-gray-11',
+ 'disabled:bg-gray-5',
+ 'disabled:active:bg-gray-5',
+ 'disabled:hover:border-gray-7',
+ 'hover:border-gray-8',
+ 'focus-visible:ring-2',
+ 'focus-visible:ring-blue-9',
+ 'data-[state=checked]:bg-blue-9',
+ 'data-[state=checked]:border-blue-9',
+ 'data-[state=checked]:text-white',
+ 'data-[state=checked]:hover:bg-blue-10',
+ ],
+ {
+ variants: {
+ size: {
+ sm: ['size-3'],
+ md: ['size-3.5'],
+ lg: ['size-4'],
+ },
+ },
+ },
+);
diff --git a/components/ui/checkbox/types.tsx b/components/ui/checkbox/types.tsx
new file mode 100644
index 00000000..45f6af16
--- /dev/null
+++ b/components/ui/checkbox/types.tsx
@@ -0,0 +1,16 @@
+import { checkboxVariants } from './styles';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import type { VariantProps } from 'class-variance-authority';
+
+// ---------------------------------------–-------------------------------------
+// Variant props
+// ---------------------------------------–-------------------------------------
+
+type CheckboxVariantProps = VariantProps;
+
+// ---------------------------------------–-------------------------------------
+// Component props
+// ---------------------------------------–-------------------------------------
+
+export type CheckboxProps = React.ComponentPropsWithoutRef &
+ CheckboxVariantProps;
diff --git a/components/ui/code-block/actions.tsx b/components/ui/code-block/actions.tsx
new file mode 100644
index 00000000..d8a1dfa7
--- /dev/null
+++ b/components/ui/code-block/actions.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+import { IconButton, Select, toast } from '..';
+import { codeBlockActionsVariants } from './styles';
+import type { CodeBlockActionsProps, CodeBlockLanguage } from './types';
+import { Check, Copy } from 'lucide-react';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const CodeBlockActions: React.FC = ({ code, switcher, inHeader }) => {
+ const [copied, setCopied] = useState(false);
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => setMounted(true), []);
+
+ const isTouchscreen = mounted ? /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) : false;
+
+ const copyToClipboard = () => {
+ if (!copied) {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ navigator.clipboard.writeText(code);
+ toast({ title: 'Copied to clipboard!', intent: 'success', hasCloseButton: true });
+ };
+
+ return (
+
+ {switcher && switcher.options.length > 1 ? (
+ switcher.onChange(e.target.value as CodeBlockLanguage)}
+ aria-label="Select a language for the code block."
+ >
+ {switcher.options.map((option, index) => (
+
+ {option.label}
+
+ ))}
+
+ ) : null}
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+CodeBlockActions.displayName = 'CodeBlockActions';
+
+export default CodeBlockActions;
diff --git a/components/ui/code-block/index.tsx b/components/ui/code-block/index.tsx
index c731862b..ebba81f2 100644
--- a/components/ui/code-block/index.tsx
+++ b/components/ui/code-block/index.tsx
@@ -1,5 +1,6 @@
-import { type FC, useEffect, useState } from 'react';
+'use client';
+import CodeBlockActions from './actions';
import CodeBlockLanguageLogo from './language-logo';
import {
codeBlockContainerVariants,
@@ -9,21 +10,19 @@ import {
codeBlockHeaderStyles,
codeBlockLineHighlightedStyles,
codeBlockLineNumberStyles,
- codeBlockLineStyles,
+ codeBlockLineVariants,
codeBlockPreVariants,
codeBlockStyles,
} from './styles';
import { theme } from './theme';
import type { CodeBlockProps } from './types';
import clsx from 'clsx';
-import { Check, Copy, File, TerminalSquare } from 'lucide-react';
+import { File, FileDiff, TerminalSquare } from 'lucide-react';
import { Highlight } from 'prism-react-renderer';
import Prism from 'prismjs';
import { twMerge } from 'tailwind-merge';
-import { IconButton } from '@/components/ui';
-
-// Add support for additional languagaes
+// Add support for additional languagaes.
(typeof global === 'undefined' ? window : global).Prism = Prism;
require('prismjs/components/prism-javascript');
require('prismjs/components/prism-typescript');
@@ -32,79 +31,69 @@ require('prismjs/components/prism-tsx');
require('prismjs/components/prism-solidity');
require('prismjs/components/prism-python');
require('prismjs/components/prism-bash');
+require('prismjs/components/prism-diff');
-const CodeBlock: FC = ({
+const CodeBlock: React.FC = ({
className,
fileName,
language = 'none',
+ logo,
+ switcher,
highlightLines = [],
+ breakLines = false,
showLineNumbers = true,
roundedTop = true,
+ containerized = true,
children,
...rest
}) => {
- const [copied, setCopied] = useState(false);
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => setIsMounted(true), []);
-
- const isMobile = isMounted ? /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) : false;
+ const hasHeader = fileName !== undefined;
- const hasFileName = fileName !== undefined;
-
- const Icon =
- language === 'javascript' || language === 'js'
+ const Icon = logo
+ ? logo
+ : language === 'javascript' || language === 'js'
? CodeBlockLanguageLogo.JavaScript
: language === 'typescript' || language === 'ts'
- ? CodeBlockLanguageLogo.TypeScript
- : language === 'jsx'
- ? CodeBlockLanguageLogo.React
- : language === 'tsx'
- ? CodeBlockLanguageLogo.React
- : language === 'solidity' || language === 'sol'
- ? CodeBlockLanguageLogo.Solidity
- : language === 'python' || language === 'py'
- ? CodeBlockLanguageLogo.Python
- : language === 'bash' || language === 'sh'
- ? TerminalSquare
- : File;
-
- const copyToClipboard = () => {
- if (!copied) {
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- navigator.clipboard.writeText(children);
- };
+ ? CodeBlockLanguageLogo.TypeScript
+ : language === 'jsx'
+ ? CodeBlockLanguageLogo.React
+ : language === 'tsx'
+ ? CodeBlockLanguageLogo.React
+ : language === 'solidity' || language === 'sol'
+ ? CodeBlockLanguageLogo.Solidity
+ : language === 'python' || language === 'py'
+ ? CodeBlockLanguageLogo.Python
+ : language === 'bash' || language === 'sh'
+ ? TerminalSquare
+ : language === 'diff'
+ ? FileDiff
+ : File;
return (
-
- {hasFileName ? (
-
+
+ {hasHeader ? (
+
) : null}
{({ tokens, getLineProps, getTokenProps }) => (
-
+
{tokens.map((line, i) => {
const { className, ...restLineProps } = getLineProps({ line });
@@ -113,36 +102,24 @@ const CodeBlock: FC = ({
key={i}
className={clsx(
className,
- codeBlockLineStyles,
+ codeBlockLineVariants({ breakLines }),
highlightLines.includes(i + 1) ? codeBlockLineHighlightedStyles : '',
)}
{...restLineProps}
+ code-block-line=""
>
{showLineNumbers ? (
- {i + 1}
+
+ {i + 1}
+
) : null}
{line.map((token, key) => (
-
+
))}
);
})}
- {!hasFileName ? (
-
- {copied ? : }
-
- ) : null}
+ {!hasHeader ? : null}
@@ -152,6 +129,10 @@ const CodeBlock: FC
= ({
);
};
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
CodeBlock.displayName = 'CodeBlock';
export default CodeBlock;
diff --git a/components/ui/code-block/language-logo/index.tsx b/components/ui/code-block/language-logo/index.tsx
index 8710e664..b90b1389 100644
--- a/components/ui/code-block/language-logo/index.tsx
+++ b/components/ui/code-block/language-logo/index.tsx
@@ -1,13 +1,16 @@
import { JavaScriptLogo, PythonLogo, ReactLogo, SolidityLogo, TypeScriptLogo } from './logos';
-const CodeBlockLanguageLogo = () => null;
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
-CodeBlockLanguageLogo.JavaScript = JavaScriptLogo;
-CodeBlockLanguageLogo.Python = PythonLogo;
-CodeBlockLanguageLogo.React = ReactLogo;
-CodeBlockLanguageLogo.Solidity = SolidityLogo;
-CodeBlockLanguageLogo.TypeScript = TypeScriptLogo;
-
-CodeBlockLanguageLogo.displayName = 'CodeBlockLanguageLogo';
+const CodeBlockLanguageLogo = {
+ displayName: 'CodeBlockLanguageLogo',
+ JavaScript: JavaScriptLogo,
+ Python: PythonLogo,
+ React: ReactLogo,
+ Solidity: SolidityLogo,
+ TypeScript: TypeScriptLogo,
+};
export default CodeBlockLanguageLogo;
diff --git a/components/ui/code-block/language-logo/logos.tsx b/components/ui/code-block/language-logo/logos.tsx
index 50fbdbd9..cde8bd79 100644
--- a/components/ui/code-block/language-logo/logos.tsx
+++ b/components/ui/code-block/language-logo/logos.tsx
@@ -1,6 +1,4 @@
-import type { FC } from 'react';
-
-export const JavaScriptLogo: FC = ({ className, ...rest }) => {
+export const JavaScriptLogo: React.FC = ({ className, ...rest }) => {
return (
= ({ className, ..
);
};
-export const PythonLogo: FC = ({ className, ...rest }) => {
+export const PythonLogo: React.FC = ({ className, ...rest }) => {
return (
= ({ className, ...res
);
};
-export const ReactLogo: FC = ({ className, ...rest }) => {
+export const ReactLogo: React.FC = ({ className, ...rest }) => {
return (
= ({ className, ...rest
);
};
-export const SolidityLogo: FC = ({ className, ...rest }) => {
+export const SolidityLogo: React.FC = ({ className, ...rest }) => {
return (
= ({ className, ...r
);
};
-export const TypeScriptLogo: FC = ({ className, ...rest }) => {
+export const TypeScriptLogo: React.FC = ({ className, ...rest }) => {
return (
;
+
type CodeBlockVariantProps = VariantProps;
// -----------------------------------------------------------------------------
// Component props
// -----------------------------------------------------------------------------
+export type CodeBlockActionsProps = CodeBlockActionsVariantProps & {
+ code: string;
+ switcher?: {
+ options: { label: string; value: CodeBlockLanguage }[];
+ value: CodeBlockLanguage;
+ onChange: (value: CodeBlockLanguage) => void;
+ };
+};
+
+export type CodeBlockLanguage =
+ | 'javascript'
+ | 'js'
+ | 'typescript'
+ | 'ts'
+ | 'jsx'
+ | 'tsx'
+ | 'solidity'
+ | 'sol'
+ | 'python'
+ | 'py'
+ | 'bash'
+ | 'sh'
+ | 'diff'
+ | 'none';
+
export type CodeBlockProps = Omit &
CodeBlockVariantProps & {
fileName?: string;
- language?:
- | 'javascript'
- | 'js'
- | 'typescript'
- | 'ts'
- | 'jsx'
- | 'tsx'
- | 'solidity'
- | 'sol'
- | 'python'
- | 'py'
- | 'bash'
- | 'sh'
- | 'none';
+ language?: CodeBlockLanguage;
+ logo?: React.FC;
highlightLines?: number[];
showLineNumbers?: boolean;
+ breakLines?: boolean;
+ switcher?: {
+ options: { label: string; value: CodeBlockLanguage }[];
+ value: CodeBlockLanguage;
+ onChange: (value: CodeBlockLanguage) => void;
+ };
children: string;
};
diff --git a/components/ui/command/index.tsx b/components/ui/command/index.tsx
new file mode 100644
index 00000000..35d09a5c
--- /dev/null
+++ b/components/ui/command/index.tsx
@@ -0,0 +1,150 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import { Dialog } from '..';
+import {
+ commandDialogStyles,
+ commandEmptyStyles,
+ commandGroupStyles,
+ commandInputIconStyles,
+ commandInputParentStyles,
+ commandInputStyles,
+ commandItemIconContainerStyles,
+ commandItemStyles,
+ commandListStyles,
+ commandRootVariants,
+ commandSeparatorStyles,
+} from './styles';
+import type {
+ CommandComposition,
+ CommandDialogProps,
+ CommandEmptyProps,
+ CommandGroupProps,
+ CommandInputProps,
+ CommandItemProps,
+ CommandListProps,
+ CommandRootProps,
+ CommandSeparatorProps,
+} from './types';
+import clsx from 'clsx';
+import { Command as CommandPrimitive } from 'cmdk';
+import { Search } from 'lucide-react';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const CommandDialog: React.FC = ({ children, ...rest }) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const CommandEmpty = forwardRef, CommandEmptyProps>(
+ (props, ref) => (
+
+ ),
+);
+
+const CommandGroup = forwardRef, CommandGroupProps>(
+ ({ className, ...rest }, ref) => (
+
+ ),
+);
+
+const CommandInput = forwardRef, CommandInputProps>(
+ ({ className, ...rest }, ref) => (
+
+
+
+
+ ),
+);
+
+const CommandItem = forwardRef, CommandItemProps>(
+ ({ className, icon, children, ...rest }, ref) => (
+
+ {icon ? (
+
+ {icon}
+
+ ) : null}
+ {children}
+
+ ),
+);
+
+const CommandList = forwardRef, CommandListProps>(
+ ({ className, ...rest }, ref) => (
+
+ ),
+);
+
+const CommandRoot = forwardRef, CommandRootProps>(
+ ({ className, noBorder = false, ...rest }, ref) => (
+
+ ),
+);
+
+const CommandSeparator = forwardRef<
+ React.ElementRef,
+ CommandSeparatorProps
+>(({ className, ...rest }, ref) => (
+
+));
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+CommandDialog.displayName = CommandPrimitive.Dialog.displayName;
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+CommandList.displayName = CommandPrimitive.List.displayName;
+CommandRoot.displayName = 'CommandRoot';
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const Command: CommandComposition = {
+ Dialog: CommandDialog,
+ Empty: CommandEmpty,
+ Group: CommandGroup,
+ Input: CommandInput,
+ Item: CommandItem,
+ List: CommandList,
+ Root: CommandRoot,
+ Separator: CommandSeparator,
+};
+
+export default Command;
diff --git a/components/ui/command/styles.tsx b/components/ui/command/styles.tsx
new file mode 100644
index 00000000..f883b347
--- /dev/null
+++ b/components/ui/command/styles.tsx
@@ -0,0 +1,120 @@
+import { cva } from 'class-variance-authority';
+
+export const commandDialogStyles = [
+ 'rounded-lg',
+ 'border-0',
+ '[&_[cmdk-group]]:p-2',
+ '[&_[cmdk-input]]:h-[46px]',
+ '[&_[cmdk-item]]:h-9',
+ '[&_[cmdk-item]]:px-2',
+ '[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0',
+ '[&_[cmdk-group-heading]]:py-1',
+ '[&_[cmdk-separator]_+[cmdk-group]_[cmdk-group-heading]]:pt-3',
+];
+
+export const commandEmptyStyles = [
+ 'text-center',
+ 'text-sm',
+ 'py-6',
+ 'px-4',
+ 'leading-normal',
+ 'text-gray-11',
+];
+
+export const commandGroupStyles = [
+ 'text-gray-12',
+ 'overflow-hidden',
+ 'p-1',
+ '[&_[cmdk-group-heading]]:text-gray-11',
+ '[&_[cmdk-group-heading]]:px-2',
+ '[&_[cmdk-group-heading]]:py-1.5',
+ '[&_[cmdk-group-heading]]:text-xs',
+];
+
+export const commandInputIconStyles = ['shrink-0', 'w-4', 'h-4', 'mr-2'];
+
+export const commandInputParentStyles = [
+ 'flex',
+ 'items-center',
+ 'border-b',
+ 'border-gray-6',
+ 'px-3',
+];
+
+export const commandInputStyles = [
+ 'flex',
+ 'h-10',
+ 'w-full',
+ 'bg-transparent',
+ 'text-sm',
+ 'text-gray-12',
+ 'outline-none',
+ 'placeholder:text-gray-11',
+ 'disabled:cursor-not-allowed',
+ 'disabled:opacity-50',
+];
+
+export const commandItemIconContainerStyles = [
+ 'flex',
+ 'items-center',
+ 'justify-center',
+ 'text-gray-11',
+ 'w-4',
+ 'h-4',
+ 'mr-2',
+];
+
+export const commandItemStyles = [
+ 'relative',
+ 'flex',
+ 'cursor-default',
+ 'select-none',
+ 'items-center',
+ 'rounded',
+ 'font-normal',
+ 'px-2',
+ 'h-8',
+ 'text-sm',
+ 'outline-none',
+ 'text-gray-11',
+ '[&_[cmdk-item-content]]:line-clamp-1',
+ '[&_[cmdk-item-content]]:overflow-hidden',
+ '[&_[cmdk-item-content]]:text-ellipsis',
+ '[&_[cmdk-item-content]]:text-gray-12',
+ '[&_[cmdk-item-icon]]:text-gray-11',
+ '[&_[cmdk-item-icon]]:transition-colors',
+ '[&_[cmdk-item-icon]]:hover:text-gray-12',
+ 'aria-selected:bg-gray-4',
+ 'data-[disabled=true]:pointer-events-none',
+ 'data-[disabled=true]:opacity-50',
+];
+
+export const commandListStyles = [
+ 'max-h-[300px]',
+ 'overflow-y-auto',
+ 'overflow-x-hidden',
+ 'hide-scrollbar',
+];
+
+export const commandRootVariants = cva(
+ [
+ 'flex',
+ 'max-w-full',
+ 'flex-col',
+ 'overflow-hidden',
+ 'rounded-lg',
+ 'bg-gray-2',
+ 'text-gray-12',
+ 'shadow-none',
+ ],
+ {
+ variants: {
+ noBorder: {
+ true: [],
+ false: ['border', 'border-gray-6'],
+ },
+ },
+ },
+);
+
+export const commandSeparatorStyles = ['-mx-1', 'h-px', 'bg-gray-6'];
diff --git a/components/ui/command/types.tsx b/components/ui/command/types.tsx
new file mode 100644
index 00000000..61ab9988
--- /dev/null
+++ b/components/ui/command/types.tsx
@@ -0,0 +1,50 @@
+import { commandRootVariants } from './styles';
+import type { DialogProps } from '@radix-ui/react-dialog';
+import type { VariantProps } from 'class-variance-authority';
+import { Command as CommandPrimitive } from 'cmdk';
+
+// -----------------------------------------------------------------------------
+// Variant props
+// -----------------------------------------------------------------------------
+
+type CommandRootVariantProps = VariantProps;
+
+// -----------------------------------------------------------------------------
+// Component props
+// -----------------------------------------------------------------------------
+
+export type CommandDialogProps = DialogProps;
+
+export type CommandEmptyProps = React.ComponentPropsWithoutRef;
+
+export type CommandGroupProps = React.ComponentPropsWithoutRef;
+
+export type CommandInputProps = React.ComponentPropsWithoutRef;
+
+export type CommandItemProps = React.ComponentPropsWithoutRef & {
+ icon?: React.ReactNode;
+};
+
+export type CommandListProps = React.ComponentPropsWithoutRef;
+
+export type CommandRootProps = React.ComponentPropsWithoutRef &
+ CommandRootVariantProps;
+
+export type CommandSeparatorProps = React.ComponentPropsWithoutRef<
+ typeof CommandPrimitive.Separator
+>;
+
+// -----------------------------------------------------------------------------
+// Composition
+// -----------------------------------------------------------------------------
+
+export type CommandComposition = {
+ Dialog: React.FC;
+ Empty: React.FC;
+ Group: React.FC;
+ Input: React.FC;
+ Item: React.FC;
+ List: React.FC;
+ Root: React.FC;
+ Separator: React.FC;
+};
diff --git a/components/ui/dialog/index.tsx b/components/ui/dialog/index.tsx
new file mode 100644
index 00000000..1e369d8a
--- /dev/null
+++ b/components/ui/dialog/index.tsx
@@ -0,0 +1,133 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import { IconButton } from '..';
+import {
+ dialogContentStyles,
+ dialogDescriptionStyles,
+ dialogFooterStyles,
+ dialogHeaderStyles,
+ dialogOverlayStyles,
+ dialogTitleStyles,
+} from './styles';
+import type {
+ DialogComposition,
+ DialogContentProps,
+ DialogDescriptionProps,
+ DialogFooterProps,
+ DialogHeaderProps,
+ DialogOverlayProps,
+ DialogTitleProps,
+} from './types';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import clsx from 'clsx';
+import { X } from 'lucide-react';
+import { twMerge } from 'tailwind-merge';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogContent = forwardRef<
+ React.ElementRef,
+ DialogContentProps
+>(({ className, hasCloseButton = true, fakeOverlay = false, children, ...rest }, ref) => (
+
+
+ {fakeOverlay ?
: null}
+
+ {children}
+ {hasCloseButton ? (
+
+
+
+ Close
+
+
+ ) : null}
+
+
+));
+
+const DialogDescription = forwardRef<
+ React.ElementRef,
+ DialogDescriptionProps
+>(({ className, ...rest }, ref) => (
+
+));
+
+const DialogFooter: React.FC = ({ className, ...rest }) => (
+
+);
+
+const DialogHeader: React.FC = ({ className, ...rest }) => (
+
+);
+
+const DialogOverlay = forwardRef<
+ React.ElementRef,
+ DialogOverlayProps
+>(({ className, ...rest }, ref) => (
+
+));
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogRoot = DialogPrimitive.Root;
+
+const DialogTitle = forwardRef, DialogTitleProps>(
+ ({ className, ...rest }, ref) => (
+
+ ),
+);
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+DialogClose.displayName = DialogPrimitive.Close.displayName;
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+DialogFooter.displayName = 'DialogFooter';
+DialogHeader.displayName = 'DialogHeader';
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+DialogPortal.displayName = DialogPrimitive.Portal.displayName;
+DialogRoot.displayName = DialogPrimitive.Root.displayName;
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
+
+const Dialog: DialogComposition = {
+ Close: DialogClose,
+ Content: DialogContent,
+ Description: DialogDescription,
+ Footer: DialogFooter,
+ Header: DialogHeader,
+ Overlay: DialogOverlay,
+ Portal: DialogPortal,
+ Root: DialogRoot,
+ Title: DialogTitle,
+ Trigger: DialogTrigger,
+};
+
+export default Dialog;
diff --git a/components/ui/dialog/styles.tsx b/components/ui/dialog/styles.tsx
new file mode 100644
index 00000000..bdafec08
--- /dev/null
+++ b/components/ui/dialog/styles.tsx
@@ -0,0 +1,77 @@
+export const dialogContentStyles = [
+ 'fixed',
+ 'left-[50%]',
+ 'top-[100%]',
+ 'z-50',
+ 'grid',
+ 'p-4',
+ 'w-full',
+ 'translate-x-[-50%]',
+ 'translate-y-[-100%]',
+ 'max-h-[80vh]',
+ 'overflow-y-scroll',
+ 'border-t',
+ 'border-gray-6',
+ 'bg-gray-2',
+ 'rounded-t-lg',
+ 'duration-200',
+ 'focus:outline-none',
+ 'overflow-x-hidden',
+ 'sm:top-[50%]',
+ 'sm:max-w-lg',
+ 'sm:max-h-[50vh]',
+ 'sm:p-5',
+ 'sm:border-x',
+ 'sm:border-b',
+ 'sm:rounded-b-lg',
+ 'sm:translate-y-[-50%]',
+ '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]:slide-out-to-top-[48%]',
+ 'data-[state=open]:slide-in-from-top-[48%]',
+ 'data-[state=closed]:slide-out-to-left-1/2',
+ 'data-[state=open]:slide-in-from-left-1/2',
+ 'sm:data-[state=closed]:zoom-out-95',
+ 'sm:data-[state=open]:zoom-in-95',
+];
+
+export const dialogDescriptionStyles = [
+ 'text-sm',
+ 'text-gray-11',
+ 'leading-normal',
+ 'mb-4',
+ 'sm:mb-5',
+];
+
+export const dialogFooterStyles = [
+ 'flex',
+ 'border-t',
+ 'border-gray-6',
+ 'gap-2',
+ '-mx-4',
+ '-mb-4',
+ 'px-4',
+ 'py-2',
+ 'sm:px-2',
+ 'sm:-mx-5',
+ 'sm:-mb-5',
+ 'sm:justify-end',
+];
+
+export const dialogHeaderStyles = ['flex', 'flex-col', 'gap-1', 'text-center', 'sm:text-left'];
+
+export const dialogOverlayStyles = [
+ 'bg-black/10',
+ 'fixed',
+ 'inset-0',
+ 'z-50',
+ 'backdrop-blur-sm',
+ 'data-[state=open]:animate-in',
+ 'data-[state=closed]:animate-out',
+ 'data-[state=closed]:fade-out-0',
+ 'data-[state=open]:fade-in-0',
+];
+
+export const dialogTitleStyles = ['text-lg', 'font-medium', 'leading-6', 'text-gray-12'];
diff --git a/components/ui/dialog/types.tsx b/components/ui/dialog/types.tsx
new file mode 100644
index 00000000..1f7dd08d
--- /dev/null
+++ b/components/ui/dialog/types.tsx
@@ -0,0 +1,47 @@
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+
+// -----------------------------------------------------------------------------
+// Component props
+// -----------------------------------------------------------------------------
+
+export type DialogCloseProps = React.ComponentPropsWithoutRef;
+
+export type DialogContentProps = React.ComponentPropsWithoutRef & {
+ hasCloseButton?: boolean;
+ fakeOverlay?: boolean;
+};
+
+export type DialogDescriptionProps = React.ComponentPropsWithoutRef<
+ typeof DialogPrimitive.Description
+>;
+
+export type DialogFooterProps = JSX.IntrinsicElements['div'];
+
+export type DialogHeaderProps = JSX.IntrinsicElements['div'];
+
+export type DialogOverlayProps = React.ComponentPropsWithoutRef;
+
+export type DialogPortalProps = React.ComponentPropsWithoutRef;
+
+export type DialogRootProps = React.ComponentPropsWithoutRef;
+
+export type DialogTitleProps = React.ComponentPropsWithoutRef;
+
+export type DialogTriggerProps = React.ComponentPropsWithoutRef;
+
+// -----------------------------------------------------------------------------
+// Composition
+// -----------------------------------------------------------------------------
+
+export type DialogComposition = {
+ Close: React.FC;
+ Content: React.FC;
+ Description: React.FC;
+ Footer: React.FC;
+ Header: React.FC;
+ Overlay: React.FC;
+ Portal: React.FC;
+ Root: React.FC;
+ Title: React.FC;
+ Trigger: React.FC;
+};
diff --git a/components/ui/drawer/index.tsx b/components/ui/drawer/index.tsx
new file mode 100644
index 00000000..85c84ea1
--- /dev/null
+++ b/components/ui/drawer/index.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { forwardRef } from 'react';
+
+import {
+ drawerContentContainerStyles,
+ drawerContentHandleContainerStyles,
+ drawerContentHandleStyles,
+ drawerContentStyles,
+ drawerDescriptionStyles,
+ drawerFooterStyles,
+ drawerHeaderStyles,
+ drawerOverlayStyles,
+ drawerTitleStyles,
+} from './styles';
+import type {
+ DrawerComposition,
+ DrawerContentProps,
+ DrawerDescriptionProps,
+ DrawerFooterProps,
+ DrawerHeaderProps,
+ DrawerOverlayProps,
+ DrawerRootProps,
+ DrawerTitleProps,
+} from './types';
+import clsx from 'clsx';
+import { twMerge } from 'tailwind-merge';
+import { Drawer as DrawerPrimitive } from 'vaul';
+
+// -----------------------------------------------------------------------------
+// Component
+// -----------------------------------------------------------------------------
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerContent = forwardRef<
+ React.ElementRef,
+ DrawerContentProps
+>(({ className, children, ...rest }, ref) => (
+
+
+
+
+ {/* We wrap the inner contents so the overflow scrolls are dealt with
+ properly without messing up other components of ` `. */}
+
+ {children}
+
+
+
+));
+
+const DrawerDescription = forwardRef<
+ React.ElementRef,
+ DrawerDescriptionProps
+>(({ className, ...rest }, ref) => (
+
+));
+
+const DrawerFooter = ({ className, ...rest }: DrawerFooterProps) => (
+
+);
+
+const DrawerHeader = ({ className, ...rest }: DrawerHeaderProps) => (
+
+);
+
+const DrawerOverlay = forwardRef<
+ React.ElementRef,
+ DrawerOverlayProps
+>(({ className, ...rest }, ref) => (
+
+));
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerRoot = ({ shouldScaleBackground = false, ...rest }: DrawerRootProps) => (
+
+);
+
+const DrawerTitle = forwardRef, DrawerTitleProps>(
+ ({ className, ...rest }, ref) => (
+
+ ),
+);
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+// -----------------------------------------------------------------------------
+// Export
+// -----------------------------------------------------------------------------
+
+DrawerClose.displayName = DrawerPrimitive.Close.displayName;
+DrawerContent.displayName = DrawerPrimitive.Content.displayName;
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+DrawerFooter.displayName = 'DrawerFooter';
+DrawerHeader.displayName = 'DrawerHeader';
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+DrawerPortal.displayName = DrawerPrimitive.Portal.displayName;
+DrawerRoot.displayName = 'DrawerRoot';
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+DrawerTrigger.displayName = DrawerPrimitive.Trigger.displayName;
+
+const Drawer: DrawerComposition = {
+ Close: DrawerClose,
+ Content: DrawerContent,
+ Description: DrawerDescription,
+ Footer: DrawerFooter,
+ Header: DrawerHeader,
+ Overlay: DrawerOverlay,
+ Portal: DrawerPortal,
+ Root: DrawerRoot,
+ Title: DrawerTitle,
+ Trigger: DrawerTrigger,
+};
+
+export default Drawer;
diff --git a/components/ui/drawer/styles.tsx b/components/ui/drawer/styles.tsx
new file mode 100644
index 00000000..a1bc5577
--- /dev/null
+++ b/components/ui/drawer/styles.tsx
@@ -0,0 +1,72 @@
+export const drawerContentContainerStyles = [
+ 'overflow-y-scroll',
+ 'overflow-x-scroll',
+ 'px-4',
+ 'pb-4',
+ '-pt-1.5', // `pt-4` - drawer handle container height = 16px - 22px = `-pt-1.5`
+ 'hide-scrollbar',
+];
+
+export const drawerContentHandleContainerStyles = [
+ 'pointer-events-none',
+ 'sticky',
+ 'top-0',
+ 'flex',
+ 'min-h-[1.375rem]', // Handle height + `py-2` = 6px + 16px = 22px = 1.375rem
+ 'w-full',
+ 'items-center',
+ 'justify-items-center',
+ 'rounded-t-lg',
+ 'bg-gray-2',
+];
+
+export const drawerContentHandleStyles = [
+ 'bg-gray-5',
+ 'mx-auto',
+ 'sticky',
+ 'top-2',
+ 'h-1.5',
+ 'w-8',
+ 'rounded-full',
+];
+
+export const drawerContentStyles = [
+ 'bg-gray-2',
+ 'fixed',
+ 'mx-auto',
+ 'inset-x-0',
+ 'bottom-0',
+ 'z-50',
+ 'flex',
+ 'h-auto',
+ 'flex-col',
+ 'rounded-t-lg',
+ 'border-t',
+ 'border-gray-6',
+ 'max-h-[80vh]',
+ 'focus:outline-none',
+];
+
+export const drawerDescriptionStyles = ['text-sm', 'text-gray-11', 'leading-normal', 'mb-4'];
+
+export const drawerFooterStyles = [
+ 'z-30',
+ 'bg-gray-2',
+ 'flex',
+ 'border-t',
+ 'border-gray-6',
+ 'px-4',
+ 'py-2',
+ '-mx-4',
+ '-mb-4',
+ 'gap-2',
+ 'sticky',
+ '-bottom-4',
+ 'sm:justify-end',
+];
+
+export const drawerHeaderStyles = ['flex', 'flex-col', 'gap-1', 'text-center', 'sm:text-left'];
+
+export const drawerOverlayStyles = ['bg-black/75', 'fixed', 'inset-0', 'z-50'];
+
+export const drawerTitleStyles = ['text-lg', 'font-medium', 'leading-6', 'text-gray-12'];
diff --git a/components/ui/drawer/types.tsx b/components/ui/drawer/types.tsx
new file mode 100644
index 00000000..41db18ec
--- /dev/null
+++ b/components/ui/drawer/types.tsx
@@ -0,0 +1,44 @@
+import { Drawer as DrawerPrimitive } from 'vaul';
+
+// -----------------------------------------------------------------------------
+// Component props
+// -----------------------------------------------------------------------------
+
+export type DrawerCloseProps = React.ComponentPropsWithoutRef;
+
+export type DrawerContentProps = React.ComponentPropsWithoutRef;
+
+export type DrawerDescriptionProps = React.ComponentPropsWithoutRef<
+ typeof DrawerPrimitive.Description
+>;
+
+export type DrawerFooterProps = JSX.IntrinsicElements['div'];
+
+export type DrawerHeaderProps = JSX.IntrinsicElements['div'];
+
+export type DrawerOverlayProps = React.ComponentPropsWithoutRef;
+
+export type DrawerPortalProps = React.ComponentPropsWithoutRef;
+
+export type DrawerRootProps = React.ComponentPropsWithRef;
+
+export type DrawerTitleProps = React.ComponentPropsWithoutRef;
+
+export type DrawerTriggerProps = React.ComponentPropsWithoutRef;
+
+// -----------------------------------------------------------------------------
+// Composition
+// -----------------------------------------------------------------------------
+
+export type DrawerComposition = {
+ Close: React.FC;
+ Content: React.FC;
+ Description: React.FC;
+ Footer: React.FC;
+ Header: React.FC;
+ Overlay: React.FC;
+ Portal: React.FC