Monorepo for Tangled tangled.org
1

Configure Feed

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

web: add error component

slightly inspired by vercel/geist

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
committer
dawn
date (Jul 23, 2026, 6:48 PM +0300) commit 56d1fc95 parent 5c251edf change-id toxrpqxo
+104
+24
web/src/lib/components/ui/Error.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Error from "./Error.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "UI/Error", 7 + component: Error, 8 + tags: ["autodocs"], 9 + argTypes: { 10 + size: { 11 + control: { type: "inline-radio" }, 12 + options: ["small", "large"] 13 + }, 14 + label: { control: "text" } 15 + }, 16 + args: { 17 + size: "small", 18 + label: "This email address is already in use." 19 + } 20 + }); 21 + </script> 22 + 23 + <Story name="Small" /> 24 + <Story name="Large" args={{ size: "large" }} />
+80
web/src/lib/components/ui/Error.svelte
··· 1 + <script module lang="ts"> 2 + import { tv, type VariantProps } from "tailwind-variants"; 3 + 4 + export const error = tv({ 5 + slots: { 6 + root: "overflow-hidden rounded border border-border-danger bg-background-danger-subtle text-foreground-danger dark:border-border-danger/60 dark:bg-background-danger-subtle/20", 7 + header: "flex items-center gap-2", 8 + icon: "shrink-0", 9 + label: "min-w-0", 10 + toggle: 11 + "flex w-full items-center gap-2 border-t border-border-danger text-foreground-danger transition-colors hover:text-foreground-danger-strong dark:border-border-danger/60", 12 + chevron: "shrink-0 transition-transform", 13 + body: "border-t border-border-danger font-mono text-foreground-danger-strong dark:border-border-danger/60" 14 + }, 15 + variants: { 16 + size: { 17 + small: { 18 + header: "px-3 py-2 text-sm", 19 + icon: "size-4", 20 + toggle: "px-3 py-2 text-sm", 21 + chevron: "size-4", 22 + body: "px-3 py-2 text-xs" 23 + }, 24 + large: { 25 + header: "px-4 py-3 text-base", 26 + icon: "size-5", 27 + toggle: "px-4 py-3 text-sm", 28 + chevron: "size-4", 29 + body: "px-4 py-3 text-sm" 30 + } 31 + } 32 + }, 33 + defaultVariants: { 34 + size: "small" 35 + } 36 + }); 37 + 38 + export type ErrorVariants = VariantProps<typeof error>; 39 + </script> 40 + 41 + <script lang="ts"> 42 + import type { Snippet } from "svelte"; 43 + import type { HTMLAttributes } from "svelte/elements"; 44 + import OctagonAlert from "$icon/octagon-alert"; 45 + import ChevronRight from "$icon/chevron-right"; 46 + 47 + interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class" | "children"> { 48 + /** The error message shown next to the icon. */ 49 + label: string; 50 + size?: ErrorVariants["size"]; 51 + class?: string; 52 + /** Optional collapsible content revealed by a "Show code" toggle. */ 53 + children?: Snippet; 54 + } 55 + 56 + let { label, size = "small", class: className, children, ...rest }: Props = $props(); 57 + 58 + let open = $state(false); 59 + 60 + const classes = $derived(error({ size })); 61 + </script> 62 + 63 + <div class={classes.root({ class: className })} role="alert" {...rest}> 64 + <div class={classes.header()}> 65 + <OctagonAlert class={classes.icon()} aria-hidden="true" /> 66 + <span class={classes.label()}>{label}</span> 67 + </div> 68 + 69 + {#if children} 70 + <button type="button" class={classes.toggle()} onclick={() => (open = !open)}> 71 + <ChevronRight class="{classes.chevron()} {open ? 'rotate-90' : ''}" aria-hidden="true" /> 72 + <span>Show code</span> 73 + </button> 74 + {#if open} 75 + <div class={classes.body()}> 76 + {@render children()} 77 + </div> 78 + {/if} 79 + {/if} 80 + </div>