Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
at icy/tmrrpn
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
export const button = tv({ base: [ "inline-flex cursor-pointer items-center justify-center overflow-hidden rounded-l-(--btn-radius-l) rounded-r-(--btn-radius-r) no-underline select-none [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] [--btn-radius:var(--radius-sm)]", "transition-colors duration-150 ease-in-out", "hover:no-underline", "focus-visible:outline-2 focus-visible:outline-offset-2", // Figma models disabled as a distinct fill/text pair, not a dimmed enabled state "disabled:pointer-events-none aria-disabled:pointer-events-none" ], variants: { variant: { default: [ "border border-border-default bg-background-default text-foreground-default", "hover:bg-background-subtle", "focus-visible:outline-border-focus", "disabled:border-transparent disabled:bg-background-subtle disabled:text-foreground-subtle", "aria-disabled:border-transparent aria-disabled:bg-background-subtle aria-disabled:text-foreground-subtle" ], primary: [ "border border-border-success bg-background-success-emphasis text-foreground-on-emphasis", "hover:bg-background-success-emphasis-hover", "focus-visible:outline-background-success-emphasis", "disabled:border-transparent disabled:bg-background-success-subtle disabled:text-foreground-success-disabled", "aria-disabled:border-transparent aria-disabled:bg-background-success-subtle aria-disabled:text-foreground-success-disabled" ], ghost: [ "border border-transparent bg-transparent", "hover:bg-background-inset", "focus-visible:outline-border-focus", "disabled:text-foreground-disabled aria-disabled:text-foreground-disabled" ], // the outline is what carries the variant, so dropping it would read as a // different button. danger: [ "border border-border-default bg-background-default text-foreground-danger", "hover:bg-background-subtle", "focus-visible:outline-foreground-danger", "disabled:border-border-subtle disabled:text-foreground-danger-disabled aria-disabled:text-foreground-danger-disabled" ], success: [ "border border-border-default bg-background-default text-foreground-success", "hover:bg-background-subtle", "focus-visible:outline-foreground-success", "disabled:border-border-subtle disabled:text-foreground-success-disabled aria-disabled:text-foreground-success-disabled" ], warning: [ "border border-border-default bg-background-default text-foreground-warning", "hover:bg-background-subtle", "focus-visible:outline-foreground-warning", "disabled:border-border-subtle disabled:text-foreground-warning-disabled aria-disabled:text-foreground-warning-disabled" ] }, // the min-h is the button's real height (32/36/40 in Figma), so py has to stay // small enough that line-height + padding + border never exceeds it — otherwise a // button with a label grows past the min-h while an icon-only one stays on it, and // the two render at different heights side by side. py only takes over as the // floor when a label wraps to a second line. size: { mini: "min-h-6 gap-1 px-1 py-0.5 typography-paragraph-mini", small: "min-h-8 gap-1.5 px-2 py-1 typography-paragraph-small", regular: "min-h-9 gap-2 px-3 py-1 typography-paragraph-regular", large: "min-h-10 gap-2.5 px-4 py-1 typography-paragraph-large" }, insetShadow: { true: "", false: "" } }, compoundVariants: [ { variant: ["default", "danger", "success", "warning"], insetShadow: true, class: [ "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)]", "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]", "active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]", "disabled:before:shadow-none disabled:active:*:translate-y-0" ] }, { variant: "primary", insetShadow: true, class: [ "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-alpha-light-25)]", "active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-dark-50)] dark:active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-light-50)]", "disabled:before:shadow-none disabled:active:*:translate-y-0" ] } ], defaultVariants: { variant: "default", size: "regular", insetShadow: false } });
export type ButtonVariants = VariantProps<typeof button>;</script>
<script lang="ts"> import type { ResolvedPathname } from "$app/types"; import type { Component, Snippet } from "svelte"; import type { HTMLAnchorAttributes, HTMLButtonAttributes, SvelteHTMLElements } from "svelte/elements"; import Spinner from "./Spinner.svelte";
interface Props extends Omit< HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children" > { variant?: ButtonVariants["variant"]; size?: ButtonVariants["size"]; insetShadow?: boolean; href?: ResolvedPathname; type?: "button" | "submit" | "reset"; disabled?: boolean; icon?: Component<SvelteHTMLElements["svg"]>; iconSide?: string; class?: string; loading?: boolean; spinnerClass?: string; ref?: HTMLAnchorElement | HTMLButtonElement; children?: Snippet; }
let { variant = "default", size = "regular", insetShadow = false, href, type = "button", disabled = false, icon, iconSide = "left", class: className, loading = false, spinnerClass, ref = $bindable(), children, ...rest }: Props = $props();
const classes = $derived(button({ variant, size, insetShadow, class: className })); const spinnerSize = $derived(spinnerClass ?? (size === "large" ? "size-5" : "size-4")); const iconSize = $derived(size === "large" ? "size-5" : "size-4"); const Icon = $derived(icon);</script>
{#snippet content()} <!-- single wrapper so the press animation moves the label/icon, not the button box. the transition matches the ::before shadow's so both halves of the press ease together. --> <span class="inline-flex min-w-0 items-center justify-center gap-[inherit] transition-transform duration-150 ease-in-out" > {#if loading} <Spinner class={spinnerSize} /> {:else} {#if Icon && iconSide == "left"} <Icon class="{iconSize} shrink-0" aria-hidden="true" /> {/if} {#if children} {@render children()} {/if} {#if Icon && iconSide == "right"} <Icon class="{iconSize} shrink-0" aria-hidden="true" /> {/if} {/if} </span>{/snippet}
{#if href} <a bind:this={ref} {href} class={classes} data-variant={variant} aria-disabled={disabled || loading ? "true" : undefined} tabindex={disabled ? -1 : undefined} {...rest} > {@render content()} </a>{:else} <button bind:this={ref} {type} disabled={disabled || loading} class={classes} data-variant={variant} aria-busy={loading} {...rest} > {@render content()} </button>{/if}