parent
20cfd3f50b
commit
8d5e47b2a7
@ -0,0 +1,178 @@
|
||||
<script lang="ts">
|
||||
type DemoItem = {
|
||||
name: string;
|
||||
path: string;
|
||||
status?: 'stable' | 'wip';
|
||||
};
|
||||
|
||||
type DemoGroup = {
|
||||
title: string;
|
||||
description: string;
|
||||
items: DemoItem[];
|
||||
};
|
||||
|
||||
const groups: DemoGroup[] = [
|
||||
{
|
||||
title: 'Visual / Styled',
|
||||
description: 'Componentes visuales u opinionated construidos sobre terra.',
|
||||
items: [{ name: 'Spinner', path: '/test/air/spinner', status: 'stable' }],
|
||||
},
|
||||
];
|
||||
|
||||
const totals = {
|
||||
all: groups.reduce((count, group) => count + group.items.length, 0),
|
||||
stable: groups.reduce(
|
||||
(count, group) => count + group.items.filter((item) => item.status !== 'wip').length,
|
||||
0,
|
||||
),
|
||||
wip: groups.reduce(
|
||||
(count, group) => count + group.items.filter((item) => item.status === 'wip').length,
|
||||
0,
|
||||
),
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Air Tests</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">Air</p>
|
||||
<h1>Test hub</h1>
|
||||
<p class="lead">Punto único para revisar los componentes visuales construidos sobre terra.</p>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Total</span>
|
||||
<strong>{totals.all}</strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Estables</span>
|
||||
<strong>{totals.stable}</strong>
|
||||
</div>
|
||||
<div class="stat stat--wip">
|
||||
<span class="stat-label">WIP</span>
|
||||
<strong>{totals.wip}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="top-links">
|
||||
<a href="/test">Indice general</a>
|
||||
<a href="/test/terra">Terra hub</a>
|
||||
<a href="/test/air/spinner">Spinner</a>
|
||||
</nav>
|
||||
|
||||
<main class="grid">
|
||||
{#each groups as group}
|
||||
<section class="card">
|
||||
<header class="card-header">
|
||||
<h2>{group.title}</h2>
|
||||
<p>{group.description}</p>
|
||||
</header>
|
||||
|
||||
<div class="links">
|
||||
{#each group.items as item}
|
||||
<a class="link" href={item.path}>
|
||||
<span>{item.name}</span>
|
||||
<span class:badge={true} class:badge--wip={item.status === 'wip'}>
|
||||
{item.status === 'wip' ? 'WIP' : 'OK'}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(*, *::before, *::after) { box-sizing: border-box; }
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
background: #111827;
|
||||
color: #f3f4f6;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
}
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(31, 41, 55, 0.98), rgba(17, 24, 39, 0.98));
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 .35rem;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: #93c5fd;
|
||||
}
|
||||
h1 { margin: 0; font-size: 2rem; line-height: 1.1; }
|
||||
.lead { max-width: 60ch; margin: .65rem 0 0; color: #cbd5e1; line-height: 1.5; }
|
||||
.stats { display: flex; gap: .75rem; flex-wrap: wrap; }
|
||||
.stat {
|
||||
min-width: 96px;
|
||||
padding: .85rem 1rem;
|
||||
border-radius: 14px;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
.stat-label { display: block; font-size: .72rem; color: #cbd5e1; margin-bottom: .25rem; }
|
||||
.stat strong { font-size: 1.15rem; }
|
||||
.stat--wip strong { color: #fbbf24; }
|
||||
.top-links { display: flex; gap: .75rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
|
||||
.top-links a,
|
||||
.link { color: inherit; text-decoration: none; }
|
||||
.top-links a {
|
||||
padding: .55rem .85rem;
|
||||
border-radius: 999px;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.top-links a:hover,
|
||||
.link:hover { border-color: #93c5fd; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
padding: 1.15rem;
|
||||
border-radius: 18px;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
.card-header h2 { margin: 0; font-size: 1rem; }
|
||||
.card-header p { margin: .45rem 0 0; font-size: .88rem; line-height: 1.45; color: #cbd5e1; }
|
||||
.links { display: flex; flex-direction: column; gap: .6rem; margin-top: 1rem; }
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .75rem;
|
||||
padding: .75rem .85rem;
|
||||
border-radius: 14px;
|
||||
background: #111827;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
.badge {
|
||||
padding: .18rem .48rem;
|
||||
border-radius: 999px;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
background: rgba(34, 197, 94, .14);
|
||||
color: #86efac;
|
||||
}
|
||||
.badge--wip {
|
||||
background: rgba(251, 191, 36, .14);
|
||||
color: #fbbf24;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
<script lang="ts">
|
||||
import { Spinner } from '$uix/air';
|
||||
|
||||
let size = $state<'sm' | 'md' | 'lg' | 'xl'>('md');
|
||||
let speed = $state<'slow' | 'normal' | 'fast'>('normal');
|
||||
let variant = $state<'circular' | 'dots' | 'bars' | 'pulse'>('circular');
|
||||
|
||||
const variants = ['circular', 'dots', 'bars', 'pulse'] as const;
|
||||
const sizes = ['sm', 'md', 'lg', 'xl'] as const;
|
||||
const speeds = ['slow', 'normal', 'fast'] as const;
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<h1>Spinner</h1>
|
||||
|
||||
<section class="card">
|
||||
<h2>Playground</h2>
|
||||
<div class="playground">
|
||||
<Spinner.Root variant={variant} size={size} speed={speed} class="spinner spinner--hero" />
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>Variant</span>
|
||||
<div class="chips">
|
||||
{#each variants as item}
|
||||
<button class:active={variant === item} onclick={() => (variant = item)}>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<span>Size</span>
|
||||
<div class="chips">
|
||||
{#each sizes as item}
|
||||
<button class:active={size === item} onclick={() => (size = item)}>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<span>Speed</span>
|
||||
<div class="chips">
|
||||
{#each speeds as item}
|
||||
<button class:active={speed === item} onclick={() => (speed = item)}>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Variants</h2>
|
||||
<div class="grid grid--variants">
|
||||
{#each variants as item}
|
||||
<div class="demo">
|
||||
<Spinner.Root variant={item} size="lg" class="spinner" label={`Loading (${item})`} />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Custom</h2>
|
||||
<div class="grid grid--variants">
|
||||
<Spinner.Root size="lg" class="spinner">
|
||||
<Spinner.Circular size="lg" speed="fast" />
|
||||
</Spinner.Root>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sizes</h2>
|
||||
<div class="grid grid--sizes">
|
||||
{#each sizes as item}
|
||||
<div class="demo">
|
||||
<Spinner.Root variant="circular" size={item} class="spinner" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>In context</h2>
|
||||
<div class="panel">
|
||||
<button class="action" disabled>Saving <Spinner.Root variant="dots" size="sm" speed="fast" class="spinner spinner--inline" /></button>
|
||||
<div class="status">
|
||||
<Spinner.Root variant="pulse" size="sm" class="spinner spinner--success" />
|
||||
<span>Syncing background data</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
font-family: 'DM Mono', monospace;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: .05em; }
|
||||
.card {
|
||||
border: 1.5px solid #e0ddd6;
|
||||
border-radius: 4px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
h2 { font-size: .65rem; font-weight: 700; letter-spacing: .2em; text-transform: uppercase; color: #999; }
|
||||
.playground {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.controls { display: flex; flex-direction: column; gap: .85rem; }
|
||||
.control { display: flex; flex-direction: column; gap: .35rem; }
|
||||
.control span { font-size: .72rem; color: #777; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: .4rem; }
|
||||
button {
|
||||
padding: .35rem .7rem;
|
||||
border: 1px solid #d6d1c7;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
font: inherit;
|
||||
font-size: .72rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.active { background: #111827; color: #fff; border-color: #111827; }
|
||||
.grid { display: grid; gap: 1rem; }
|
||||
.grid--variants { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.grid--sizes { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .75rem;
|
||||
padding: 1rem;
|
||||
border: 1px dashed #ddd6c9;
|
||||
border-radius: 4px;
|
||||
min-height: 110px;
|
||||
}
|
||||
.demo span { font-size: .72rem; color: #777; }
|
||||
.panel { display: flex; flex-direction: column; gap: .9rem; }
|
||||
.action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .55rem;
|
||||
padding: .7rem 1rem;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
opacity: .9;
|
||||
cursor: default;
|
||||
}
|
||||
.status { display: inline-flex; align-items: center; gap: .6rem; font-size: .82rem; color: #444; }
|
||||
:global(.spinner) { display: inline-flex; color: #111827; }
|
||||
:global(.spinner--hero) { color: #2563eb; }
|
||||
:global(.spinner--inline) { color: currentColor; }
|
||||
:global(.spinner--success) { color: #16a34a; }
|
||||
@media (max-width: 720px) {
|
||||
.playground { grid-template-columns: 1fr; }
|
||||
.grid--variants, .grid--sizes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * as Spinner from './spinner';
|
||||
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from '$terra/utils';
|
||||
import { resolveSpinnerSizeToken, resolveSpinnerSpeedToken, useSpinnerRootState } from '$terra/spinner/spinner.svelte';
|
||||
import type { SpinnerBarsProps } from '$terra/spinner/types';
|
||||
|
||||
let { size = undefined, speed = undefined, ...restProps }: SpinnerBarsProps = $props();
|
||||
|
||||
const rootState = useSpinnerRootState();
|
||||
const resolvedSize = $derived(resolveSpinnerSizeToken(size ?? rootState?.opts.size.current));
|
||||
const resolvedSpeed = $derived(resolveSpinnerSpeedToken(speed ?? rootState?.opts.speed.current));
|
||||
const mergedProps = $derived(mergeProps(restProps, {
|
||||
class: 'bars',
|
||||
'aria-hidden': true,
|
||||
'data-size': resolvedSize,
|
||||
'data-speed': resolvedSpeed,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div {...mergedProps}>
|
||||
{#each [0, 1, 2, 3, 4] as _item, i}
|
||||
<span data-index={i}></span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bars { display: inline-flex; align-items: center; height: var(--spinner-size); gap: var(--spinner-gap); }
|
||||
.bars span {
|
||||
width: var(--spinner-bar-width);
|
||||
border-radius: 2px;
|
||||
background: currentColor;
|
||||
animation: bars-scale var(--spinner-speed) ease-in-out infinite;
|
||||
}
|
||||
.bars span[data-index='1'] { animation-delay: calc(var(--spinner-speed) * 0.1); }
|
||||
.bars span[data-index='2'] { animation-delay: calc(var(--spinner-speed) * 0.2); }
|
||||
.bars span[data-index='3'] { animation-delay: calc(var(--spinner-speed) * 0.3); }
|
||||
.bars span[data-index='4'] { animation-delay: calc(var(--spinner-speed) * 0.4); }
|
||||
.bars[data-size='sm'] { --spinner-size: 16px; --spinner-gap: 2px; --spinner-bar-width: 2px; }
|
||||
.bars[data-size='md'] { --spinner-size: 24px; --spinner-gap: 2px; --spinner-bar-width: 3px; }
|
||||
.bars[data-size='lg'] { --spinner-size: 32px; --spinner-gap: 3px; --spinner-bar-width: 4px; }
|
||||
.bars[data-size='xl'] { --spinner-size: 48px; --spinner-gap: 5px; --spinner-bar-width: 6px; }
|
||||
.bars[data-speed='slow'] { --spinner-speed: 1.4s; }
|
||||
.bars[data-speed='normal'] { --spinner-speed: 0.8s; }
|
||||
.bars[data-speed='fast'] { --spinner-speed: 0.45s; }
|
||||
@keyframes bars-scale {
|
||||
0%, 100% { height: 30%; opacity: 0.4; }
|
||||
50% { height: 100%; opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from '$terra/utils';
|
||||
import { resolveSpinnerSizePx, resolveSpinnerSpeedToken, useSpinnerRootState } from '$terra/spinner/spinner.svelte';
|
||||
import type { SpinnerCircularProps } from '$terra/spinner/types';
|
||||
|
||||
let {
|
||||
size = undefined,
|
||||
speed = undefined,
|
||||
...restProps
|
||||
}: SpinnerCircularProps = $props();
|
||||
|
||||
const rootState = useSpinnerRootState();
|
||||
const resolvedSize = $derived(resolveSpinnerSizePx(size ?? rootState?.opts.size.current));
|
||||
const resolvedSpeed = $derived(resolveSpinnerSpeedToken(speed ?? rootState?.opts.speed.current));
|
||||
const radius = 10;
|
||||
const strokeWidth = 2.5;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, {
|
||||
class: 'circular',
|
||||
'aria-hidden': true,
|
||||
'data-speed': resolvedSpeed,
|
||||
width: resolvedSize,
|
||||
height: resolvedSize,
|
||||
viewBox: '0 0 24 24',
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<svg {...mergedProps}>
|
||||
<circle cx="12" cy="12" r={radius} fill="none" stroke="currentColor" stroke-width={strokeWidth} opacity="0.2" />
|
||||
<circle
|
||||
class="spinner-arc"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray={circumference}
|
||||
stroke-dashoffset={circumference * 0.75}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.circular { display: inline-block; }
|
||||
.circular[data-speed='slow'] { --spinner-speed: 1.4s; }
|
||||
.circular[data-speed='normal'] { --spinner-speed: 0.8s; }
|
||||
.circular[data-speed='fast'] { --spinner-speed: 0.45s; }
|
||||
.spinner-arc {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: spinner-rotate var(--spinner-speed) linear infinite;
|
||||
}
|
||||
@keyframes spinner-rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from '$terra/utils';
|
||||
import { resolveSpinnerSizeToken, resolveSpinnerSpeedToken, useSpinnerRootState } from '$terra/spinner/spinner.svelte';
|
||||
import type { SpinnerDotsProps } from '$terra/spinner/types';
|
||||
|
||||
let { size = undefined, speed = undefined, ...restProps }: SpinnerDotsProps = $props();
|
||||
|
||||
const rootState = useSpinnerRootState();
|
||||
const resolvedSize = $derived(resolveSpinnerSizeToken(size ?? rootState?.opts.size.current));
|
||||
const resolvedSpeed = $derived(resolveSpinnerSpeedToken(speed ?? rootState?.opts.speed.current));
|
||||
const mergedProps = $derived(mergeProps(restProps, {
|
||||
class: 'dots',
|
||||
'aria-hidden': true,
|
||||
'data-size': resolvedSize,
|
||||
'data-speed': resolvedSpeed,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div {...mergedProps}>
|
||||
<span data-index="0"></span>
|
||||
<span data-index="1"></span>
|
||||
<span data-index="2"></span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dots { display: inline-flex; align-items: center; height: var(--spinner-size); gap: var(--spinner-gap); }
|
||||
.dots span {
|
||||
width: var(--spinner-dot-size);
|
||||
height: var(--spinner-dot-size);
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
animation: dots-bounce var(--spinner-speed) ease-in-out infinite;
|
||||
}
|
||||
.dots span[data-index='1'] { animation-delay: calc(var(--spinner-speed) * 0.16); }
|
||||
.dots span[data-index='2'] { animation-delay: calc(var(--spinner-speed) * 0.32); }
|
||||
.dots[data-size='sm'] { --spinner-size: 16px; --spinner-gap: 3px; --spinner-dot-size: 4px; }
|
||||
.dots[data-size='md'] { --spinner-size: 24px; --spinner-gap: 3px; --spinner-dot-size: 5px; }
|
||||
.dots[data-size='lg'] { --spinner-size: 32px; --spinner-gap: 4px; --spinner-dot-size: 7px; }
|
||||
.dots[data-size='xl'] { --spinner-size: 48px; --spinner-gap: 7px; --spinner-dot-size: 11px; }
|
||||
.dots[data-speed='slow'] { --spinner-speed: 1.4s; }
|
||||
.dots[data-speed='normal'] { --spinner-speed: 0.8s; }
|
||||
.dots[data-speed='fast'] { --spinner-speed: 0.45s; }
|
||||
@keyframes dots-bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,5 @@
|
||||
export { default as Root } from './spinner.svelte';
|
||||
export { default as Circular } from './circular.svelte';
|
||||
export { default as Dots } from './dots.svelte';
|
||||
export { default as Bars } from './bars.svelte';
|
||||
export { default as Pulse } from './pulse.svelte';
|
||||
@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from '$terra/utils';
|
||||
import { resolveSpinnerSizeToken, resolveSpinnerSpeedToken, useSpinnerRootState } from '$terra/spinner/spinner.svelte';
|
||||
import type { SpinnerPulseProps } from '$terra/spinner/types';
|
||||
|
||||
let { size = undefined, speed = undefined, ...restProps }: SpinnerPulseProps = $props();
|
||||
|
||||
const rootState = useSpinnerRootState();
|
||||
const resolvedSize = $derived(resolveSpinnerSizeToken(size ?? rootState?.opts.size.current));
|
||||
const resolvedSpeed = $derived(resolveSpinnerSpeedToken(speed ?? rootState?.opts.speed.current));
|
||||
const mergedProps = $derived(mergeProps(restProps, {
|
||||
class: 'pulse',
|
||||
'aria-hidden': true,
|
||||
'data-size': resolvedSize,
|
||||
'data-speed': resolvedSpeed,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div {...mergedProps}>
|
||||
<span class="ring ring-1"></span>
|
||||
<span class="ring ring-2"></span>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pulse {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--spinner-size);
|
||||
height: var(--spinner-size);
|
||||
}
|
||||
.ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid currentColor;
|
||||
opacity: 0;
|
||||
animation: pulse-ring var(--spinner-speed) ease-out infinite;
|
||||
}
|
||||
.ring-2 { animation-delay: calc(var(--spinner-speed) * 0.4); }
|
||||
.dot {
|
||||
width: 28%;
|
||||
height: 28%;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.pulse[data-size='sm'] { --spinner-size: 16px; }
|
||||
.pulse[data-size='md'] { --spinner-size: 24px; }
|
||||
.pulse[data-size='lg'] { --spinner-size: 32px; }
|
||||
.pulse[data-size='xl'] { --spinner-size: 48px; }
|
||||
.pulse[data-speed='slow'] { --spinner-speed: 1.4s; }
|
||||
.pulse[data-speed='normal'] { --spinner-speed: 0.8s; }
|
||||
.pulse[data-speed='fast'] { --spinner-speed: 0.45s; }
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(0.3); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Spinner } from '$terra';
|
||||
import type { SpinnerRootProps } from '$terra/spinner/types';
|
||||
import Circular from './circular.svelte';
|
||||
import Dots from './dots.svelte';
|
||||
import Bars from './bars.svelte';
|
||||
import Pulse from './pulse.svelte';
|
||||
|
||||
let {
|
||||
variant = 'circular',
|
||||
size = 'md',
|
||||
speed = 'normal',
|
||||
children,
|
||||
...restProps
|
||||
}: SpinnerRootProps = $props();
|
||||
</script>
|
||||
|
||||
<Spinner.Root variant={variant} size={size} speed={speed} {...restProps}>
|
||||
{#snippet children()}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if variant === 'circular'}
|
||||
<Circular size={size} speed={speed} />
|
||||
{:else if variant === 'dots'}
|
||||
<Dots size={size} speed={speed} />
|
||||
{:else if variant === 'bars'}
|
||||
<Bars size={size} speed={speed} />
|
||||
{:else if variant === 'pulse'}
|
||||
<Pulse size={size} speed={speed} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Spinner.Root>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue