Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169<script module lang="ts"> export type CopyStatus = "idle" | "copied" | "error";</script>
<script lang="ts"> import Copy from "$icon/copy"; import X from "$icon/x"; import { CROSSFADE_DURATION, CROSSFADE_EASING, DRAW_DURATION, DRAW_EASING } from "$lib/motion/spring"; import { prefersReducedMotion } from "svelte/motion"; import Button, { type ButtonVariants } from "$lib/components/ui/Button.svelte";
interface Props { value: string; label?: string; copiedLabel?: string; errorLabel?: string; showLabel?: boolean; timeout?: number; variant?: ButtonVariants["variant"]; size?: ButtonVariants["size"]; disabled?: boolean; onCopy?: (value: string) => void; onError?: (reason: unknown) => void; title?: string; "aria-label"?: string; class?: string; }
let { value, label = "Copy", copiedLabel = "Copied", errorLabel = "Failed", showLabel = false, timeout = 2000, variant = "ghost", size = "small", disabled = false, onCopy, onError, class: className, title, "aria-label": ariaLabel }: Props = $props();
let status = $state<CopyStatus>("idle"); let timer: ReturnType<typeof setTimeout> | undefined;
$effect(() => () => clearTimeout(timer));
const copy = async () => { try { await navigator.clipboard.writeText(value); status = "copied"; onCopy?.(value); } catch (reason) { status = "error"; onError?.(reason); } clearTimeout(timer); timer = setTimeout(() => (status = "idle"), timeout); };
const iconSize = $derived(size === "large" ? "size-5" : "size-4"); const fade = $derived(prefersReducedMotion.current ? 0 : CROSSFADE_DURATION); const draw = $derived(prefersReducedMotion.current ? 0 : DRAW_DURATION); const labels = $derived<[CopyStatus, string][]>([ ["idle", label], ["copied", copiedLabel], ["error", errorLabel] ]);</script>
<Button {variant} {size} {disabled} class={className} {title} aria-label={ariaLabel ?? label} onclick={() => void copy()} style="--copy-fade: {fade}ms; --copy-fade-ease: {CROSSFADE_EASING}; --copy-draw: {draw}ms; --copy-draw-ease: {DRAW_EASING}"> <span class="cell {iconSize} grid shrink-0" data-status={status} aria-hidden="true"> <Copy class="layer idle {iconSize}" /> <!-- lucide's check reversed, so the stroke draws from the short leg up to the tip the way a hand would write it. pathLength normalises the dash to 1. --> <svg class="layer ok {iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path class="check" pathLength="1" d="M4 12l5 5L20 6" /> </svg> <X class="layer err {iconSize}" /> </span> {#if showLabel} <span class="cell grid" data-status={status} aria-hidden="true"> {#each labels as [key, text] (key)} <span class="label label-{key}">{text}</span> {/each} </span> {/if} <span role="status" aria-live="polite" class="sr-only"> {status === "copied" ? copiedLabel : status === "error" ? errorLabel : ""} </span></Button>
<style> .cell :global(.layer), .label { grid-column: 1; grid-row: 1; }
.cell :global(.layer) { opacity: 0; scale: 0.92; transition: opacity var(--copy-fade) var(--copy-fade-ease), scale var(--copy-fade) var(--copy-fade-ease); }
.cell[data-status="idle"] :global(.idle), .cell[data-status="copied"] :global(.ok), .cell[data-status="error"] :global(.err) { opacity: 1; scale: 1; }
.check { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset var(--copy-draw) var(--copy-draw-ease); }
.cell[data-status="copied"] .check { stroke-dashoffset: 0; }
.label { white-space: nowrap; opacity: 0; translate: 0 3px; filter: blur(3px); transition: opacity var(--copy-fade) var(--copy-fade-ease), translate var(--copy-fade) var(--copy-fade-ease), filter var(--copy-fade) var(--copy-fade-ease); }
.cell[data-status="idle"] .label-idle, .cell[data-status="copied"] .label-copied, .cell[data-status="error"] .label-error { opacity: 1; translate: 0 0; filter: blur(0); }</style>