Transfer
transferuiDual-list move-between control. Two columns plus a center pair of move buttons. Optional search, pagination, and one-way mode.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/transfer.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/transfer.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/transfer.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/transfer.jsonManifest:
https://uipkge.dev/r/svelte/transfer.jsonInstalls to:src/lib/components/ui/transfer/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
targetKeys | string[] | — | optional |
dataSource | TransferItem[] | — | required |
titles | [string, string] | — | optional |
showSearch | boolean | false | optional |
filterFn | (query: string, item: TransferItem) => boolean | — | optional |
height | number | string | 320 | optional |
pagination | boolean | { pageSize: number } | false | optional |
oneWay | boolean | false | optional |
disabled | boolean | false | optional |
draggable | boolean | false | optional |
selectable | boolean | true | optional |
footerLeft | Snippet | — | optional |
footerRight | Snippet | — | optional |
onTargetKeysChange | (keys: string[]) => void | — | optional |
onchange | (keys: string[], direction: 'left' | 'right', moved: string[]) => void | — | optional |
onsearch | (payload: { direction: 'left' | 'right'; query: string }) => void | — | optional |
onselectchange | (payload: { left: string[]; right: string[] }) => void | — | optional |
ref | HTMLDivElement | null | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
TransferIteminterface TransferItem {
key: string
label: string
description?: string
disabled?: boolean
}TransferDragPayloadinterface TransferDragPayload {
keys: string[]
fromSide: TransferSide
}TransferContextinterface TransferContext {
readonly disabled: boolean
readonly showSearch: boolean
readonly height: number | string
readonly pageSize: number | null
readonly filterFn: (query: string, item: TransferItem) => boolean
readonly draggable: boolean
readonly selectable: boolean
readonly oneWay: boolean
readonly dragPayload: TransferDragPayload | null
startDrag: (payload: TransferDragPayload) => void
endDrag: () => void
drop: (toSide: TransferSide, beforeKey: string | null) => void
/** Move left→right. Optional keys override the current left selection. */
moveRight: (keys?: string[]) => void
/** Move right→left. Optional keys override the current right selection. */
moveLeft: (keys?: string[]) => void
}npm dependencies
Includes
Files installed (5)
src/lib/components/ui/transfer/Transfer.svelte7.4 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' import type { Snippet } from 'svelte' import type { TransferItem } from './context' // Omit DOM event props shadowed by component callbacks (onchange/onsearch carry payloads, not events). export interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange' | 'onsearch'> { targetKeys?: string[] dataSource: TransferItem[] titles?: [string, string] showSearch?: boolean filterFn?: (query: string, item: TransferItem) => boolean height?: number | string pagination?: boolean | { pageSize: number } oneWay?: boolean disabled?: boolean draggable?: boolean selectable?: boolean footerLeft?: Snippet footerRight?: Snippet onTargetKeysChange?: (keys: string[]) => void onchange?: (keys: string[], direction: 'left' | 'right', moved: string[]) => void onsearch?: (payload: { direction: 'left' | 'right'; query: string }) => void onselectchange?: (payload: { left: string[]; right: string[] }) => void ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import TransferList from './TransferList.svelte' import TransferOperation from './TransferOperation.svelte' import { setTransferContext, type TransferDragPayload, type TransferSide } from './context' let { targetKeys = $bindable([]), dataSource, titles = ['Source', 'Target'], showSearch = false, filterFn, height = 320, pagination = false, oneWay = false, disabled = false, draggable = false, selectable = true, footerLeft, footerRight, onTargetKeysChange, onchange, onsearch, onselectchange, class: className, ref = $bindable(null), ...restProps }: TransferProps = $props() function setTargetKeys(next: string[]) { targetKeys = next onTargetKeysChange?.(next) } const dataMap = $derived(new Map(dataSource.map((i) => [i.key, i]))) const sourceItems = $derived(dataSource.filter((i) => !targetKeys.includes(i.key))) // When draggable, target order follows targetKeys exactly so reorder persists. // Otherwise keep legacy dataSource ordering for backwards compat. const targetItems = $derived.by((): TransferItem[] => { if (draggable) { const out: TransferItem[] = [] for (const k of targetKeys) { const item = dataMap.get(k) if (item) out.push(item) } return out } return dataSource.filter((i) => targetKeys.includes(i.key)) }) let selectedLeft = $state<string[]>([]) let selectedRight = $state<string[]>([]) const pageSize = $derived.by((): number | null => { if (pagination === false) return null if (pagination === true) return 10 return pagination.pageSize }) function defaultFilter(q: string, item: TransferItem) { return item.label.toLowerCase().includes(q.toLowerCase()) } let dragPayload = $state<TransferDragPayload | null>(null) function startDrag(payload: TransferDragPayload) { dragPayload = payload } function endDrag() { dragPayload = null } function drop(toSide: TransferSide, beforeKey: string | null) { const payload = dragPayload dragPayload = null if (!payload || disabled) return const keys = payload.keys.filter((k) => { const item = dataMap.get(k) return item && !item.disabled }) if (keys.length === 0) return // left → left: reorder source not supported (parent owns dataSource order). No-op. if (payload.fromSide === 'left' && toSide === 'left') return // right → left: remove from targetKeys (skip when oneWay). if (payload.fromSide === 'right' && toSide === 'left') { if (oneWay) return const removeSet = new Set(keys) const next = targetKeys.filter((k) => !removeSet.has(k)) selectedRight = selectedRight.filter((k) => !removeSet.has(k)) setTargetKeys(next) onchange?.(next, 'left', keys) onselectchange?.({ left: selectedLeft, right: selectedRight }) return } // → right: insert (cross-list move) or reorder (within-target). const movingSet = new Set(keys) const without = targetKeys.filter((k) => !movingSet.has(k)) let insertAt = without.length if (beforeKey != null) { const idx = without.indexOf(beforeKey) if (idx >= 0) insertAt = idx } const next = [...without.slice(0, insertAt), ...keys, ...without.slice(insertAt)] if (payload.fromSide === 'left') { const movedSet = new Set(keys) selectedLeft = selectedLeft.filter((k) => !movedSet.has(k)) setTargetKeys(next) onchange?.(next, 'right', keys) onselectchange?.({ left: selectedLeft, right: selectedRight }) } else { // right → right: pure reorder, no change event (target set unchanged). setTargetKeys(next) } } function onLeftSelected(keys: string[]) { selectedLeft = keys onselectchange?.({ left: keys, right: selectedRight }) } function onRightSelected(keys: string[]) { selectedRight = keys onselectchange?.({ left: selectedLeft, right: keys }) } function moveRight(keys?: string[]) { if (disabled) return const moved = keys?.length ? [...keys] : [...selectedLeft] if (moved.length === 0) return const next = [...targetKeys, ...moved.filter((k) => !targetKeys.includes(k))] selectedLeft = selectedLeft.filter((k) => !moved.includes(k)) setTargetKeys(next) onchange?.(next, 'right', moved) onselectchange?.({ left: selectedLeft, right: selectedRight }) } function moveLeft(keys?: string[]) { if (disabled || oneWay) return const moved = keys?.length ? [...keys] : [...selectedRight] if (moved.length === 0) return const remove = new Set(moved) const next = targetKeys.filter((k) => !remove.has(k)) selectedRight = selectedRight.filter((k) => !remove.has(k)) setTargetKeys(next) onchange?.(next, 'left', moved) onselectchange?.({ left: selectedLeft, right: selectedRight }) } setTransferContext({ get disabled() { return disabled }, get showSearch() { return showSearch }, get height() { return height }, get pageSize() { return pageSize }, get filterFn() { return filterFn ?? defaultFilter }, get draggable() { return draggable }, get selectable() { return selectable }, get oneWay() { return oneWay }, get dragPayload() { return dragPayload }, startDrag, endDrag, drop, moveRight, moveLeft, }) </script> <div bind:this={ref} data-uipkge data-slot="transfer" class={cn('flex items-stretch gap-3', className)} {...restProps}> <div class="min-w-0 flex-1"> <TransferList side="left" title={titles[0]} items={sourceItems} selected={selectedLeft} onselectedchange={onLeftSelected} onsearch={(query) => onsearch?.({ direction: 'left', query })} footer={footerLeft} /> </div> <TransferOperation canMoveRight={selectedLeft.length > 0 && !disabled} canMoveLeft={selectedRight.length > 0 && !disabled} {oneWay} onmoveright={() => moveRight()} onmoveleft={() => moveLeft()} /> <div class="min-w-0 flex-1"> <TransferList side="right" title={titles[1]} items={targetItems} selected={selectedRight} onselectedchange={onRightSelected} onsearch={(query) => onsearch?.({ direction: 'right', query })} footer={footerRight} /> </div> </div>src/lib/components/ui/transfer/TransferList.svelte14.9 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { TransferItem, TransferSide } from './context' export interface TransferListProps { side: TransferSide title: string items: TransferItem[] selected: string[] onselectedchange?: (keys: string[]) => void onsearch?: (query: string) => void footer?: Snippet } </script> <script lang="ts"> import { GripVertical, Search } from '@lucide/svelte' import { Checkbox } from '$lib/components/ui/checkbox' import { Input } from '$lib/components/ui/input' import { ScrollArea } from '$lib/components/ui/scroll-area' import { getTransferContext } from './context' let { side, title, items, selected, onselectedchange, onsearch, footer }: TransferListProps = $props() const ctx = getTransferContext() let query = $state('') let page = $state(1) const filtered = $derived(!query ? items : items.filter((i) => ctx.filterFn(query, i))) const effectivePageSize = $derived(ctx.pageSize ?? Math.max(1, filtered.length)) const totalPages = $derived(Math.max(1, Math.ceil(filtered.length / effectivePageSize))) const visible = $derived.by(() => { if (!ctx.pageSize) return filtered const start = (page - 1) * effectivePageSize return filtered.slice(start, start + effectivePageSize) }) $effect(() => { if (page > totalPages) page = totalPages }) const visibleEnabledKeys = $derived(visible.filter((i) => !i.disabled).map((i) => i.key)) const selectedSet = $derived(new Set(selected)) const visibleSelectedCount = $derived(visibleEnabledKeys.filter((k) => selectedSet.has(k)).length) const masterChecked = $derived( visibleEnabledKeys.length > 0 && visibleSelectedCount === visibleEnabledKeys.length, ) const masterIndeterminate = $derived( visibleSelectedCount > 0 && visibleSelectedCount < visibleEnabledKeys.length, ) function emitSelected(keys: string[]) { onselectedchange?.(keys) } function toggleAll(checked: boolean) { let next = [...selected] if (checked) { for (const k of visibleEnabledKeys) { if (!selectedSet.has(k)) next.push(k) } } else { next = next.filter((k) => !visibleEnabledKeys.includes(k)) } emitSelected(next) } function toggleItem(item: TransferItem, checked: boolean) { if (item.disabled || ctx.disabled) return let next = [...selected] if (checked) { if (!next.includes(item.key)) next.push(item.key) } else { next = next.filter((k) => k !== item.key) } emitSelected(next) lastAnchor = item.key } let lastAnchor = $state<string | null>(null) function onRowClick(e: MouseEvent, item: TransferItem) { if (item.disabled || ctx.disabled) return // With checkbox visible, click toggles (matches checkbox UX). if (ctx.selectable) { toggleItem(item, !selectedSet.has(item.key)) return } // No checkbox: desktop list pattern — plain=replace, cmd/ctrl=toggle, shift=range. const enabledKeys = visible.filter((i) => !i.disabled).map((i) => i.key) if (e.shiftKey && lastAnchor && enabledKeys.includes(lastAnchor)) { const start = enabledKeys.indexOf(lastAnchor) const end = enabledKeys.indexOf(item.key) const [lo, hi] = start < end ? [start, end] : [end, start] emitSelected(enabledKeys.slice(lo, hi + 1)) return } if (e.metaKey || e.ctrlKey) { let next = [...selected] if (selectedSet.has(item.key)) next = next.filter((k) => k !== item.key) else next.push(item.key) emitSelected(next) lastAnchor = item.key return } emitSelected([item.key]) lastAnchor = item.key } function getOptionRows(from: HTMLElement): HTMLElement[] { const list = from.closest('[role="listbox"]') if (!list) return [] return Array.from(list.querySelectorAll<HTMLElement>('[role="option"]:not([aria-disabled="true"])')) } function focusOption(el: HTMLElement | null | undefined) { el?.focus() } function onRowKeydown(e: KeyboardEvent, item: TransferItem) { const target = e.currentTarget as HTMLElement // Arrow navigation within the list. if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault() const rows = getOptionRows(target) const idx = rows.indexOf(target) if (idx < 0) return focusOption(e.key === 'ArrowDown' ? rows[idx + 1] : rows[idx - 1]) return } if (e.key === 'Home') { e.preventDefault() focusOption(getOptionRows(target)[0]) return } if (e.key === 'End') { e.preventDefault() const rows = getOptionRows(target) focusOption(rows[rows.length - 1]) return } // Keys to transfer: current multi-selection, or just the focused item. function keysToTransfer(): string[] { if (selectedSet.has(item.key) && selected.length > 0) { return selected.filter((k) => { const i = items.find((x) => x.key === k) return i && !i.disabled }) } return item.disabled ? [] : [item.key] } // Ctrl/Cmd+Enter transfers selected items (or the focused item if none selected). if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault() if (item.disabled || ctx.disabled) return const keys = keysToTransfer() if (!keys.length) return if (side === 'left') ctx.moveRight(keys) else if (!ctx.oneWay) ctx.moveLeft(keys) return } // Alt+Arrow transfers without redesigning the button strip. if (e.key === 'ArrowRight' && e.altKey && side === 'left') { e.preventDefault() if (item.disabled || ctx.disabled) return const keys = keysToTransfer() if (keys.length) ctx.moveRight(keys) return } if (e.key === 'ArrowLeft' && e.altKey && side === 'right' && !ctx.oneWay) { e.preventDefault() if (item.disabled || ctx.disabled) return const keys = keysToTransfer() if (keys.length) ctx.moveLeft(keys) return } if (e.key !== 'Enter' && e.key !== ' ') return e.preventDefault() if (item.disabled || ctx.disabled) return if (ctx.selectable) { toggleItem(item, !selectedSet.has(item.key)) return } // Keyboard without modifiers: toggle single-item selection (desktop replace). emitSelected(selectedSet.has(item.key) && selected.length === 1 ? [] : [item.key]) lastAnchor = item.key } function onSearch(v: string) { query = v page = 1 onsearch?.(v) } function prevPage() { if (page > 1) page-- } function nextPage() { if (page < totalPages) page++ } const heightStyle = $derived( `height: ${typeof ctx.height === 'number' ? ctx.height + 'px' : ctx.height}`, ) // ----- DnD ----- let dropIndicator = $state<{ key: string; position: 'before' | 'after' } | null>(null) let draggingKeys = $state<Set<string>>(new Set()) const isDropTarget = $derived.by(() => { const p = ctx.dragPayload if (!p) return false // left list rejects drops when oneWay if (side === 'left' && ctx.oneWay && p.fromSide === 'right') return false // left → left is a no-op (parent owns dataSource order) if (side === 'left' && p.fromSide === 'left') return false return true }) function onItemDragStart(e: DragEvent, item: TransferItem) { if (!ctx.draggable || item.disabled || ctx.disabled) { e.preventDefault() return } // If the dragged row is part of the current selection, drag the whole selection. // Else, drag just this row (and clear selection visually for clarity). const keys = selectedSet.has(item.key) ? selected.filter((k) => { const i = items.find((x) => x.key === k) return i && !i.disabled }) : [item.key] draggingKeys = new Set(keys) ctx.startDrag({ keys, fromSide: side }) if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move' // Required by Firefox to actually start the drag. try { e.dataTransfer.setData('text/plain', keys.join(',')) } catch { /* noop */ } } } function onItemDragEnd() { draggingKeys = new Set() dropIndicator = null ctx.endDrag() } function onItemDragOver(e: DragEvent, item: TransferItem) { if (!isDropTarget) return e.preventDefault() if (e.dataTransfer) e.dataTransfer.dropEffect = 'move' if (side !== 'right') { // left list: no insertion indicator, drop just removes from target return } const target = e.currentTarget as HTMLElement const rect = target.getBoundingClientRect() const after = e.clientY > rect.top + rect.height / 2 dropIndicator = { key: item.key, position: after ? 'after' : 'before' } } function onItemDrop(e: DragEvent, item: TransferItem) { if (!isDropTarget) return e.preventDefault() e.stopPropagation() if (side === 'right') { const after = dropIndicator?.position === 'after' const idx = items.findIndex((x) => x.key === item.key) const beforeKey = after ? (items[idx + 1]?.key ?? null) : item.key ctx.drop('right', beforeKey) } else { ctx.drop('left', null) } dropIndicator = null } function onListDragOver(e: DragEvent) { if (!isDropTarget) return e.preventDefault() if (e.dataTransfer) e.dataTransfer.dropEffect = 'move' } function onListDrop(e: DragEvent) { if (!isDropTarget) return e.preventDefault() // Empty zone or below all items → append (right) / remove (left). ctx.drop(side, null) dropIndicator = null } function onListDragLeave(e: DragEvent) { // Clear indicator only when leaving the list container, not when crossing item rows. const related = e.relatedTarget as Node | null const current = e.currentTarget as Node if (!related || !current.contains(related)) { dropIndicator = null } } </script> <div class="flex flex-col overflow-hidden rounded-md border bg-card transition-colors {ctx.dragPayload && isDropTarget ? 'ring-1 ring-ring/40' : ''}" > <div class="flex items-center justify-between gap-2 border-b bg-muted/40 px-3 py-2"> <div class="flex min-w-0 items-center gap-2"> {#if ctx.selectable} <Checkbox checked={masterIndeterminate ? 'indeterminate' : masterChecked} disabled={ctx.disabled || visibleEnabledKeys.length === 0} aria-label={`Select all in ${title}`} onCheckedChange={(v: boolean | 'indeterminate') => toggleAll(v === true)} /> {/if} <span class="truncate text-sm font-medium">{title}</span> </div> <span class="text-xs text-muted-foreground tabular-nums"> {selected.length}/{items.length} </span> </div> {#if ctx.showSearch} <div class="border-b p-2"> <div class="relative"> <Search class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" /> <Input value={query} placeholder="Search" aria-label={`Search ${title}`} class="h-8 pl-8" oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)} /> </div> </div> {/if} <ScrollArea style={heightStyle} class="flex-1" ondragover={onListDragOver} ondrop={onListDrop} ondragleave={onListDragLeave} > <ul role="listbox" aria-label={title} aria-multiselectable="true" class="py-1"> {#each visible as item (item.key)} <li role="option" aria-selected={selectedSet.has(item.key)} aria-disabled={item.disabled || undefined} tabindex={item.disabled || ctx.disabled ? -1 : 0} draggable={ctx.draggable && !item.disabled && !ctx.disabled} class="relative flex min-h-11 cursor-pointer items-start gap-2 px-3 py-3 text-sm select-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none {item.disabled ? 'cursor-not-allowed opacity-50' : ''} {draggingKeys.has(item.key) ? 'opacity-40' : ''} {!ctx.selectable && selectedSet.has(item.key) ? 'bg-accent' : ''}" onclick={(e) => onRowClick(e, item)} onkeydown={(e) => onRowKeydown(e, item)} ondragstart={(e) => onItemDragStart(e, item)} ondragend={onItemDragEnd} ondragover={(e) => onItemDragOver(e, item)} ondrop={(e) => onItemDrop(e, item)} > {#if dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'before' && side === 'right'} <span class="pointer-events-none absolute -top-px right-2 left-2 h-0.5 rounded-full bg-primary" aria-hidden="true" ></span> {/if} {#if dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'after' && side === 'right'} <span class="pointer-events-none absolute right-2 -bottom-px left-2 h-0.5 rounded-full bg-primary" aria-hidden="true" ></span> {/if} {#if ctx.selectable} <Checkbox checked={selectedSet.has(item.key)} disabled={item.disabled || ctx.disabled} onCheckedChange={(v: boolean | 'indeterminate') => toggleItem(item, v === true)} onclick={(e: MouseEvent) => e.stopPropagation()} /> {/if} <div class="min-w-0 flex-1"> <div class="truncate">{item.label}</div> {#if item.description} <div class="truncate text-xs text-muted-foreground"> {item.description} </div> {/if} </div> {#if ctx.draggable && !item.disabled} <GripVertical class="mt-0.5 size-3.5 shrink-0 text-muted-foreground/60" aria-hidden="true" /> {/if} </li> {/each} {#if visible.length === 0} <li class="px-3 py-6 text-center text-sm text-muted-foreground">No items</li> {/if} </ul> </ScrollArea> {#if ctx.pageSize && totalPages > 1} <div class="flex items-center justify-center gap-2 border-t p-2 text-xs"> <button type="button" class="rounded px-2 py-1 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" disabled={page <= 1} aria-label={`Previous page of ${title}`} onclick={prevPage} > Prev </button> <span class="tabular-nums" aria-live="polite">{page} / {totalPages}</span> <button type="button" class="rounded px-2 py-1 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" disabled={page >= totalPages} aria-label={`Next page of ${title}`} onclick={nextPage} > Next </button> </div> {/if} {#if footer} <div class="border-t p-2"> {@render footer()} </div> {/if} </div>src/lib/components/ui/transfer/TransferOperation.svelte1 kB
<script lang="ts" module> export interface TransferOperationProps { canMoveRight: boolean canMoveLeft: boolean oneWay?: boolean onmoveright?: () => void onmoveleft?: () => void } </script> <script lang="ts"> import { ChevronLeft, ChevronRight } from '@lucide/svelte' import { Button } from '$lib/components/ui/button' let { canMoveRight, canMoveLeft, oneWay = false, onmoveright, onmoveleft }: TransferOperationProps = $props() </script> <div class="flex flex-col items-center justify-center gap-2 px-2"> <Button size="icon-sm" variant="outline" disabled={!canMoveRight} aria-label="Move selected to right" onclick={() => onmoveright?.()} > <ChevronRight aria-hidden="true" /> </Button> {#if !oneWay} <Button size="icon-sm" variant="outline" disabled={!canMoveLeft} aria-label="Move selected to left" onclick={() => onmoveleft?.()} > <ChevronLeft aria-hidden="true" /> </Button> {/if} </div>src/lib/components/ui/transfer/context.ts1.4 kB
import { getContext, setContext } from 'svelte' export interface TransferItem { key: string label: string description?: string disabled?: boolean } export type TransferSide = 'left' | 'right' export interface TransferDragPayload { keys: string[] fromSide: TransferSide } export interface TransferContext { readonly disabled: boolean readonly showSearch: boolean readonly height: number | string readonly pageSize: number | null readonly filterFn: (query: string, item: TransferItem) => boolean readonly draggable: boolean readonly selectable: boolean readonly oneWay: boolean readonly dragPayload: TransferDragPayload | null startDrag: (payload: TransferDragPayload) => void endDrag: () => void drop: (toSide: TransferSide, beforeKey: string | null) => void /** Move left→right. Optional keys override the current left selection. */ moveRight: (keys?: string[]) => void /** Move right→left. Optional keys override the current right selection. */ moveLeft: (keys?: string[]) => void } const TRANSFER_CONTEXT_KEY = Symbol('uipkge-transfer') export function setTransferContext(ctx: TransferContext): void { setContext(TRANSFER_CONTEXT_KEY, ctx) } export function getTransferContext(): TransferContext { const ctx = getContext<TransferContext | undefined>(TRANSFER_CONTEXT_KEY) if (!ctx) throw new Error('TransferList must be used inside <Transfer>.') return ctx }src/lib/components/ui/transfer/index.ts0.3 kB
export { default as Transfer, type TransferProps } from './Transfer.svelte' export { default as TransferList, type TransferListProps } from './TransferList.svelte' export { default as TransferOperation, type TransferOperationProps } from './TransferOperation.svelte' export type { TransferItem, TransferSide, TransferDragPayload } from './context'
Raw manifest:https://uipkge.dev/r/svelte/transfer.json