Animate library tabs, rows, editor track lists, status badges and dropzone

Tabs get a sliding indicator that persists across library routes, grid rows flip and collapse on removal, the selection bar and filters crossfade, album tracks move between lists with a crossfade, processing badges pulse and the dropzone scales while dragging.
This commit is contained in:
2026-09-25 01:23:40 +02:00
parent ebe9769d4e
commit 43928ba9b9
15 changed files with 137 additions and 40 deletions
@@ -12,16 +12,43 @@
}
let { label, tabs, class: klass = '' }: Props = $props();
let links = $state<Record<string, HTMLAnchorElement | undefined>>({});
let indicator = $state({ x: 0, width: 0 });
let measured = $state(false);
let animated = $state(false);
const activeHref = $derived(tabs.find((tab) => tab.active)?.href);
$effect(() => {
const link = activeHref ? links[activeHref] : undefined;
if (!link) return;
indicator = { x: link.offsetLeft, width: link.offsetWidth };
measured = true;
if (!animated) requestAnimationFrame(() => (animated = true));
});
</script>
<nav aria-label={label} class="flex gap-2 {klass}">
<nav
aria-label={label}
class="relative inline-flex max-w-full rounded-full bg-surface-2 p-1 {klass}"
>
{#if measured}
<div
class="absolute inset-y-1 left-0 rounded-full bg-fg ease-snappy {animated
? 'transition-[transform,width] duration-200'
: ''}"
style="width:{indicator.width}px; transform:translateX({indicator.x}px)"
></div>
{/if}
{#each tabs as tab (tab.href)}
<a
bind:this={links[tab.href]}
href={tab.href}
aria-current={tab.active ? 'page' : undefined}
class="rounded-full px-4 py-1.5 text-xs font-medium focus-ring transition-colors {tab.active
? 'bg-fg text-ink'
: 'bg-surface-2 text-fg-2 hover:text-fg'}"
class="relative z-(--z-raised) rounded-full px-4 py-1.5 text-xs font-medium focus-ring transition-colors {tab.active
? 'text-ink'
: 'text-fg-2 hover:text-fg'}"
>
{tab.label}
</a>