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:
@@ -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> </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';
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user