Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions apps/docs/src/examples/button.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
'use client';

import { Button } from '@vitnode/core/components/ui/button';
import { Card } from '@vitnode/core/components/ui/card';
import { ArrowRight, CheckCircle, Eye, Home, Star, Trash2 } from 'lucide-react';
import React from 'react';

export default function ButtonExample() {
const [isLoading, setIsLoading] = React.useState(false);

return (
<Card className="flex flex-row flex-wrap items-center justify-center gap-6 p-8">
<Button size="lg">
<Button isLoading={isLoading} size="lg">
<Home />
Default
</Button>
<Button variant="secondary">
<Button isLoading={isLoading} variant="secondary">
<Star />
Secondary
</Button>
<Button variant="outline">
<Button isLoading={isLoading} variant="outline">
<Eye />
Outline
</Button>
<Button variant="ghost">
<Button isLoading={isLoading} variant="ghost">
<CheckCircle />
Ghost
</Button>
<Button variant="link">
<Button isLoading={isLoading} variant="link">
<ArrowRight />
Link
</Button>
<Button size="sm" variant="destructive">
<Button isLoading={isLoading} size="sm" variant="destructive">
<Trash2 />
Destructive
</Button>
<Button aria-label="Delete" size="icon" variant="destructiveGhost">
<Button
aria-label="Delete"
isLoading={isLoading}
size="icon"
variant="destructiveGhost"
>
<Trash2 />
</Button>
<Button onClick={() => setIsLoading(!isLoading)}>Toggle Loading</Button>
</Card>
);
}
2 changes: 2 additions & 0 deletions packages/vitnode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react": "19.1.x",
"react-dom": "19.1.x",
"react-hook-form": "^7.x.x",
"motion": "^12.x.x",
"typescript": "^5.8.x",
"zod": "4.x.x"
},
Expand Down Expand Up @@ -115,6 +116,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"input-otp": "^1.4.2",
"motion": "^12.23.6",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.5",
"postgres": "^3.4.7",
Expand Down
57 changes: 57 additions & 0 deletions packages/vitnode/src/components/ui/button-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { AnimatePresence, motion } from 'motion/react';
import { useTranslations } from 'next-intl';
import { Slot } from 'radix-ui';

import { cn } from '../../lib/utils';
import { type ButtonProps, buttonVariants } from './button';
import { Loader } from './loader';

export function ClientButton({
className,
variant,
size,
asChild = false,
isLoading,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : 'button';
const t = useTranslations('core.global');

return (
<Comp
aria-label={isLoading ? t('loading') : props['aria-label']}
className={cn(buttonVariants({ variant, size, className }))}
data-slot="button"
disabled={isLoading ?? props.disabled}
{...props}
>
<div className="relative flex items-center justify-center">
<div
className={cn(
'flex items-center justify-center gap-2 transition-opacity duration-300',
isLoading ? 'opacity-0' : 'opacity-100',
)}
>
{children}
</div>

<AnimatePresence>
{isLoading && (
<motion.div
animate={{ opacity: 1, transform: 'translateY(0px)' }}
className="absolute inset-0 flex items-center justify-center"
exit={{ opacity: 0, transform: 'translateY(20px)' }}
initial={{ opacity: 0, transform: 'translateY(-20px)' }}
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
>
<Loader small />
</motion.div>
)}
</AnimatePresence>
</div>
</Comp>
);
}
49 changes: 5 additions & 44 deletions packages/vitnode/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { useTranslations } from 'next-intl';
import { Slot } from 'radix-ui';
import * as React from 'react';

import { cn } from '@/lib/utils';

import { Loader } from './loader';
import { ClientButton } from './button-client';

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer overflow-hidden",
{
variants: {
variant: {
Expand Down Expand Up @@ -40,52 +36,17 @@ const buttonVariants = cva(
},
);

type ButtonProps = React.ComponentProps<'button'> &
export type ButtonProps = React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
isLoading?: boolean;
loadingText?: string;
} & (
| { 'aria-label': string; size: 'icon' }
| { 'aria-label'?: string; size?: 'default' | 'lg' | 'sm' }
);

function Button({
className,
variant,
size,
asChild = false,
isLoading,
loadingText,
...props
}: ButtonProps) {
const t = useTranslations('core.global');
const Comp = asChild ? Slot.Root : 'button';

if (isLoading) {
const text = loadingText ?? t('loading');

return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
{...props}
aria-label={text}
disabled
type="button"
>
<Loader small />
<span className="truncate">{size !== 'icon' && text}</span>
</Comp>
);
}

return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
data-slot="button"
{...props}
/>
);
function Button(props: ButtonProps) {
return <ClientButton {...props} />;
}

export { Button, buttonVariants };
2 changes: 1 addition & 1 deletion packages/vitnode/src/components/ui/loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Loader = ({
small?: boolean;
}) => {
if (small) {
return <Loader2 className={cn('size-4 animate-spin', className)} />;
return <Loader2 className={cn('size-5 animate-spin', className)} />;
}

return (
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.