magicuidesign / magicui

UI Library for Design Engineers. Animated components and effects you can copy and paste into your apps. Free. Open Source.
https://magicui.design
MIT License
6.29k stars 225 forks source link

Magic UI for Svelte 5 #76

Open dillionverma opened 2 months ago

dillionverma commented 2 months ago

I'm making this issue to track any ports of Magic UI components to Svelte

If you have any ports, feel free to comment them below to share with the community!

We will eventually try and merge everything together in one repo if possible down the line.

However our current priority will be on react first.

If someone from the community wants to take initiative to integrate Svelte support into this repo please feel free to comment below and get started :) No permission needed

dillionverma commented 2 months ago

linking #74 since it has some ports

dillionverma commented 2 months ago

Border Beam for Svelte

Thank you @gursheyss

<script lang="ts">
    import { cn } from '$lib/utils';

    interface Props {
        class?: string;
        size?: number;
        duration?: number;
        borderWidth?: number;
        anchor?: number;
        colorFrom?: string;
        colorTo?: string;
        delay?: number;
    }

    let {
        class: className,
        size = 200,
        duration = 15,
        anchor = 90,
        borderWidth = 1.5,
        colorFrom = '#ffaa40',
        colorTo = '#9c40ff',
        delay = 0
    }: Props = $props();

    let styleVars = $derived.by(() => {
        return `--size: ${size}; 
            --duration: ${duration}; 
            --anchor: ${anchor}; 
            --border-width: ${borderWidth}; 
            --color-from: ${colorFrom}; 
            --color-to: ${colorTo}; 
            --delay: -${delay}s;`;
    });
</script>

<div
    style={styleVars}
    class={cn(
        'absolute inset-[0] rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',

        // mask styles
        '![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',

        // pseudo styles
        'after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
        className
    )}
></div>
dillionverma commented 2 months ago

Shimmer Button for Svelte

Thank you @gursheyss 🙏

Shimmer Button

<script lang="ts">
    import { cn } from '$lib/utils';
    import type { Snippet } from 'svelte';
    import type { HTMLButtonAttributes } from 'svelte/elements';

    interface ShimmerButtonProps extends HTMLButtonAttributes {
        shimmerColor?: string;
        shimmerSize?: string;
        borderRadius?: string;
        shimmerDuration?: string;
        background?: string;
        class?: string;
        children?: Snippet;
    }

    let {
        shimmerColor = '#ffffff',
        shimmerSize = '0.05em',
        shimmerDuration = '3s',
        borderRadius = '100px',
        background = 'rgba(0, 0, 0, 1)',
        class: className,
        children,
        ...restProps
    }: ShimmerButtonProps = $props();

    let styleVars = $derived.by(() => {
        return `--spread: 90deg; 
            --shimmer-color:${shimmerColor}; 
            --radius: ${borderRadius}; 
            --speed: ${shimmerDuration}; 
            --cut: ${shimmerSize}; 
            --bg: ${background};`;
    });
</script>

<button
    style={styleVars}
    class={cn(
        'group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--bg)] [border-radius:var(--radius)] dark:text-black',
        'transform-gpu transition-transform duration-300 ease-in-out active:translate-y-[1px]',
        className
    )}
    {...restProps}
>
    <!-- spark container -->
    <div class={cn('-z-30 blur-[2px]', 'absolute inset-0 overflow-visible [container-type:size]')}>
        <!-- spark -->
        <div
            class="animate-slide absolute inset-0 h-[100cqh] [aspect-ratio:1] [border-radius:0] [mask:none]"
        >
            <!-- spark before -->
            <div
                class="animate-spin-around absolute inset-[-100%] w-auto rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))] [translate:0_0]"
            ></div>
        </div>
    </div>
    {#if children}
        {@render children()}
    {/if}

    <!-- Highlight -->
    <div
        class={cn(
            'insert-0 absolute h-full w-full',

            'rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]',

            // transition
            'transform-gpu transition-all duration-300 ease-in-out',

            // on hover
            'group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]',

            // on click
            'group-active:shadow-[inset_0_-10px_10px_#ffffff3f]'
        )}
    ></div>

    <!-- backdrop -->
    <div
        class={cn(
            'absolute -z-20 [background:var(--bg)] [border-radius:var(--radius)] [inset:var(--cut)]'
        )}
    ></div>
</button>
gursheyss commented 2 months ago

Animated Shiny Text

<script lang="ts">
    import { cn } from '$lib/utils';
    import type { Snippet } from 'svelte';

    interface AnimatedShinyTextProps {
        children: Snippet;
        class?: string;
        shimmerWidth?: number;
    }

    let { children, class: className, shimmerWidth = 100 }: AnimatedShinyTextProps = $props();

    let styleVars = $derived.by(() => {
        return `--shimmer-width: ${shimmerWidth}px;`;
    });
</script>

<p
    style={styleVars}
    class={cn(
        'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',

        // Shimmer effect
        'animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',

        // Shimmer gradient
        'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent  dark:via-white/80',

        className
    )}
>
    {@render children()}
</p>
gursheyss commented 2 months ago

Will try integrating into the docs as well

gursheyss commented 2 months ago

Number ticker, had to modify it a bit to use svelte's built in motion stuff, also introduced a new prop duration for how long the animation lasts

<script lang="ts">
    import { cn } from '$lib/utils';
    import { cubicOut } from 'svelte/easing';
    import { tweened } from 'svelte/motion';

    interface NumberTickerProps {
        value: number;
        direction?: 'up' | 'down';
        class?: string;
        delay?: number;
        duration?: number;
    }

    let {
        value,
        direction = 'up',
        class: className = '',
        delay = 0,
        duration = 4000
    }: NumberTickerProps = $props();

    const count = tweened(direction === 'down' ? value : 0, {
        duration: duration,
        easing: cubicOut,
        delay: delay
    });

    $effect.pre(() => {
        if (direction === 'down') {
            count.set(0);
        }
    });

    $effect(() => {
        if (value !== $count) {
            if (direction === 'down') {
                count.set(0);
            } else {
                count.set(value);
            }
        }
    });

    let formattedCount = $derived(Intl.NumberFormat('en-US').format(Math.round($count)));
</script>

<span class={cn('inline-block tabular-nums text-black dark:text-white', className)}>
    {formattedCount}
</span>
dillionverma commented 2 months ago

love it 🫶

gursheyss commented 2 months ago

Marquee

<script lang="ts">
    import { cn } from '$lib/utils';
    import type { Snippet } from 'svelte';

    interface MarqueeProps {
        class?: string;
        reverse?: boolean;
        pauseOnHover?: boolean;
        children: Snippet;
        vertical?: boolean;
        repeat?: number;
        [key: string]: any;
    }

    let {
        class: className,
        reverse,
        pauseOnHover = false,
        children,
        vertical = false,
        repeat = 4,
        ...restProps
    }: MarqueeProps = $props();
</script>

<div
    {...restProps}
    class={cn(
        'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]',
        {
            'flex-row': !vertical,
            'flex-col': vertical
        },
        className
    )}
>
    {#each Array(repeat).fill(0) as _}
        <div
            class={cn('flex shrink-0 justify-around [gap:var(--gap)]', {
                'animate-marquee flex-row': !vertical,
                'animate-marquee-vertical flex-col': vertical,
                'group-hover:[animation-play-state:paused]': pauseOnHover,
                '[animation-direction:reverse]': reverse
            })}
        >
            {@render children()}
        </div>
    {/each}
</div>
gursheyss commented 2 months ago

Bentocard, had to change it a little bit

Bentocard

<script lang="ts">
    import { cn } from '$lib/utils';
    import { ArrowRightIcon } from 'lucide-svelte';
    import type { ComponentType, Snippet } from 'svelte';
    import { Button } from '$lib/components/ui/button';

    interface BentoCardProps {
        name: string;
        class?: string;
        children: Snippet;
        Icon: ComponentType;
        description: string;
        href: string;
        cta: string;
    }

    let { name, class: className, children, Icon, description, href, cta }: BentoCardProps = $props();
</script>

<div
    class={cn(
        'group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl',
        // light styles
        'bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]',
        // dark styles
        'transform-gpu dark:bg-black dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]',
        className
    )}
>
    <div>{@render children()}</div>
    <div
        class="pointer-events-none z-10 flex transform-gpu flex-col gap-1 p-6 transition-all duration-300 group-hover:-translate-y-10"
    >
        <Icon
            class="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75"
        />
        <h3 class="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
            {name}
        </h3>
        <p class="max-w-lg text-neutral-400">{description}</p>
    </div>

    <div
        class="absolute bottom-0 flex w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100"
    >
        <Button variant="ghost" {href} class="flex justify-between">
            {cta}
            <ArrowRightIcon class="ml-1 mt-1 h-4 w-4" />
        </Button>
    </div>
    <div
        class="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10"
    ></div>
</div>

Bentogrid

<script lang="ts">
    import { cn } from '$lib/utils';
    import type { Snippet } from 'svelte';

    let { children, class: className }: { children: Snippet; class?: string } = $props();
</script>

<div class={cn('grid w-full auto-rows-[22rem] grid-cols-3 gap-4', className)}>
    {@render children()}
</div>

Usage

<script lang="ts">
    import BentoCard from '$lib/components/magicui/BentoCard.svelte';
    import BentoGrid from '$lib/components/magicui/BentoGrid.svelte';

    import { Bell, Calendar, FileText, Globe, Input } from 'svelte-radix';
    import TestImage from './TestImage.svelte';

    const features = [
        {
            Icon: FileText,
            name: 'Save your files',
            description: 'We automatically save your files as you type.',
            href: '/',
            cta: 'Learn more',
            background: TestImage,
            class: 'lg:row-start-1 lg:row-end-4 lg:col-start-2 lg:col-end-3'
        },
        {
            Icon: Input,
            name: 'Full text search',
            description: 'Search through all your files in one place.',
            href: '/',
            cta: 'Learn more',
            background: TestImage,
            class: 'lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-3'
        },
        {
            Icon: Globe,
            name: 'Multilingual',
            description: 'Supports 100+ languages and counting.',
            href: '/',
            cta: 'Learn more',
            background: TestImage,
            class: 'lg:col-start-1 lg:col-end-2 lg:row-start-3 lg:row-end-4'
        },
        {
            Icon: Calendar,
            name: 'Calendar',
            description: 'Use the calendar to filter your files by date.',
            href: '/',
            cta: 'Learn more',
            background: TestImage,
            class: 'lg:col-start-3 lg:col-end-3 lg:row-start-1 lg:row-end-2'
        },
        {
            Icon: Bell,
            name: 'Notifications',
            description: 'Get notified when someone shares a file or mentions you in a comment.',
            href: '/',
            cta: 'Learn more',
            background: TestImage,
            class: 'lg:col-start-3 lg:col-end-3 lg:row-start-2 lg:row-end-4'
        }
    ];
</script>

<BentoGrid class="lg:grid-rows-3">
    {#each features as feature}
        <BentoCard {...feature}>
            <svelte:component this={feature.background}></svelte:component>
        </BentoCard>
    {/each}
</BentoGrid>