Monorepo for Tangled tangled.org
1

Configure Feed

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

web/ui/components: 3rd batch 🥧

Signed-off-by: eti <eti@eti.tf>

author
eti
committer
dawn
date (Jul 27, 2026, 3:40 PM +0300) commit 6b42df7c parent 66d73cf2
+60 -185
+2
web/src/app.css
··· 47 47 --color-foreground-success-disabled: #16a34a80; 48 48 --color-foreground-danger: #dc2626; 49 49 --color-foreground-danger-strong: #991b1b; 50 + --color-foreground-danger-subtle: #f87171; 50 51 --color-foreground-warning: #d97706; 51 52 --color-foreground-warning-strong: #92400e; 52 53 --color-foreground-info: #4338ca; ··· 73 74 --color-border-focus-danger: #ef4444; 74 75 --color-border-success: #15803d; 75 76 --color-border-danger: #fca5a5; 77 + --color-border-danger-subtle: #fecaca; 76 78 77 79 /* --- shadow (inner depth) --- */ 78 80 --color-shadow-inner-strong: #d1d5db;
+6 -1
web/src/lib/components/ui/Button.svelte
··· 104 104 type?: "button" | "submit" | "reset"; 105 105 disabled?: boolean; 106 106 icon?: Component<SvelteHTMLElements["svg"]>; 107 + iconSide?: string; 107 108 class?: string; 108 109 loading?: boolean; 109 110 spinnerClass?: string; ··· 118 119 type = "button", 119 120 disabled = false, 120 121 icon, 122 + iconSide = "left", 121 123 class: className, 122 124 loading = false, 123 125 spinnerClass, ··· 135 137 {#if loading} 136 138 <Spinner class={spinnerSize} /> 137 139 {:else} 138 - {#if Icon} 140 + {#if Icon && iconSide == "left"} 139 141 <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 140 142 {/if} 141 143 {#if children} 142 144 {@render children()} 145 + {/if} 146 + {#if Icon && iconSide == "right"} 147 + <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 143 148 {/if} 144 149 {/if} 145 150 {/snippet}
+3 -1
web/src/lib/components/ui/Pagination.stories.svelte
··· 8 8 tags: ["autodocs"], 9 9 argTypes: { 10 10 page: { control: { type: "number" } }, 11 - total: { control: { type: "number" } } 11 + total: { control: { type: "number" } }, 12 + labels: { control: { type: "boolean" } } 12 13 }, 13 14 args: { 14 15 page: 1, ··· 23 24 <Story name="FewPages" args={{ page: 2, total: 3 }} /> 24 25 <Story name="SinglePage" args={{ page: 1, total: 1 }} /> 25 26 <Story name="ManyPages" args={{ page: 12, total: 40 }} /> 27 + <Story name="WithLabels" args={{ page: 5, total: 10, labels: true }} />
+9 -9
web/src/lib/components/ui/Pagination.svelte
··· 58 58 total: number; 59 59 onchange?: (page: number) => void; 60 60 class?: string; 61 + labels?: boolean; 61 62 } 62 63 63 - let { page = $bindable(1), total, onchange, class: className }: Props = $props(); 64 + let { page = $bindable(1), total, onchange, class: className, labels }: Props = $props(); 64 65 65 66 const count = $derived(Math.max(total, 1)); 66 67 const items = $derived(getPageItems(Math.min(Math.max(page, 1), count), count)); ··· 76 77 <nav aria-label="Pagination" class={pagination({ class: className })}> 77 78 <Button 78 79 variant="ghost" 79 - size="sm" 80 - class="size-8 p-0" 81 80 icon={ChevronLeft} 82 81 disabled={page <= 1} 83 82 aria-label="Previous page" 84 83 onclick={() => goTo(page - 1)} 85 - /> 84 + > 85 + <span class="hidden sm:inline">{labels ? "Previous" : ""}</span> 86 + </Button> 86 87 87 88 {#each items as item, i (typeof item === "number" ? item : `${item}-${i}`)} 88 89 {#if typeof item === "number"} 89 90 <Button 90 91 variant={item === page ? "default" : "ghost"} 91 - size="sm" 92 - class="size-8 p-0" 93 92 aria-current={item === page ? "page" : undefined} 94 93 aria-label={`Page ${item}`} 95 94 onclick={() => goTo(item)} ··· 108 107 109 108 <Button 110 109 variant="ghost" 111 - size="sm" 112 - class="size-8 p-0" 113 110 icon={ChevronRight} 111 + iconSide="right" 114 112 disabled={page >= count} 115 113 aria-label="Next page" 116 114 onclick={() => goTo(page + 1)} 117 - /> 115 + > 116 + <span class="hidden sm:inline">{labels ? "Next" : ""}</span> 117 + </Button> 118 118 </nav>
-8
web/src/lib/components/ui/Radio.stories.svelte
··· 24 24 <Story name="Checked" args={{ group: "a" }}>Option</Story> 25 25 <Story name="Disabled" args={{ disabled: true }}>Option</Story> 26 26 <Story name="Disabled checked" args={{ group: "a", disabled: true }}>Option</Story> 27 - 28 - <Story name="Group"> 29 - <div class="flex items-center gap-4"> 30 - <Radio value="a" group="a">Option</Radio> 31 - <Radio value="b" group="a">Option</Radio> 32 - <Radio value="c" group="a" disabled>Option</Radio> 33 - </div> 34 - </Story>
+11 -4
web/src/lib/components/ui/Radio.svelte
··· 20 20 }: Props = $props(); 21 21 22 22 const labelClasses = $derived( 23 - `inline-flex items-center gap-2 cursor-pointer ${disabled ? "cursor-not-allowed text-foreground-disabled" : ""} ${className ?? ""}` 23 + `inline-flex items-center gap-2 ${disabled ? "cursor-not-allowed text-foreground-disabled" : "cursor-pointer"} ${className ?? ""}` 24 24 ); 25 25 </script> 26 26 27 27 <label class={labelClasses}> 28 - <span class="relative inline-flex size-4 shrink-0"> 28 + <span class="relative inline-flex"> 29 29 <input 30 30 type="radio" 31 31 {value} 32 32 bind:group 33 33 {disabled} 34 - class="peer size-4 shrink-0 appearance-none rounded-full border border-border-strong bg-background-default transition-colors duration-150 hover:bg-background-subtle checked:border-foreground-default focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground-default disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50" 34 + class="peer size-4 shrink-0 appearance-none rounded-full cursor-pointer border border-border-strong bg-background-default transition-colors duration-150 \ 35 + hover:bg-background-subtle \ 36 + checked:border-foreground-default \ 37 + focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground-default \ 38 + disabled:cursor-not-allowed disabled:pointer-events-none disabled:border-border-disabled disabled:hover:bg-inherit" 35 39 {...rest} 36 40 /> 37 41 <span 38 - class="pointer-events-none absolute inset-0 m-auto hidden size-2 rounded-full bg-foreground-default peer-checked:block peer-disabled:opacity-50" 42 + class="absolute inset-0 m-auto hidden size-2 rounded-full \ 43 + bg-foreground-default \ 44 + peer-checked:block \ 45 + peer-disabled:text-foreground-disabled peer-disabled:bg-foreground-disabled" 39 46 aria-hidden="true" 40 47 ></span> 41 48 </span>
-38
web/src/lib/components/ui/Scaffold.stories.svelte
··· 1 - <script module lang="ts"> 2 - import { defineMeta } from "@storybook/addon-svelte-csf"; 3 - import Scaffold from "./Scaffold.svelte"; 4 - 5 - const { Story } = defineMeta({ 6 - title: "UI/Scaffold", 7 - component: Scaffold, 8 - tags: ["autodocs"], 9 - argTypes: { 10 - width: { 11 - control: { type: "inline-radio" }, 12 - options: ["lg", "xl", "full"] 13 - } 14 - }, 15 - args: { 16 - width: "lg" 17 - } 18 - }); 19 - </script> 20 - 21 - <!-- Scaffold content comes through its `children` snippet, provided here as Story slot content. --> 22 - <Story name="Default"> 23 - <Scaffold> 24 - <div class="rounded border border-border-default bg-background-default p-4">Content</div> 25 - </Scaffold> 26 - </Story> 27 - 28 - <Story name="Wide" args={{ width: "xl" }}> 29 - <Scaffold width="xl"> 30 - <div class="rounded border border-border-default bg-background-default p-4">Content</div> 31 - </Scaffold> 32 - </Story> 33 - 34 - <Story name="Full" args={{ width: "full" }}> 35 - <Scaffold width="full"> 36 - <div class="rounded border border-border-default bg-background-default p-4">Content</div> 37 - </Scaffold> 38 - </Story>
-39
web/src/lib/components/ui/Scaffold.svelte
··· 1 - <script module lang="ts"> 2 - import { tv, type VariantProps } from "tailwind-variants"; 3 - 4 - // Echoes the +layout.svelte container idiom: a centered, padded section 5 - // clamped to a max width (matches Figma node 35:791's single Content slot). 6 - export const scaffold = tv({ 7 - base: "mx-auto w-full px-4 py-6", 8 - variants: { 9 - width: { 10 - lg: "max-w-screen-lg", 11 - xl: "max-w-screen-xl", 12 - full: "max-w-full" 13 - } 14 - }, 15 - defaultVariants: { 16 - width: "lg" 17 - } 18 - }); 19 - 20 - export type ScaffoldVariants = VariantProps<typeof scaffold>; 21 - </script> 22 - 23 - <script lang="ts"> 24 - import type { Snippet } from "svelte"; 25 - 26 - interface Props { 27 - width?: ScaffoldVariants["width"]; 28 - class?: string; 29 - children: Snippet; 30 - } 31 - 32 - let { width = "lg", class: className, children }: Props = $props(); 33 - 34 - const classes = $derived(scaffold({ width, class: className })); 35 - </script> 36 - 37 - <div class={classes}> 38 - {@render children()} 39 - </div>
-21
web/src/lib/components/ui/Select.stories.svelte
··· 42 42 <option value="two">Two</option> 43 43 <option value="three">Three</option> 44 44 </Story> 45 - 46 - <Story name="States"> 47 - <div class="flex flex-col gap-3"> 48 - <Select> 49 - <option value="" disabled selected>Select</option> 50 - <option value="one">One</option> 51 - </Select> 52 - <Select value="one"> 53 - <option value="one">One</option> 54 - <option value="two">Two</option> 55 - </Select> 56 - <Select error value="one"> 57 - <option value="one">One</option> 58 - <option value="two">Two</option> 59 - </Select> 60 - <Select disabled value="one"> 61 - <option value="one">One</option> 62 - <option value="two">Two</option> 63 - </Select> 64 - </div> 65 - </Story>
+29 -5
web/src/lib/components/ui/Select.svelte
··· 11 11 }, 12 12 disabled: { 13 13 false: "", 14 - true: "bg-background-inset text-foreground-disabled" 14 + true: "text-foreground-disabled" 15 15 } 16 16 }, 17 + compoundVariants: [ 18 + { 19 + error: true, 20 + disabled: true, 21 + class: 22 + "border-border-danger-subtle bg-background-danger-subtle text-foreground-danger-subtle" 23 + } 24 + ], 17 25 defaultVariants: { 18 26 error: false, 19 27 disabled: false ··· 21 29 }); 22 30 23 31 export type SelectVariants = VariantProps<typeof select>; 32 + 33 + const chevron = tv({ 34 + base: "pointer-events-none absolute right-3 top-1/2 size-3.5 -translate-y-1/2 text-foreground-subtle", 35 + variants: { 36 + error: { 37 + true: "text-foreground-danger" 38 + }, 39 + disabled: { 40 + true: "text-foreground-disabled" 41 + } 42 + }, 43 + compoundVariants: [ 44 + { 45 + error: true, 46 + disabled: true, 47 + class: "text-foreground-danger-subtle" 48 + } 49 + ] 50 + }); 24 51 </script> 25 52 26 53 <script lang="ts"> ··· 54 81 {@render children()} 55 82 {/if} 56 83 </select> 57 - <ChevronDown 58 - class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-foreground-subtle" 59 - aria-hidden="true" 60 - /> 84 + <ChevronDown class={chevron({ error, disabled })} aria-hidden="true" /> 61 85 </div>
-30
web/src/lib/components/ui/Thumbnail.stories.svelte
··· 1 - <script module lang="ts"> 2 - import { defineMeta } from "@storybook/addon-svelte-csf"; 3 - import Thumbnail from "./Thumbnail.svelte"; 4 - 5 - const { Story } = defineMeta({ 6 - title: "UI/Thumbnail", 7 - component: Thumbnail, 8 - tags: ["autodocs"], 9 - argTypes: { 10 - title: { control: "text" } 11 - }, 12 - args: { 13 - title: "Design System" 14 - } 15 - }); 16 - </script> 17 - 18 - <Story name="Default"> 19 - <div class="w-96"> 20 - <Thumbnail /> 21 - </div> 22 - </Story> 23 - 24 - <Story name="Custom content"> 25 - <div class="w-96"> 26 - <Thumbnail> 27 - <span class="text-2xl font-bold text-foreground-default sm:text-3xl">Thumbnail</span> 28 - </Thumbnail> 29 - </div> 30 - </Story>
-29
web/src/lib/components/ui/Thumbnail.svelte
··· 1 - <script lang="ts"> 2 - import type { Snippet } from "svelte"; 3 - import Logo from "./Logo.svelte"; 4 - 5 - interface Props { 6 - title?: string; 7 - class?: string; 8 - children?: Snippet; 9 - } 10 - 11 - let { title, class: className, children }: Props = $props(); 12 - </script> 13 - 14 - <div 15 - class={`relative flex aspect-video flex-col gap-6 overflow-hidden rounded-lg border border-border-default bg-background-subtle p-8 ${className ?? ""}`} 16 - > 17 - <div 18 - class="pointer-events-none absolute inset-0 bg-[radial-gradient(var(--color-border-strong)_1px,transparent_1px)] bg-[length:12px_12px] [mask-image:linear-gradient(to_bottom,transparent,black)] opacity-60" 19 - aria-hidden="true" 20 - ></div> 21 - <div class="relative z-10 flex flex-col gap-6"> 22 - <Logo wordmark /> 23 - {#if children} 24 - {@render children()} 25 - {:else if title} 26 - <span class="text-2xl font-bold text-foreground-default sm:text-3xl">{title}</span> 27 - {/if} 28 - </div> 29 - </div>