Индикатор загрузки
Latticeloader показывает ход асинхронной операции анимацией ячеек. После завершения или ошибки анимация сменяется на постоянную отметку, а текст сообщает итоговое время.
Интерактивное демо
Thinking Done in Failed after 0.0s Thinking, in progress
Пропсы
Статус
Анимация
Размер решётки
Цвет
Свечение
<script module lang="ts">
export type LatticeStatus = 'working' | 'done' | 'error';
export type LatticePatternName =
| 'arrow'
| 'dots'
| 'orbit'
| 'ripple'
| 'snake'
| 'spiral'
| 'sweep'
| 'spin'
| 'rain'
| 'pulse';
export type LatticeGrid = 3 | 4;
export type LatticePattern = {
cells: (number | null)[];
loop?: number;
scale?: number;
lit?: 0.25 | 0.35 | 0.45 | 0.62;
};
</script>
<script lang="ts">
import { onDestroy } from 'svelte';
type ResolvedPattern = { cells: (number | null)[]; loop: number; scale: number; lit?: number };
// ---- Static pattern data (unchanged from the React source) ----
const ORBIT_3: ResolvedPattern = { cells: [0, 1, 2, 7, null, 3, 6, 5, 4], loop: 8, scale: 1.2 };
const SWEEP_4: ResolvedPattern = {
cells: [0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6],
loop: 5,
scale: 1,
lit: 0.45
};
const PATTERNS: Record<LatticePatternName, Partial<Record<LatticeGrid, ResolvedPattern>>> = {
arrow: { 3: { cells: [1, 2, 3, 0, 1, 2, 1, 2, 3], loop: 7.2, scale: 1 } },
dots: { 3: { cells: [0, 1, 2, 0, 1, 2, 0, 1, 2], loop: 3, scale: 2.4 } },
ripple: { 3: { cells: [2, 1, 2, 1, 0, 1, 2, 1, 2], loop: 4.8, scale: 1.5 } },
spiral: { 3: { cells: [0, 1, 2, 7, 8, 3, 6, 5, 4], loop: 9, scale: 1.2, lit: 0.35 } },
orbit: {
3: ORBIT_3,
4: { cells: [0, 1, 2, 3, 11, null, null, 4, 10, null, null, 5, 9, 8, 7, 6], loop: 6, scale: 1.2, lit: 0.45 }
},
snake: {
3: { cells: [0, 1, 2, 5, 4, 3, 6, 7, 8], loop: 9, scale: 1, lit: 0.35 },
4: { cells: [0, 1, 2, 3, 7, 6, 5, 4, 8, 9, 10, 11, 15, 14, 13, 12], loop: 16, scale: 1, lit: 0.25 }
},
sweep: { 4: SWEEP_4 },
spin: { 4: { cells: [0, 0, 1, 1, 0, 0, 1, 1, 3, 3, 2, 2, 3, 3, 2, 2], loop: 4, scale: 1.6, lit: 0.35 } },
rain: { 4: { cells: [0, 2, 1, 3, 1, 3, 2, 4, 2, 4, 3, 5, 3, 5, 4, 6], loop: 4, scale: 1.2, lit: 0.35 } },
pulse: { 4: { cells: [2, 1, 1, 2, 1, 0, 0, 1, 1, 0, 0, 1, 2, 1, 1, 2], loop: 2.4, scale: 2.5, lit: 0.45 } }
};
const DEFAULT_PATTERN: Record<LatticeGrid, ResolvedPattern> = { 3: ORBIT_3, 4: SWEEP_4 };
const MARKS: Record<LatticeGrid, Record<'done' | 'error', number[]>> = {
3: { done: [2, 3, 5, 7], error: [0, 2, 4, 6, 8] },
4: { done: [7, 8, 10, 13], error: [0, 3, 5, 6, 9, 10, 12, 15] }
};
const toGrid = (value: number): LatticeGrid => (value === 4 ? 4 : 3);
function resolvePattern(pattern: LatticePatternName | LatticePattern, grid: LatticeGrid): ResolvedPattern {
if (typeof pattern === 'string') {
const named = PATTERNS[pattern];
return (named && named[grid]) || DEFAULT_PATTERN[grid];
}
const cells = Array.from({ length: grid * grid }, (_, i) => pattern.cells[i] ?? null);
const max = Math.max(0, ...cells.filter((v) => v != null));
return { cells, loop: pattern.loop ?? max + 4.2, scale: pattern.scale ?? 1, lit: pattern.lit ?? 0.62 };
}
const fmt = (ds: number) =>
ds < 600 ? `${(ds / 10).toFixed(1)}s` : `${Math.floor(ds / 600)}m ${((ds % 600) / 10).toFixed(1)}s`;
const spoken = (ds: number) =>
ds < 600
? `${(ds / 10).toFixed(1)} seconds`
: `${Math.floor(ds / 600)} minutes ${((ds % 600) / 10).toFixed(1)} seconds`;
// ---- Props ----
export let label = 'Thinking';
export let doneLabel = 'Done in';
export let errorLabel = 'Failed after';
export let status: LatticeStatus = 'working';
export let pattern: LatticePatternName | LatticePattern = 'orbit';
export let grid: LatticeGrid = 3;
export let shape: 'square' | 'round' = 'round';
export let color = 'var(--accent, #f43f5e)';
export let doneColor = '#22c55e';
export let errorColor = '#ef4444';
export let cellSize = 6;
export let gap = 2;
export let fontSize = 14;
export let step = 90;
export let idleOpacity = 0.15;
export let glow = false;
export let glowColor = '';
export let showTimer = true;
export let elapsed: number | undefined = undefined;
export let style = '';
let className = '';
export { className as class };
// ---- Derived pattern data ----
$: n = toGrid(grid);
$: pat = resolvePattern(pattern, n);
$: marks = MARKS[n];
$: d = step * pat.scale;
$: cycle = Math.round(pat.loop * d);
$: litKey = Math.round((pat.lit ?? 0.62) * 100);
// ---- Status → "mark" (done/error dots stay lit even while working resumes) ----
let lastMark: 'done' | 'error' = 'done';
$: if (status !== 'working') lastMark = status;
$: mark = status === 'working' ? lastMark : status;
// ---- Timer (mirrors the useLayoutEffect in the React version) ----
let ds = 0;
let intervalId: ReturnType<typeof setInterval> | undefined;
function clearTimer() {
if (intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
}
}
$: {
clearTimer();
if (elapsed != null) {
ds = Math.round(elapsed * 10);
} else if (status === 'working') {
const startedAt = performance.now();
ds = 0;
intervalId = setInterval(() => {
// eslint-disable-next-line svelte/infinite-reactive-loop -- ds is only written here, never read, so it is not a dependency of this block
ds = Math.floor((performance.now() - startedAt) / 100);
}, 100);
}
}
onDestroy(clearTimer);
$: announce =
status === 'working'
? `${label}, in progress`
: `${status === 'done' ? doneLabel : errorLabel}${showTimer ? ` ${spoken(ds)}` : ''}`;
$: rootStyle = `--ll-n:${n};--ll-cell:${cellSize}px;--ll-gap:${gap}px;--ll-font:${fontSize}px;--ll-color:${color};--ll-mark:${
status === 'error' ? errorColor : doneColor
};--ll-idle:${idleOpacity};--ll-glow:${glowColor || color};--ll-mark-glow:${
glowColor || (status === 'error' ? errorColor : doneColor)
};--ll-cycle:${cycle}ms;--ll-peak:1;${style}`;
</script>
<span
role="status"
class="ll-root {className}"
data-status={status}
data-shape={shape}
data-glow={glow ? '' : undefined}
style={rootStyle}
>
<span class="ll-grid-wrap" aria-hidden="true">
<span class="ll-run">
{#each pat.cells as unit, i (i)}
{#if unit == null}
<span class="ll-cell ll-hole"></span>
{:else}
<span class="ll-cell ll-lit-{litKey}" style="animation-delay:{Math.round(unit * d)}ms"></span>
{/if}
{/each}
</span>
<span class="ll-mark">
{#each pat.cells as _, i (i)}
<span class="ll-cell" class:ll-on={marks[mark].includes(i)}></span>
{/each}
</span>
</span>
<span class="ll-label" aria-hidden="true">
<span class="ll-text" class:ll-active={status === 'working'}>{label}</span>
<span class="ll-text" class:ll-active={status === 'done'}>{doneLabel}</span>
<span class="ll-text" class:ll-active={status === 'error'}>{errorLabel}</span>
</span>
{#if showTimer}
<span class="ll-timer" aria-hidden="true">{fmt(ds)}</span>
{/if}
<span class="sr-only">{announce}</span>
</span>
<style>
.ll-root {
position: relative;
display: inline-flex;
align-items: center;
line-height: 1;
font-family: inherit;
gap: calc(var(--ll-font) * 0.625);
font-size: var(--ll-font);
--ll-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ll-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}
.ll-grid-wrap {
display: grid;
flex-shrink: 0;
}
.ll-run,
.ll-mark {
grid-area: 1 / 1;
display: grid;
grid-template-columns: repeat(var(--ll-n), var(--ll-cell));
gap: var(--ll-gap);
}
.ll-run {
transition: opacity 200ms ease;
}
.ll-root[data-status='done'] .ll-run,
.ll-root[data-status='error'] .ll-run {
opacity: 0;
}
.ll-root[data-status='done'] .ll-run .ll-cell,
.ll-root[data-status='error'] .ll-run .ll-cell {
animation-play-state: paused;
}
.ll-mark {
transform-origin: center;
opacity: 0;
transform: scale(0.9);
transition: opacity 160ms var(--ll-ease-out), transform 160ms var(--ll-ease-out);
}
.ll-root[data-status='done'] .ll-mark,
.ll-root[data-status='error'] .ll-mark {
opacity: 1;
transform: none;
transition: opacity 200ms ease, transform 200ms var(--ll-ease-out);
}
.ll-cell {
height: var(--ll-cell);
width: var(--ll-cell);
border-radius: max(1px, calc(var(--ll-cell) * 0.25));
background: var(--ll-color);
opacity: var(--ll-idle);
}
.ll-root[data-shape='round'] .ll-cell {
border-radius: 50%;
}
.ll-hole {
opacity: calc(var(--ll-idle) * 0.47);
}
.ll-lit-62 {
animation: lattice-on var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-45 {
animation: lattice-on-45 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-35 {
animation: lattice-on-35 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-25 {
animation: lattice-on-25 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-root[data-glow] .ll-run .ll-cell:not(.ll-hole) {
box-shadow: 0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * 0.12) var(--ll-glow);
}
.ll-mark .ll-cell {
transition: opacity 200ms ease, background-color 200ms ease;
}
.ll-mark .ll-cell.ll-on {
background: var(--ll-mark);
opacity: var(--ll-peak);
}
.ll-root[data-glow] .ll-mark .ll-cell.ll-on {
box-shadow: 0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * 0.12) var(--ll-mark-glow);
}
.ll-label {
position: relative;
display: inline-block;
font-weight: 500;
}
.ll-text {
position: absolute;
top: 0;
left: 0;
white-space: nowrap;
opacity: 0;
filter: blur(2px);
transition: opacity 200ms ease, filter 200ms ease;
}
.ll-text.ll-active {
position: static;
opacity: 1;
filter: blur(0);
}
.ll-timer {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-variant-numeric: tabular-nums;
opacity: 0.6;
font-size: calc(var(--ll-font) * 0.875);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@keyframes lattice-on {
0%, 100% { opacity: var(--ll-idle); }
18%, 42% { opacity: var(--ll-peak); }
62% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-45 {
0%, 100% { opacity: var(--ll-idle); }
13%, 31% { opacity: var(--ll-peak); }
45% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-35 {
0%, 100% { opacity: var(--ll-idle); }
10%, 24% { opacity: var(--ll-peak); }
35% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-25 {
0%, 100% { opacity: var(--ll-idle); }
7%, 17% { opacity: var(--ll-peak); }
25% { opacity: var(--ll-idle); }
}
@media (prefers-reduced-motion: reduce) {
.ll-run { --ll-peak: 0.7; }
.ll-run .ll-cell {
animation-delay: 0ms !important;
animation-duration: 1400ms !important;
}
.ll-mark { transform: none !important; }
.ll-text { filter: none !important; }
}
</style>
<script module lang="ts">
export type LatticeStatus = 'working' | 'done' | 'error';
export type LatticePatternName =
| 'arrow'
| 'dots'
| 'orbit'
| 'ripple'
| 'snake'
| 'spiral'
| 'sweep'
| 'spin'
| 'rain'
| 'pulse';
export type LatticeGrid = 3 | 4;
export type LatticePattern = {
cells: (number | null)[];
loop?: number;
scale?: number;
lit?: 0.25 | 0.35 | 0.45 | 0.62;
};
</script>
<script lang="ts">
import { onDestroy } from 'svelte';
type ResolvedPattern = { cells: (number | null)[]; loop: number; scale: number; lit?: number };
// ---- Static pattern data (unchanged from the React source) ----
const ORBIT_3: ResolvedPattern = { cells: [0, 1, 2, 7, null, 3, 6, 5, 4], loop: 8, scale: 1.2 };
const SWEEP_4: ResolvedPattern = {
cells: [0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6],
loop: 5,
scale: 1,
lit: 0.45
};
const PATTERNS: Record<LatticePatternName, Partial<Record<LatticeGrid, ResolvedPattern>>> = {
arrow: { 3: { cells: [1, 2, 3, 0, 1, 2, 1, 2, 3], loop: 7.2, scale: 1 } },
dots: { 3: { cells: [0, 1, 2, 0, 1, 2, 0, 1, 2], loop: 3, scale: 2.4 } },
ripple: { 3: { cells: [2, 1, 2, 1, 0, 1, 2, 1, 2], loop: 4.8, scale: 1.5 } },
spiral: { 3: { cells: [0, 1, 2, 7, 8, 3, 6, 5, 4], loop: 9, scale: 1.2, lit: 0.35 } },
orbit: {
3: ORBIT_3,
4: { cells: [0, 1, 2, 3, 11, null, null, 4, 10, null, null, 5, 9, 8, 7, 6], loop: 6, scale: 1.2, lit: 0.45 }
},
snake: {
3: { cells: [0, 1, 2, 5, 4, 3, 6, 7, 8], loop: 9, scale: 1, lit: 0.35 },
4: { cells: [0, 1, 2, 3, 7, 6, 5, 4, 8, 9, 10, 11, 15, 14, 13, 12], loop: 16, scale: 1, lit: 0.25 }
},
sweep: { 4: SWEEP_4 },
spin: { 4: { cells: [0, 0, 1, 1, 0, 0, 1, 1, 3, 3, 2, 2, 3, 3, 2, 2], loop: 4, scale: 1.6, lit: 0.35 } },
rain: { 4: { cells: [0, 2, 1, 3, 1, 3, 2, 4, 2, 4, 3, 5, 3, 5, 4, 6], loop: 4, scale: 1.2, lit: 0.35 } },
pulse: { 4: { cells: [2, 1, 1, 2, 1, 0, 0, 1, 1, 0, 0, 1, 2, 1, 1, 2], loop: 2.4, scale: 2.5, lit: 0.45 } }
};
const DEFAULT_PATTERN: Record<LatticeGrid, ResolvedPattern> = { 3: ORBIT_3, 4: SWEEP_4 };
const MARKS: Record<LatticeGrid, Record<'done' | 'error', number[]>> = {
3: { done: [2, 3, 5, 7], error: [0, 2, 4, 6, 8] },
4: { done: [7, 8, 10, 13], error: [0, 3, 5, 6, 9, 10, 12, 15] }
};
const toGrid = (value: number): LatticeGrid => (value === 4 ? 4 : 3);
function resolvePattern(pattern: LatticePatternName | LatticePattern, grid: LatticeGrid): ResolvedPattern {
if (typeof pattern === 'string') {
const named = PATTERNS[pattern];
return (named && named[grid]) || DEFAULT_PATTERN[grid];
}
const cells = Array.from({ length: grid * grid }, (_, i) => pattern.cells[i] ?? null);
const max = Math.max(0, ...cells.filter((v) => v != null));
return { cells, loop: pattern.loop ?? max + 4.2, scale: pattern.scale ?? 1, lit: pattern.lit ?? 0.62 };
}
const fmt = (ds: number) =>
ds < 600 ? `${(ds / 10).toFixed(1)}s` : `${Math.floor(ds / 600)}m ${((ds % 600) / 10).toFixed(1)}s`;
const spoken = (ds: number) =>
ds < 600
? `${(ds / 10).toFixed(1)} seconds`
: `${Math.floor(ds / 600)} minutes ${((ds % 600) / 10).toFixed(1)} seconds`;
// ---- Props ----
export let label = 'Thinking';
export let doneLabel = 'Done in';
export let errorLabel = 'Failed after';
export let status: LatticeStatus = 'working';
export let pattern: LatticePatternName | LatticePattern = 'orbit';
export let grid: LatticeGrid = 3;
export let shape: 'square' | 'round' = 'round';
export let color = 'var(--accent, #f43f5e)';
export let doneColor = '#22c55e';
export let errorColor = '#ef4444';
export let cellSize = 6;
export let gap = 2;
export let fontSize = 14;
export let step = 90;
export let idleOpacity = 0.15;
export let glow = false;
export let glowColor = '';
export let showTimer = true;
export let elapsed: number | undefined = undefined;
export let style = '';
let className = '';
export { className as class };
// ---- Derived pattern data ----
$: n = toGrid(grid);
$: pat = resolvePattern(pattern, n);
$: marks = MARKS[n];
$: d = step * pat.scale;
$: cycle = Math.round(pat.loop * d);
$: litKey = Math.round((pat.lit ?? 0.62) * 100);
// ---- Status → "mark" (done/error dots stay lit even while working resumes) ----
let lastMark: 'done' | 'error' = 'done';
$: if (status !== 'working') lastMark = status;
$: mark = status === 'working' ? lastMark : status;
// ---- Timer (mirrors the useLayoutEffect in the React version) ----
let ds = 0;
let intervalId: ReturnType<typeof setInterval> | undefined;
function clearTimer() {
if (intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
}
}
$: {
clearTimer();
if (elapsed != null) {
ds = Math.round(elapsed * 10);
} else if (status === 'working') {
const startedAt = performance.now();
ds = 0;
intervalId = setInterval(() => {
// eslint-disable-next-line svelte/infinite-reactive-loop -- ds is only written here, never read, so it is not a dependency of this block
ds = Math.floor((performance.now() - startedAt) / 100);
}, 100);
}
}
onDestroy(clearTimer);
$: announce =
status === 'working'
? `${label}, in progress`
: `${status === 'done' ? doneLabel : errorLabel}${showTimer ? ` ${spoken(ds)}` : ''}`;
$: rootStyle = `--ll-n:${n};--ll-cell:${cellSize}px;--ll-gap:${gap}px;--ll-font:${fontSize}px;--ll-color:${color};--ll-mark:${
status === 'error' ? errorColor : doneColor
};--ll-idle:${idleOpacity};--ll-glow:${glowColor || color};--ll-mark-glow:${
glowColor || (status === 'error' ? errorColor : doneColor)
};--ll-cycle:${cycle}ms;--ll-peak:1;${style}`;
</script>
<span
role="status"
class="ll-root {className}"
data-status={status}
data-shape={shape}
data-glow={glow ? '' : undefined}
style={rootStyle}
>
<span class="ll-grid-wrap" aria-hidden="true">
<span class="ll-run">
{#each pat.cells as unit, i (i)}
{#if unit == null}
<span class="ll-cell ll-hole"></span>
{:else}
<span class="ll-cell ll-lit-{litKey}" style="animation-delay:{Math.round(unit * d)}ms"></span>
{/if}
{/each}
</span>
<span class="ll-mark">
{#each pat.cells as _, i (i)}
<span class="ll-cell" class:ll-on={marks[mark].includes(i)}></span>
{/each}
</span>
</span>
<span class="ll-label" aria-hidden="true">
<span class="ll-text" class:ll-active={status === 'working'}>{label}</span>
<span class="ll-text" class:ll-active={status === 'done'}>{doneLabel}</span>
<span class="ll-text" class:ll-active={status === 'error'}>{errorLabel}</span>
</span>
{#if showTimer}
<span class="ll-timer" aria-hidden="true">{fmt(ds)}</span>
{/if}
<span class="sr-only">{announce}</span>
</span>
<style>
.ll-root {
position: relative;
display: inline-flex;
align-items: center;
line-height: 1;
font-family: inherit;
gap: calc(var(--ll-font) * 0.625);
font-size: var(--ll-font);
--ll-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ll-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}
.ll-grid-wrap {
display: grid;
flex-shrink: 0;
}
.ll-run,
.ll-mark {
grid-area: 1 / 1;
display: grid;
grid-template-columns: repeat(var(--ll-n), var(--ll-cell));
gap: var(--ll-gap);
}
.ll-run {
transition: opacity 200ms ease;
}
.ll-root[data-status='done'] .ll-run,
.ll-root[data-status='error'] .ll-run {
opacity: 0;
}
.ll-root[data-status='done'] .ll-run .ll-cell,
.ll-root[data-status='error'] .ll-run .ll-cell {
animation-play-state: paused;
}
.ll-mark {
transform-origin: center;
opacity: 0;
transform: scale(0.9);
transition: opacity 160ms var(--ll-ease-out), transform 160ms var(--ll-ease-out);
}
.ll-root[data-status='done'] .ll-mark,
.ll-root[data-status='error'] .ll-mark {
opacity: 1;
transform: none;
transition: opacity 200ms ease, transform 200ms var(--ll-ease-out);
}
.ll-cell {
height: var(--ll-cell);
width: var(--ll-cell);
border-radius: max(1px, calc(var(--ll-cell) * 0.25));
background: var(--ll-color);
opacity: var(--ll-idle);
}
.ll-root[data-shape='round'] .ll-cell {
border-radius: 50%;
}
.ll-hole {
opacity: calc(var(--ll-idle) * 0.47);
}
.ll-lit-62 {
animation: lattice-on var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-45 {
animation: lattice-on-45 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-35 {
animation: lattice-on-35 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-lit-25 {
animation: lattice-on-25 var(--ll-cycle) infinite var(--ll-ease-in-out);
}
.ll-root[data-glow] .ll-run .ll-cell:not(.ll-hole) {
box-shadow: 0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * 0.12) var(--ll-glow);
}
.ll-mark .ll-cell {
transition: opacity 200ms ease, background-color 200ms ease;
}
.ll-mark .ll-cell.ll-on {
background: var(--ll-mark);
opacity: var(--ll-peak);
}
.ll-root[data-glow] .ll-mark .ll-cell.ll-on {
box-shadow: 0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * 0.12) var(--ll-mark-glow);
}
.ll-label {
position: relative;
display: inline-block;
font-weight: 500;
}
.ll-text {
position: absolute;
top: 0;
left: 0;
white-space: nowrap;
opacity: 0;
filter: blur(2px);
transition: opacity 200ms ease, filter 200ms ease;
}
.ll-text.ll-active {
position: static;
opacity: 1;
filter: blur(0);
}
.ll-timer {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-variant-numeric: tabular-nums;
opacity: 0.6;
font-size: calc(var(--ll-font) * 0.875);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@keyframes lattice-on {
0%, 100% { opacity: var(--ll-idle); }
18%, 42% { opacity: var(--ll-peak); }
62% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-45 {
0%, 100% { opacity: var(--ll-idle); }
13%, 31% { opacity: var(--ll-peak); }
45% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-35 {
0%, 100% { opacity: var(--ll-idle); }
10%, 24% { opacity: var(--ll-peak); }
35% { opacity: var(--ll-idle); }
}
@keyframes lattice-on-25 {
0%, 100% { opacity: var(--ll-idle); }
7%, 17% { opacity: var(--ll-peak); }
25% { opacity: var(--ll-idle); }
}
@media (prefers-reduced-motion: reduce) {
.ll-run { --ll-peak: 0.7; }
.ll-run .ll-cell {
animation-delay: 0ms !important;
animation-duration: 1400ms !important;
}
.ll-mark { transform: none !important; }
.ll-text { filter: none !important; }
}
</style>
Использование
Базовая загрузка
<script lang="ts">
import Latticeloader from '$lib/components/ui-registry/Latticeloader.svelte';
</script>
<Latticeloader /><script lang="ts">
import Latticeloader from '$lib/components/ui-registry/Latticeloader.svelte';
</script>
<Latticeloader />Завершённая операция
Передайте состояние done, чтобы показать отметку завершения и зафиксировать время.
<Latticeloader status="done" elapsed={2.8} doneLabel="Готово за" /><Latticeloader status="done" elapsed={2.8} doneLabel="Готово за" />Ошибка с решёткой 4 × 4
<Latticeloader
status="error"
grid={4}
pattern="pulse"
errorLabel="Не удалось выполнить за"
color="#f59e0b"
errorColor="#ef4444"
glow={true}
/><Latticeloader
status="error"
grid={4}
pattern="pulse"
errorLabel="Не удалось выполнить за"
color="#f59e0b"
errorColor="#ef4444"
glow={true}
/>Пропсы
| Пропс | Тип | По умолчанию | Описание |
|---|---|---|---|
label | string | "Thinking" | Текст во время загрузки |
doneLabel | string | "Done in" | Текст после успешного завершения |
errorLabel | string | "Failed after" | Текст после ошибки |
status | "working" "done" "error" | "working" | Состояние операции |
pattern | LatticePatternName LatticePattern | "orbit" | Встроенный или пользовательский порядок анимации |
grid | 3 4 | 3 | Размер решётки |
shape | "square" "round" | "round" | Форма ячеек |
color | string | "var(--accent, #f43f5e)" | Цвет активных ячеек |
doneColor | string | "#22c55e" | Цвет отметки успеха |
errorColor | string | "#ef4444" | Цвет отметки ошибки |
cellSize | number | 6 | Размер ячейки в пикселях |
gap | number | 2 | Расстояние между ячейками в пикселях |
fontSize | number | 14 | Размер текста в пикселях |
step | number | 90 | Задержка между ячейками в миллисекундах |
idleOpacity | number | 0.15 | Прозрачность неактивных ячеек |
glow | boolean | false | Включить свечение активных ячеек |
glowColor | string | "" | Цвет свечения; пустая строка использует цвет состояния |
showTimer | boolean | true | Показывать таймер |
elapsed | number undefined | undefined | Зафиксированное время в секундах |
class | string | "" | Дополнительные CSS-классы |
style | string | "" | Дополнительные inline-стили |
Пользовательский паттерн
Передайте объект с индексами ячеек. Значения задают порядок их включения, а null оставляет в решётке пустую ячейку.
<Latticeloader
grid={3}
pattern={{
cells: [0, 1, 2, 7, null, 3, 6, 5, 4],
loop: 8,
scale: 1.2,
lit: 0.45
}}
/><Latticeloader
grid={3}
pattern={{
cells: [0, 1, 2, 7, null, 3, 6, 5, 4],
loop: 8,
scale: 1.2,
lit: 0.45
}}
/>