feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.

This commit is contained in:
Rotimi Best
2026-04-12 08:58:41 +01:00
parent 5a8f50f67b
commit d6e812d08b
573 changed files with 31964 additions and 8708 deletions
@@ -0,0 +1,62 @@
<script lang="ts">
import { Motion, AnimatePresence } from 'svelte-motion';
import { inview } from 'svelte-inview';
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
duration?: number;
delay?: number;
yOffset?: number;
inViewMargin?: string;
blur?: string;
once?: boolean;
class?: string;
children?: Snippet;
}
let {
duration = 0.4,
delay = 0,
yOffset = 6,
inViewMargin = '-50px',
blur = '6px',
once = false,
class: className = '',
children
}: Props = $props();
const id = crypto.randomUUID().slice(0, 8);
let isInView = $state<'hidden' | 'visible'>('hidden');
const defaultVariants = $derived({
hidden: { opacity: 0, y: yOffset, filter: `blur(${blur})` },
visible: { opacity: 1, y: 0, filter: `blur(0px)` }
});
</script>
<AnimatePresence let:item list={[{ key: id }]}>
<Motion
initial="hidden"
animate={isInView}
exit="hidden"
variants={defaultVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: 'easeOut'
}}
let:motion
>
<div
use:inview={{ rootMargin: inViewMargin, unobserveOnEnter: once }}
use:motion
oninview_change={({ detail }) => {
isInView = detail.inView ? 'visible' : 'hidden';
}}
class={cn(className)}
>
{@render children?.()}
</div>
</Motion>
</AnimatePresence>
@@ -0,0 +1 @@
export { default as BlurFade } from './blurfade.svelte';
@@ -0,0 +1,34 @@
<script lang="ts">
import { cn } from '../../../tools';
import { Motion } from 'svelte-motion';
import type { Snippet } from 'svelte';
interface Props {
variant?: {
hidden: { filter: string; opacity: number };
visible: { filter: string; opacity: number };
};
duration?: number;
class?: string;
children?: Snippet;
}
let {
variant,
duration = 1,
class: className = 'text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-20',
children
}: Props = $props();
const defaultVariants = {
hidden: { filter: 'blur(10px)', opacity: 0 },
visible: { filter: 'blur(0px)', opacity: 1 }
};
const combinedVariants = $derived(variant ?? defaultVariants);
</script>
<Motion initial="hidden" animate="visible" transition={{ duration }} variants={combinedVariants} let:motion>
<h1 class={cn(className)} use:motion>
{@render children?.()}
</h1>
</Motion>
@@ -0,0 +1 @@
export { default as BlurIn } from './blurin.svelte';
@@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from '../../../tools';
interface Props {
size?: number;
duration?: number;
anchor?: number;
borderWidth?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
class?: string;
}
let {
size = 200,
duration = 15,
anchor = 90,
borderWidth = 1.5,
colorFrom = '#ffaa40',
colorTo = '#9c40ff',
delay = 0,
class: className = ''
}: Props = $props();
</script>
<div
style:--border-width={borderWidth}
style:--size={size}
style:--color-from={colorFrom}
style:--color-to={colorTo}
style:--delay="{delay}s"
style:--anchor={anchor}
style:--duration={duration}
class={cn('cio-marketing-border-beam', className)}
></div>
@@ -0,0 +1 @@
export { default as BorderBeam } from './border-beam.svelte';
@@ -0,0 +1,15 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
className?: string;
children: Snippet;
}
let { children, className }: Props = $props();
</script>
<div class={cn('p-4 text-left', className)}>
{@render children()}
</div>
@@ -0,0 +1,28 @@
<script lang="ts">
import { cn } from '../../../tools';
import Ellipses from './ellipses.svelte';
import type { Snippet } from 'svelte';
interface Props {
className?: string;
children?: Snippet;
}
let { className, children }: Props = $props();
</script>
<div
class={cn(
'relative mx-auto w-full border border-dashed border-zinc-300 px-8 shadow-sm sm:px-6 md:px-8 dark:border-zinc-800 dark:bg-[#09090B]',
className
)}
>
<div class="absolute top-4 left-0 z-0 h-px w-full bg-zinc-400 sm:top-6 md:top-8 dark:bg-zinc-700"></div>
<div class="absolute bottom-4 left-0 z-0 h-px w-full bg-zinc-400 sm:bottom-6 md:bottom-8 dark:bg-zinc-700"></div>
<div class="relative w-full border-x border-zinc-400 dark:border-zinc-700">
<Ellipses />
<div class="relative z-20 mx-auto py-8">
{@render children?.()}
</div>
</div>
</div>
@@ -0,0 +1,13 @@
<script lang="ts">
const sharedClass =
'rounded-full outline outline-8 dark:outline-gray-950 sm:my-6 md:my-8 size-1 my-4 outline-gray-50 bg-green-400';
</script>
<div class="absolute z-0 grid h-full w-full items-center gap-8 lg:grid-cols-2">
<section class="absolute z-0 grid h-full w-full grid-cols-2 place-content-between">
<div class={`${sharedClass} -mx-[2.5px]`}></div>
<div class={`${sharedClass} -mx-[2px] place-self-end`}></div>
<div class={`${sharedClass} -mx-[2.5px]`}></div>
<div class={`${sharedClass} -mx-[2px] place-self-end`}></div>
</section>
</div>
@@ -0,0 +1,2 @@
export { default as CardContainer } from './container.svelte';
export { default as CardBody } from './body.svelte';
@@ -0,0 +1,45 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { SVGAttributes } from 'svelte/elements';
interface Props extends SVGAttributes<SVGSVGElement> {
width?: string | number;
height?: string | number;
x?: number;
y?: number;
cx?: number;
cy?: number;
cr?: number;
fillColor?: string;
class?: string;
}
let {
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
fillColor = 'rgb(163 163 163 / 0.8)',
class: className = '',
...restProps
}: Props = $props();
const patternId = crypto.randomUUID().toString().slice(0, 10);
</script>
<svg
aria-hidden="true"
class={cn('pointer-events-none absolute inset-0 h-full w-full', className)}
{...restProps}
fill={fillColor}
>
<defs>
<pattern id={patternId} {width} {height} patternUnits="userSpaceOnUse" patternContentUnits="userSpaceOnUse" {x} {y}>
<circle id="pattern-circle" {cx} {cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" stroke-width={0} fill={`url(#${patternId})`} />
</svg>
@@ -0,0 +1 @@
export { default as DotPattern } from './dot-pattern.svelte';
@@ -0,0 +1,57 @@
<script lang="ts">
import { cn } from '../../../tools';
import { AnimatePresence, Motion } from 'svelte-motion';
interface Props {
class?: string;
/** Applied to the flex row wrapper (e.g. alignment). */
containerClass?: string;
words?: string;
duration?: number;
delayMultiple?: number;
framerProps?: {
hidden: { opacity: number; x: number };
visible: { opacity: number; x: number };
};
}
let {
class: className = 'drop-shadow-sm',
containerClass = '',
words = 'Gradual Spacing',
duration = 0.5,
delayMultiple = 0.04,
framerProps = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
}
}: Props = $props();
const wordsspilit = $derived(words.split(''));
</script>
<div class={cn('flex justify-center space-x-1', containerClass)}>
<AnimatePresence let:item list={[{ key: wordsspilit }]}>
{#each wordsspilit as char, i}
<Motion
initial="hidden"
animate="visible"
exit="hidden"
variants={framerProps}
transition={{
duration,
delay: i * delayMultiple
}}
let:motion
>
<span use:motion class={cn(className)}>
{#if char === ' '}
<span>&nbsp;</span>
{:else}
{char}
{/if}
</span>
</Motion>
{/each}
</AnimatePresence>
</div>
@@ -0,0 +1 @@
export { default as GradualSpacing } from './gradual-spacing.svelte';
@@ -0,0 +1,171 @@
<script lang="ts">
import { AnimatePresence, Motion } from 'svelte-motion';
import { Play, X } from '@lucide/svelte';
type AnimationStyle =
| 'from-bottom'
| 'from-center'
| 'from-top'
| 'from-left'
| 'from-right'
| 'fade'
| 'top-in-bottom-out'
| 'left-in-right-out';
interface Props {
animationStyle?: AnimationStyle;
videoSrc: string;
thumbnailSrc: string;
thumbnailAlt?: string;
iconColor?: string;
}
let {
animationStyle = 'from-center',
videoSrc,
thumbnailSrc,
thumbnailAlt = 'Video thumbnail',
iconColor = 'white'
}: Props = $props();
let isVideoOpen = $state(false);
let isCloseHovered = $state(false);
let isPlayHovered = $state(false);
function openVideo() {
isVideoOpen = true;
}
function closeVideo() {
isVideoOpen = false;
}
const animationVariants: Record<
AnimationStyle,
{ initial: Record<string, number>; animate: Record<string, number>; exit: Record<string, number> }
> = {
'from-bottom': {
initial: { y: 100, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: 100, opacity: 0 }
},
'from-center': {
initial: { scale: 0.5, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 0.5, opacity: 0 }
},
'from-top': {
initial: { y: -100, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -100, opacity: 0 }
},
'from-left': {
initial: { x: -100, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: -100, opacity: 0 }
},
'from-right': {
initial: { x: 100, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: 100, opacity: 0 }
},
fade: {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 }
},
'top-in-bottom-out': {
initial: { y: -100, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: 200, opacity: 0 }
},
'left-in-right-out': {
initial: { x: -100, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: 200, opacity: 0 }
}
};
const selectedAnimation = $derived(animationVariants[animationStyle]);
</script>
<div class="relative">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="relative cursor-pointer" onclick={openVideo} role="presentation">
<img
src={thumbnailSrc}
alt={thumbnailAlt}
width={1920}
height={1080}
class="w-full rounded-2xl transition-all duration-200"
/>
<div class="absolute inset-0 flex items-center justify-center">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex size-24 items-center justify-center rounded-full border border-neutral-400 bg-black backdrop-blur-md transition-transform duration-300 ease-out"
onmouseenter={() => (isPlayHovered = true)}
onmouseleave={() => (isPlayHovered = false)}
role="presentation"
>
<div
class="relative flex size-20 items-center justify-center rounded-full border border-neutral-400 bg-black backdrop-blur-2xl transition-all duration-300 ease-out"
>
<span
class="inline-flex size-8 items-center justify-center transition-transform duration-300 ease-out"
style:transform={`scale(${isPlayHovered ? 1.1 : 1})`}
style:color={iconColor}
>
<Play strokeWidth={1.7} class="size-full" />
</span>
</div>
</div>
</div>
</div>
<AnimatePresence let:item list={[{ key: isVideoOpen }]}>
{#if item.key}
<Motion initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} let:motion>
<div use:motion class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md">
<Motion
initial={selectedAnimation.initial}
animate={selectedAnimation.animate}
exit={selectedAnimation.exit}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
let:motion
>
<div use:motion class="relative mx-4 aspect-video w-full max-w-4xl md:mx-0">
<Motion let:motion whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
<button
type="button"
use:motion
class="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md"
onclick={closeVideo}
onmouseenter={() => (isCloseHovered = true)}
onmouseleave={() => (isCloseHovered = false)}
>
<X class="size-5 text-white" />
</button>
</Motion>
<Motion animate={{ scale: isCloseHovered ? 0.98 : 1 }} transition={{ duration: 0.2 }} let:motion>
<div
use:motion
class="relative isolate z-1 size-full overflow-hidden rounded-2xl border-2 border-white"
>
<!-- svelte-ignore a11y_missing_attribute -->
<iframe
src={videoSrc}
class="size-full rounded-2xl"
allowfullscreen
title="Video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
</Motion>
</div>
</Motion>
</div>
</Motion>
{/if}
</AnimatePresence>
</div>
@@ -0,0 +1 @@
export { default as HeroVideoDialog } from './hero-video-dialog.svelte';
+13
View File
@@ -0,0 +1,13 @@
export * from './border-beam';
export * from './dot-pattern';
export * from './card';
export * from './blurfade';
export * from './blurin';
export * from './meteors';
export * from './shimmerbutton';
export * from './sparkle';
export * from './hero-video-dialog';
export * from './shinny-text';
export * from './magic-card';
export * from './gradual-spacing';
export * from './square-card';
@@ -0,0 +1 @@
export { default as MagicCard } from './magic-card.svelte';
@@ -0,0 +1,78 @@
<script lang="ts">
import { Motion, useMotionValue, useMotionTemplate } from 'svelte-motion';
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
gradientSize?: number;
gradientColor?: string;
gradientOpacity?: number;
class?: string;
children?: Snippet;
}
let {
gradientSize = 200,
gradientColor = '#262626',
gradientOpacity = 0.8,
class: className = '',
children
}: Props = $props();
const gradSize = useMotionValue(200);
const gradColor = useMotionValue('#262626');
const mouseX = useMotionValue(-200);
const mouseY = useMotionValue(-200);
$effect(() => {
gradSize.set(gradientSize);
gradColor.set(gradientColor);
mouseX.set(-gradientSize);
mouseY.set(-gradientSize);
});
function handleMouseMove(e: MouseEvent) {
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
mouseX.set(e.clientX - rect.left);
mouseY.set(e.clientY - rect.top);
}
function handleMouseLeave() {
mouseX.set(-gradientSize);
mouseY.set(-gradientSize);
}
const bg = useMotionTemplate`radial-gradient(${gradSize}px circle at ${mouseX}px ${mouseY}px, ${gradColor}, transparent 100%)`;
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
class={cn(
'group relative flex size-full justify-center overflow-hidden rounded-xl border bg-neutral-100 py-4 text-black dark:bg-neutral-900 dark:text-white',
className
)}
>
<div class="relative z-10">
{#if children}
{@render children()}
{:else}
<div class="flex h-full items-center justify-center text-center">
<p class="text-2xl">Magic Card</p>
</div>
{/if}
</div>
<Motion
style={{
background: bg,
opacity: gradientOpacity
}}
let:motion
>
<div
use:motion
class="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div>
</Motion>
</div>
@@ -0,0 +1,81 @@
/* Shared by @cio/ui index.css and course-app (no Tailwind prefix; no global theme reset). */
@keyframes border-beam {
100% {
offset-distance: 100%;
}
}
@keyframes meteor {
0% {
transform: rotate(215deg) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(215deg) translateX(-500px);
opacity: 0;
}
}
@keyframes shimmer-marketing {
0%,
90%,
100% {
background-position: calc(-100% - var(--shimmer-width)) 0;
}
30%,
60% {
background-position: calc(100% + var(--shimmer-width)) 0;
}
}
@keyframes spin-around {
0% {
transform: translateZ(0) rotate(0);
}
15%,
35% {
transform: translateZ(0) rotate(90deg);
}
65%,
85% {
transform: translateZ(0) rotate(270deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
@keyframes magicslide {
to {
transform: translate(calc(100cqw - 100%), 0);
}
}
@layer components {
.cio-marketing-border-beam {
pointer-events: none;
position: absolute;
inset: 0;
border-radius: inherit;
border: calc(var(--border-width) * 1px) solid transparent;
mask: linear-gradient(transparent, transparent), linear-gradient(white, white);
mask-clip: padding-box, border-box;
mask-composite: intersect;
}
.cio-marketing-border-beam::after {
content: '';
position: absolute;
aspect-ratio: 1;
width: calc(var(--size) * 1px);
animation-delay: var(--delay);
background: linear-gradient(to left, var(--color-from), var(--color-to), transparent);
offset-anchor: calc(var(--anchor) * 1%) 50%;
offset-path: rect(0 auto auto 0 round calc(var(--size) * 1px));
animation: border-beam calc(var(--duration) * 1s) infinite linear;
}
}
@@ -0,0 +1 @@
export { default as Meteors } from './meteors.svelte';
@@ -0,0 +1,48 @@
<script lang="ts">
import { cn } from '../../../tools';
interface Props {
number?: number;
}
let { number = 10 }: Props = $props();
type MeteorStyle = {
top: number;
left: string;
animationDelay: string;
animationDuration: string;
};
let meteorStyles = $state<MeteorStyle[]>([]);
function changeMeteors(num: number) {
meteorStyles = [...Array(num)].map(() => ({
// Keep within typical card heights so parents with overflow-hidden still show meteors
top: Math.floor(Math.random() * 160) + 8,
left: Math.floor(Math.random() * 100) + '%',
animationDelay: Math.random() * 1 + 0.2 + 's',
animationDuration: Math.floor(Math.random() * 8 + 2.9) + 's'
}));
}
$effect(() => {
const count = number;
if (typeof window === 'undefined') return;
changeMeteors(count);
});
</script>
{#each meteorStyles as style, idx}
<span
id="meteor-{idx + 1}"
class={cn(
'ui:animate-meteor ui:pointer-events-none ui:absolute ui:size-[2.4px] ui:origin-center ui:rounded-full ui:bg-slate-500 ui:shadow-[0_0_0_1px_#ffffff10]'
)}
style="top: {style.top}px; left: {style.left}; animation-delay: {style.animationDelay}; animation-duration: {style.animationDuration};"
>
<div
class="ui:pointer-events-none ui:absolute ui:top-1/2 ui:-z-10 ui:h-px ui:w-[50px] ui:-translate-y-1/2 ui:bg-linear-to-r ui:from-slate-500 ui:via-blue-600/30 ui:to-transparent"
></div>
</span>
{/each}
@@ -0,0 +1 @@
export { default as ShimmerButton } from './shimmerbutton.svelte';
@@ -0,0 +1,67 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
href: string;
target?: string;
rel?: string;
shimmerColor?: string;
shimmerSize?: string;
shimmerDuration?: string;
borderRadius?: string;
background?: string;
class?: string;
children?: Snippet;
}
let {
href,
target = '',
rel = '',
shimmerColor = '#ffffff',
shimmerSize = '0.05em',
shimmerDuration = '3s',
borderRadius = '100px',
background = 'rgba(0, 0, 0, 1)',
class: className = '',
children
}: Props = $props();
</script>
<a
style:--spread="90deg"
style:--shimmer-color={shimmerColor}
style:--radius={borderRadius}
style:--speed={shimmerDuration}
style:--cut={shimmerSize}
style:--bg={background}
class={cn(
'group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden rounded-(--radius) border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)] dark:text-white',
'transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px',
className
)}
{href}
{target}
{rel}
data-rewardful
>
<div class={cn('-z-30 blur-[2px]', '@container-[size] absolute inset-0 overflow-visible')}>
<div class="animate-magicslide absolute inset-0 aspect-[1] h-[100cqh] rounded-none [mask:none]">
<div
class="animate-spin-around absolute -inset-full w-auto [translate:0_0] rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]"
></div>
</div>
</div>
{@render children?.()}
<div
class={cn(
'absolute inset-0 h-full w-full',
'rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]',
'transform-gpu transition-all duration-300 ease-in-out',
'group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]',
'group-active:shadow-[inset_0_-10px_10px_#ffffff3f]'
)}
></div>
<div class={cn('absolute inset-(--cut) -z-20 rounded-(--radius) [background:var(--bg)]')}></div>
</a>
@@ -0,0 +1 @@
export { default as ShinnyText } from './shinny-text.svelte';
@@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
shimmerWidth?: number;
class?: string;
children?: Snippet;
}
let { shimmerWidth = 100, class: className, children }: Props = $props();
</script>
<p
style:--shimmer-width="{shimmerWidth}px"
class={cn(
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50',
'animate-shimmer bg-size-[var(--shimmer-width)_100%] bg-clip-text bg-position-[0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
'bg-linear-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
className
)}
>
{@render children?.()}
</p>
@@ -0,0 +1 @@
export { default as Sparkle } from './sparkle-text.svelte';
@@ -0,0 +1,34 @@
<script lang="ts">
import { Motion } from 'svelte-motion';
interface Props {
id: string;
x: string;
y: string;
color: string;
delay: number;
scale: number;
}
let { id, x, y, color, delay, scale }: Props = $props();
</script>
<Motion
initial={{ opacity: 0, left: x, top: y }}
animate={{
opacity: [0, 1, 0],
scale: [0, scale, 0],
rotate: [75, 120, 150]
}}
transition={{ duration: 0.8, repeat: Infinity, delay }}
let:motion
>
<div {id} use:motion class="pointer-events-none absolute z-20" style:left={x} style:top={y}>
<svg width="21" height="21" viewBox="0 0 21 21" class="block">
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={color}
/>
</svg>
</div>
</Motion>
@@ -0,0 +1,72 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import SparkleParticle from './sparkle-particle.svelte';
interface TSparkle {
id: string;
x: string;
y: string;
color: string;
delay: number;
scale: number;
lifespan: number;
}
interface Props {
text?: string;
colors?: { first: string; second: string };
sparklesCount?: number;
class?: string;
}
let {
text = 'Hello World',
colors = { first: '#9E7AFF', second: '#FE8BBB' },
sparklesCount = 5,
class: className = ''
}: Props = $props();
let sparkles = $state<TSparkle[]>([]);
function generateStar(): TSparkle {
const starX = `${Math.random() * 100}%`;
const starY = `${Math.random() * 100}%`;
const color = Math.random() > 0.5 ? colors.first : colors.second;
const delay = Math.random() * 4;
const scale = Math.random() * 1 + 0.3;
const lifespan = Math.random() * 10 + 5;
const id = `${starX}-${starY}-${Date.now()}`;
return { id, x: starX, y: starY, color, delay, scale, lifespan };
}
function initializeStars() {
sparkles = Array.from({ length: sparklesCount }, generateStar);
}
function updateStars() {
sparkles = sparkles.map((star) => {
if (star.lifespan <= 0) {
return generateStar();
}
return { ...star, lifespan: star.lifespan - 0.1 };
});
}
let interval: ReturnType<typeof setInterval>;
onMount(() => {
initializeStars();
interval = setInterval(updateStars, 100);
});
onDestroy(() => clearInterval(interval));
</script>
<div style:--sparkles-first-color="{colors.first};" style:--sparkles-second-color="{colors.second};" class={className}>
<span class="relative inline-block">
{#each sparkles as item}
<SparkleParticle {...item} />
{/each}
<strong>{text}</strong>
</span>
</div>
@@ -0,0 +1,15 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { Snippet } from 'svelte';
interface Props {
className?: string;
children: Snippet;
}
let { children, className }: Props = $props();
</script>
<div class={cn('text-left', className)}>
{@render children()}
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
import { cn } from '../../../tools';
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<SVGElement> {
class?: string;
}
let { class: className = '', ...rest }: Props = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
width={24}
height={24}
stroke-width="1"
stroke-linecap="round"
stroke="currentColor"
class={cn('absolute text-black dark:text-white', className)}
{...rest}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
@@ -0,0 +1,2 @@
export { default as SquareCardBody } from './body.svelte';
export { default as SquareCardIcon } from './icon.svelte';