Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164<script module lang="ts"> import { getContext, setContext } from "svelte"; import { tv } from "tailwind-variants"; import type { ResolvedPathname } from "$app/types";
export const segmentedControl = tv({ slots: { group: [ "relative isolate inline-flex items-stretch rounded-md bg-background-inset p-0.5 ring ring-border-default/70 dark:bg-background-canvas", "[--btn-press:1] [--seg-inset:4px] *:[--btn-radius:calc(var(--radius-md)-2px)]", "[&>*:focus-visible]:-outline-offset-2" ], thumb: [ "pointer-events-none absolute top-0 left-0 rounded-(--btn-radius) bg-background-default edge-ring-inset", "transition-[translate,width,height]" ] }, variants: { ready: { true: { thumb: "opacity-100" }, false: { thumb: "opacity-0" } } } });
interface SegmentedControlContext { readonly value: string | undefined; readonly ready: boolean; href?: (value: string) => ResolvedPathname; select: (value: string) => void; }
const SEGMENTED_CONTROL = Symbol("segmented-control");
export const getSegmentedControl = () => getContext<SegmentedControlContext>(SEGMENTED_CONTROL);</script>
<script lang="ts" generics="T extends string"> import type { Snippet } from "svelte"; import { navigating } from "$app/state"; import { prefersReducedMotion } from "svelte/motion"; import { RAIL_DURATION, RAIL_EASING } from "#lib/motion/spring.ts";
interface Props { value: T; href?: (value: T) => ResolvedPathname; label?: string; class?: string; children: Snippet; }
let { value = $bindable(), href, label, class: className, children }: Props = $props();
let pending = $state<T>(); const current = $derived(pending ?? value);
$effect(() => { void value; pending = undefined; });
let navigated = false; $effect(() => { if (navigating.to) { navigated = true; } else if (navigated) { navigated = false; pending = undefined; } });
let group = $state<HTMLElement>(); let box = $state<{ left: number; top: number; width: number; height: number }>(); let animate = $state(false);
let placed: string | undefined;
const measure = (animated: boolean) => { const node = group?.querySelector<HTMLElement>(`[data-segment="${CSS.escape(current)}"]`); const next = node?.offsetParent ? { left: node.offsetLeft, top: node.offsetTop, width: node.offsetWidth, height: node.offsetHeight } : undefined; animate = animated; box = next; return next !== undefined; };
$effect(() => { placed = measure(placed !== undefined && placed !== current) ? current : undefined; });
$effect(() => { if (!group) return; const observer = new ResizeObserver(() => measure(false)); observer.observe(group); for (const node of group.querySelectorAll("[data-segment]")) observer.observe(node); return () => observer.disconnect(); });
setContext<SegmentedControlContext>(SEGMENTED_CONTROL, { get value() { return current; }, get ready() { return box !== undefined; }, get href() { return href as ((value: string) => ResolvedPathname) | undefined; }, select: (next) => { if (href) pending = next as T; else value = next as T; } });
const onkeydown = (event: KeyboardEvent) => { if (!group) return; const radios = [ ...group.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)') ]; const index = radios.indexOf(event.target as HTMLButtonElement); if (index < 0) return; const target = { ArrowRight: index + 1, ArrowDown: index + 1, ArrowLeft: index - 1, ArrowUp: index - 1, Home: 0, End: radios.length - 1 }[event.key]; if (target === undefined) return; event.preventDefault(); const radio = radios[(target + radios.length) % radios.length]; radio.focus(); radio.click(); };
const style = $derived(segmentedControl({ ready: box !== undefined }));</script>
<div bind:this={group} role={href ? "group" : "radiogroup"} aria-label={label} class={style.group({ class: className })} onkeydown={href ? undefined : onkeydown}> <span aria-hidden="true" class={style.thumb()} style:translate="{box?.left ?? 0}px {box?.top ?? 0}px" style:width="{box?.width ?? 0}px" style:height="{box?.height ?? 0}px" style:transition-duration="{animate && !prefersReducedMotion.current ? RAIL_DURATION : 0}ms" style:transition-timing-function={RAIL_EASING} ></span> {@render children()}</div>