MultiToggle cool animation + clearable

This commit is contained in:
Diego Imbert
2025-07-03 02:12:57 +02:00
parent 2d6b34a574
commit caeb2337fd
2 changed files with 51 additions and 11 deletions
+47 -9
View File
@@ -2,38 +2,76 @@
<script lang="ts" generics="T">
import { twMerge } from 'tailwind-merge'
import { getLabel } from './select/utils.svelte'
import { fade } from 'svelte/transition'
let {
items,
value = $bindable(),
class: className,
error,
disabled
disabled,
clearable = false
}: {
items: { label?: string; value: T }[]
value: T
class?: string
error?: boolean
disabled?: boolean
clearable?: boolean
} = $props()
let btnRefs = $state<Record<number, HTMLButtonElement | undefined>>({})
let btnWidths: { w: number; offsetX: number }[] = $state([])
$effect(() => {
btnRefs
for (const item of items) {
item.label
item.value
}
setTimeout(() => {
btnWidths = []
for (let i = 0; i in btnRefs; i++) {
btnWidths.push({
w: btnRefs[i]?.getBoundingClientRect().width ?? 0,
offsetX: btnRefs[i]?.offsetLeft ?? 0
})
}
}, 0)
})
let selectedIdx = $derived(items.findIndex((item) => item.value === value))
let selectedBtnBox: (typeof btnWidths)[number] | undefined = $derived(btnWidths[selectedIdx])
</script>
<ul
class={twMerge(
'flex text-nowrap bg-gray-300 rounded-full items-center select-none border',
'flex text-nowrap bg-gray-300 rounded-full items-center border relative transition-colors',
error ? 'border-red-300 bg-red-100' : '',
className
)}
>
{#each items as item}
<li class="h-6 mx-[1px] text-xs flex items-center">
<!-- White animated selector -->
{#if selectedBtnBox}
<div
class={twMerge('h-[22px] bg-white rounded-full absolute transition-all')}
transition:fade={{ duration: 120 }}
style={`width: ${selectedBtnBox?.w ?? 22}px; left: ${selectedBtnBox?.offsetX ?? 0}px;`}
></div>
{/if}
{#each items as item, i}
<li class="h-6 text-xs flex items-center">
<button
class={twMerge(
'rounded-full px-1.5 h-[22px] min-w-[22px] flex items-center justify-center',
value === item.value ? 'bg-white' : ''
)}
bind:this={btnRefs[i]}
class="z-10 rounded-full px-1.5 h-6 min-w-6 flex items-center justify-center"
{disabled}
onclick={() => !disabled && (value = item.value)}
onclick={() => {
if (disabled) return
if (clearable && value === item.value) {
value = undefined as any
} else {
value = item.value
}
}}
>
{getLabel(item)}
</button>
@@ -166,13 +166,15 @@
items={safeSelectItems(['r', 'w', 'rw'])}
error={!asset.access_type && !alternativeAccessType}
disabled={!!asset.access_type}
clearable
bind:value={
() => asset.access_type ?? alternativeAccessType,
async (access_type) => {
alternativeAccessTypes = alternativeAccessTypes?.filter((a) => !assetEq(a, asset))
alternativeAccessTypes ??= []
await tick()
alternativeAccessTypes.push({ ...asset, access_type })
let val = alternativeAccessTypes?.filter((a) => !assetEq(a, asset))
val.push({ ...asset, access_type })
alternativeAccessTypes = val
}
}
/>