This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web: add Toggle component

+101
+44
web/src/lib/components/ui/Toggle.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Toggle from "./Toggle.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "UI/Toggle", 7 + component: Toggle, 8 + tags: ["autodocs"], 9 + argTypes: { 10 + checked: { control: "boolean" }, 11 + disabled: { control: "boolean" }, 12 + layout: { control: "inline-radio", options: ["inline", "block"] } 13 + }, 14 + args: { 15 + checked: false, 16 + disabled: false, 17 + layout: "inline" 18 + } 19 + }); 20 + </script> 21 + 22 + <!-- Toggle content comes through its `children` snippet, provided here as Story slot content. --> 23 + <Story name="Default">Label</Story> 24 + <Story name="Checked" args={{ checked: true }}>Label</Story> 25 + <Story name="Disabled" args={{ disabled: true }}>Label</Story> 26 + <Story name="Disabled checked" args={{ disabled: true, checked: true }}>Label</Story> 27 + 28 + <Story name="Without label" /> 29 + 30 + <Story name="States"> 31 + <div class="flex flex-col gap-3"> 32 + <Toggle>Unchecked</Toggle> 33 + <Toggle checked>Checked</Toggle> 34 + <Toggle disabled>Disabled</Toggle> 35 + <Toggle disabled checked>Disabled checked</Toggle> 36 + </div> 37 + </Story> 38 + 39 + <Story name="Block layout"> 40 + <div class="flex w-64 flex-col gap-3"> 41 + <Toggle layout="block">Label pushed to the far edge</Toggle> 42 + <Toggle layout="block" checked>Another one, wrapping when the text runs long</Toggle> 43 + </div> 44 + </Story>
+57
web/src/lib/components/ui/Toggle.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + import type { HTMLInputAttributes } from "svelte/elements"; 4 + 5 + interface Props extends Omit<HTMLInputAttributes, "type" | "class" | "checked" | "children"> { 6 + checked?: boolean; 7 + disabled?: boolean; 8 + /** `inline` hugs the label, `block` stretches it to fill the row. */ 9 + layout?: "inline" | "block"; 10 + class?: string; 11 + children?: Snippet; 12 + } 13 + 14 + let { 15 + checked = $bindable(false), 16 + disabled = false, 17 + layout = "inline", 18 + class: className, 19 + children, 20 + ...rest 21 + }: Props = $props(); 22 + 23 + const labelClasses = $derived( 24 + [ 25 + "items-center gap-2", 26 + layout === "block" ? "flex w-full" : "inline-flex", 27 + disabled ? "cursor-not-allowed text-foreground-disabled" : "cursor-pointer", 28 + className ?? "" 29 + ].join(" ") 30 + ); 31 + 32 + const inputClasses = [ 33 + "peer absolute inset-0 size-full appearance-none rounded-full bg-background-muted", 34 + "transition-colors duration-150", 35 + "checked:bg-foreground-default", 36 + "enabled:hover:bg-background-inset checked:enabled:hover:bg-foreground-muted", 37 + "focus-visible:ring-2 focus-visible:ring-border-strong focus-visible:outline-none", 38 + "disabled:cursor-not-allowed disabled:bg-background-inset checked:disabled:bg-foreground-disabled" 39 + ].join(" "); 40 + 41 + const thumbClasses = [ 42 + "pointer-events-none absolute top-px left-px size-4 rounded-full bg-foreground-on-emphasis", 43 + "transition-transform duration-150 peer-checked:translate-x-[15px]" 44 + ].join(" "); 45 + 46 + const textClasses = $derived(layout === "block" ? "min-w-0 flex-1" : "whitespace-nowrap"); 47 + </script> 48 + 49 + <label class={labelClasses}> 50 + <span class="relative inline-flex h-[18px] w-[33px] shrink-0"> 51 + <input type="checkbox" role="switch" bind:checked {disabled} class={inputClasses} {...rest} /> 52 + <span class={thumbClasses} aria-hidden="true"></span> 53 + </span> 54 + {#if children} 55 + <span class={textClasses}>{@render children()}</span> 56 + {/if} 57 + </label>