detached d178634

feat(webui): add shadcn-svelte ui component library with utilities

commit
d1786342b8d3f0ba0323e5cad0579f60deb918e9
author
bnema · · 2025-12-21 13:53 UTC
parent
ac16f2b
signature
unsigned
34 files · +710 -0
A webui/components.json +16 -0
@@ -0,0 +1,16 @@
1+{
2+ "$schema": "https://shadcn-svelte.com/schema.json",
3+ "tailwind": {
4+ "css": "src/styles/app.css",
5+ "baseColor": "neutral"
6+ },
7+ "aliases": {
8+ "components": "$lib/components",
9+ "utils": "$lib/utils",
10+ "ui": "$lib/components/ui",
11+ "hooks": "$lib/hooks",
12+ "lib": "$lib"
13+ },
14+ "typescript": true,
15+ "registry": "https://shadcn-svelte.com/registry"
16+}
@@ -0,0 +1,16 @@
1+{
2+ "$schema": "https://shadcn-svelte.com/schema.json",
3+ "tailwind": {
4+ "css": "src/styles/app.css",
5+ "baseColor": "neutral"
6+ },
7+ "aliases": {
8+ "components": "$lib/components",
9+ "utils": "$lib/utils",
10+ "ui": "$lib/components/ui",
11+ "hooks": "$lib/hooks",
12+ "lib": "$lib"
13+ },
14+ "typescript": true,
15+ "registry": "https://shadcn-svelte.com/registry"
16+}
A webui/src/lib/components/ui/button/button.svelte +82 -0
@@ -0,0 +1,82 @@
1+<script lang="ts" module>
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4+ import { type VariantProps, tv } from "tailwind-variants";
5+
6+ export const buttonVariants = tv({
7+ base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8+ variants: {
9+ variant: {
10+ default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
11+ destructive:
12+ "bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
13+ outline:
14+ "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border border-border shadow-xs",
15+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
16+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
17+ link: "text-primary underline-offset-4 hover:underline",
18+ },
19+ size: {
20+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
21+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
22+ lg: "h-10 px-6 has-[>svg]:px-4",
23+ icon: "size-9",
24+ "icon-sm": "size-8",
25+ "icon-lg": "size-10",
26+ },
27+ },
28+ defaultVariants: {
29+ variant: "default",
30+ size: "default",
31+ },
32+ });
33+
34+ export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
35+ export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
36+
37+ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
38+ WithElementRef<HTMLAnchorAttributes> & {
39+ variant?: ButtonVariant;
40+ size?: ButtonSize;
41+ };
42+</script>
43+
44+<script lang="ts">
45+ let {
46+ class: className,
47+ variant = "default",
48+ size = "default",
49+ ref = $bindable(null),
50+ href = undefined,
51+ type = "button",
52+ disabled,
53+ children,
54+ ...restProps
55+ }: ButtonProps = $props();
56+</script>
57+
58+{#if href}
59+ <a
60+ bind:this={ref}
61+ data-slot="button"
62+ class={cn(buttonVariants({ variant, size }), className)}
63+ href={disabled ? undefined : href}
64+ aria-disabled={disabled}
65+ role={disabled ? "link" : undefined}
66+ tabindex={disabled ? -1 : undefined}
67+ {...restProps}
68+ >
69+ {@render children?.()}
70+ </a>
71+{:else}
72+ <button
73+ bind:this={ref}
74+ data-slot="button"
75+ class={cn(buttonVariants({ variant, size }), className)}
76+ {type}
77+ {disabled}
78+ {...restProps}
79+ >
80+ {@render children?.()}
81+ </button>
82+{/if}
@@ -0,0 +1,82 @@
1+<script lang="ts" module>
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4+ import { type VariantProps, tv } from "tailwind-variants";
5+
6+ export const buttonVariants = tv({
7+ base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8+ variants: {
9+ variant: {
10+ default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
11+ destructive:
12+ "bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
13+ outline:
14+ "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border border-border shadow-xs",
15+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
16+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
17+ link: "text-primary underline-offset-4 hover:underline",
18+ },
19+ size: {
20+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
21+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
22+ lg: "h-10 px-6 has-[>svg]:px-4",
23+ icon: "size-9",
24+ "icon-sm": "size-8",
25+ "icon-lg": "size-10",
26+ },
27+ },
28+ defaultVariants: {
29+ variant: "default",
30+ size: "default",
31+ },
32+ });
33+
34+ export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
35+ export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
36+
37+ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
38+ WithElementRef<HTMLAnchorAttributes> & {
39+ variant?: ButtonVariant;
40+ size?: ButtonSize;
41+ };
42+</script>
43+
44+<script lang="ts">
45+ let {
46+ class: className,
47+ variant = "default",
48+ size = "default",
49+ ref = $bindable(null),
50+ href = undefined,
51+ type = "button",
52+ disabled,
53+ children,
54+ ...restProps
55+ }: ButtonProps = $props();
56+</script>
57+
58+{#if href}
59+ <a
60+ bind:this={ref}
61+ data-slot="button"
62+ class={cn(buttonVariants({ variant, size }), className)}
63+ href={disabled ? undefined : href}
64+ aria-disabled={disabled}
65+ role={disabled ? "link" : undefined}
66+ tabindex={disabled ? -1 : undefined}
67+ {...restProps}
68+ >
69+ {@render children?.()}
70+ </a>
71+{:else}
72+ <button
73+ bind:this={ref}
74+ data-slot="button"
75+ class={cn(buttonVariants({ variant, size }), className)}
76+ {type}
77+ {disabled}
78+ {...restProps}
79+ >
80+ {@render children?.()}
81+ </button>
82+{/if}
A webui/src/lib/components/ui/button/index.ts +17 -0
@@ -0,0 +1,17 @@
1+import Root, {
2+ type ButtonProps,
3+ type ButtonSize,
4+ type ButtonVariant,
5+ buttonVariants,
6+} from "./button.svelte";
7+
8+export {
9+ Root,
10+ type ButtonProps as Props,
11+ //
12+ Root as Button,
13+ buttonVariants,
14+ type ButtonProps,
15+ type ButtonSize,
16+ type ButtonVariant,
17+};
@@ -0,0 +1,17 @@
1+import Root, {
2+ type ButtonProps,
3+ type ButtonSize,
4+ type ButtonVariant,
5+ buttonVariants,
6+} from "./button.svelte";
7+
8+export {
9+ Root,
10+ type ButtonProps as Props,
11+ //
12+ Root as Button,
13+ buttonVariants,
14+ type ButtonProps,
15+ type ButtonSize,
16+ type ButtonVariant,
17+};
A webui/src/lib/components/ui/card/card-action.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-action"
16+ class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-action"
16+ class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
A webui/src/lib/components/ui/card/card-content.svelte +15 -0
@@ -0,0 +1,15 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
14+ {@render children?.()}
15+</div>
@@ -0,0 +1,15 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
14+ {@render children?.()}
15+</div>
A webui/src/lib/components/ui/card/card-description.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
11+</script>
12+
13+<p
14+ bind:this={ref}
15+ data-slot="card-description"
16+ class={cn("text-muted-foreground text-sm", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</p>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
11+</script>
12+
13+<p
14+ bind:this={ref}
15+ data-slot="card-description"
16+ class={cn("text-muted-foreground text-sm", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</p>
A webui/src/lib/components/ui/card/card-footer.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-footer"
16+ class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-footer"
16+ class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
A webui/src/lib/components/ui/card/card-header.svelte +23 -0
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-header"
16+ class={cn(
17+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</div>
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-header"
16+ class={cn(
17+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</div>
A webui/src/lib/components/ui/card/card-title.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-title"
16+ class={cn("leading-none font-semibold", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card-title"
16+ class={cn("leading-none font-semibold", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</div>
A webui/src/lib/components/ui/card/card.svelte +23 -0
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card"
16+ class={cn(
17+ "bg-card text-card-foreground flex flex-col gap-6 border border-border py-6 shadow-sm",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</div>
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import type { HTMLAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+</script>
12+
13+<div
14+ bind:this={ref}
15+ data-slot="card"
16+ class={cn(
17+ "bg-card text-card-foreground flex flex-col gap-6 border border-border py-6 shadow-sm",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</div>
A webui/src/lib/components/ui/card/index.ts +25 -0
@@ -0,0 +1,25 @@
1+import Root from "./card.svelte";
2+import Content from "./card-content.svelte";
3+import Description from "./card-description.svelte";
4+import Footer from "./card-footer.svelte";
5+import Header from "./card-header.svelte";
6+import Title from "./card-title.svelte";
7+import Action from "./card-action.svelte";
8+
9+export {
10+ Root,
11+ Content,
12+ Description,
13+ Footer,
14+ Header,
15+ Title,
16+ Action,
17+ //
18+ Root as Card,
19+ Content as CardContent,
20+ Description as CardDescription,
21+ Footer as CardFooter,
22+ Header as CardHeader,
23+ Title as CardTitle,
24+ Action as CardAction,
25+};
@@ -0,0 +1,25 @@
1+import Root from "./card.svelte";
2+import Content from "./card-content.svelte";
3+import Description from "./card-description.svelte";
4+import Footer from "./card-footer.svelte";
5+import Header from "./card-header.svelte";
6+import Title from "./card-title.svelte";
7+import Action from "./card-action.svelte";
8+
9+export {
10+ Root,
11+ Content,
12+ Description,
13+ Footer,
14+ Header,
15+ Title,
16+ Action,
17+ //
18+ Root as Card,
19+ Content as CardContent,
20+ Description as CardDescription,
21+ Footer as CardFooter,
22+ Header as CardHeader,
23+ Title as CardTitle,
24+ Action as CardAction,
25+};
A webui/src/lib/components/ui/input/index.ts +7 -0
@@ -0,0 +1,7 @@
1+import Root from "./input.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Input,
7+};
@@ -0,0 +1,7 @@
1+import Root from "./input.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Input,
7+};
A webui/src/lib/components/ui/input/input.svelte +52 -0
@@ -0,0 +1,52 @@
1+<script lang="ts">
2+ import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ type InputType = Exclude<HTMLInputTypeAttribute, "file">;
6+
7+ type Props = WithElementRef<
8+ Omit<HTMLInputAttributes, "type"> &
9+ ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
10+ >;
11+
12+ let {
13+ ref = $bindable(null),
14+ value = $bindable(),
15+ type,
16+ files = $bindable(),
17+ class: className,
18+ "data-slot": dataSlot = "input",
19+ ...restProps
20+ }: Props = $props();
21+</script>
22+
23+{#if type === "file"}
24+ <input
25+ bind:this={ref}
26+ data-slot={dataSlot}
27+ class={cn(
28+ "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-border ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
29+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
30+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
31+ className
32+ )}
33+ type="file"
34+ bind:files
35+ bind:value
36+ {...restProps}
37+ />
38+{:else}
39+ <input
40+ bind:this={ref}
41+ data-slot={dataSlot}
42+ class={cn(
43+ "border-border bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
44+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
45+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
46+ className
47+ )}
48+ {type}
49+ bind:value
50+ {...restProps}
51+ />
52+{/if}
@@ -0,0 +1,52 @@
1+<script lang="ts">
2+ import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ type InputType = Exclude<HTMLInputTypeAttribute, "file">;
6+
7+ type Props = WithElementRef<
8+ Omit<HTMLInputAttributes, "type"> &
9+ ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
10+ >;
11+
12+ let {
13+ ref = $bindable(null),
14+ value = $bindable(),
15+ type,
16+ files = $bindable(),
17+ class: className,
18+ "data-slot": dataSlot = "input",
19+ ...restProps
20+ }: Props = $props();
21+</script>
22+
23+{#if type === "file"}
24+ <input
25+ bind:this={ref}
26+ data-slot={dataSlot}
27+ class={cn(
28+ "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-border ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
29+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
30+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
31+ className
32+ )}
33+ type="file"
34+ bind:files
35+ bind:value
36+ {...restProps}
37+ />
38+{:else}
39+ <input
40+ bind:this={ref}
41+ data-slot={dataSlot}
42+ class={cn(
43+ "border-border bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
44+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
45+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
46+ className
47+ )}
48+ {type}
49+ bind:value
50+ {...restProps}
51+ />
52+{/if}
A webui/src/lib/components/ui/label/index.ts +7 -0
@@ -0,0 +1,7 @@
1+import Root from "./label.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Label,
7+};
@@ -0,0 +1,7 @@
1+import Root from "./label.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Label,
7+};
A webui/src/lib/components/ui/label/label.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Label as LabelPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: LabelPrimitive.RootProps = $props();
10+</script>
11+
12+<LabelPrimitive.Root
13+ bind:ref
14+ data-slot="label"
15+ class={cn(
16+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17+ className
18+ )}
19+ {...restProps}
20+/>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Label as LabelPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: LabelPrimitive.RootProps = $props();
10+</script>
11+
12+<LabelPrimitive.Root
13+ bind:ref
14+ data-slot="label"
15+ class={cn(
16+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17+ className
18+ )}
19+ {...restProps}
20+/>
A webui/src/lib/components/ui/skeleton/index.ts +7 -0
@@ -0,0 +1,7 @@
1+import Root from "./skeleton.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Skeleton,
7+};
@@ -0,0 +1,7 @@
1+import Root from "./skeleton.svelte";
2+
3+export {
4+ Root,
5+ //
6+ Root as Skeleton,
7+};
A webui/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
@@ -0,0 +1,17 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
10+</script>
11+
12+<div
13+ bind:this={ref}
14+ data-slot="skeleton"
15+ class={cn("bg-accent animate-pulse", className)}
16+ {...restProps}
17+></div>
@@ -0,0 +1,17 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
10+</script>
11+
12+<div
13+ bind:this={ref}
14+ data-slot="skeleton"
15+ class={cn("bg-accent animate-pulse", className)}
16+ {...restProps}
17+></div>
A webui/src/lib/components/ui/spinner/index.ts +1 -0
@@ -0,0 +1,1 @@
1+export { default as Spinner } from "./spinner.svelte";
@@ -0,0 +1,1 @@
1+export { default as Spinner } from "./spinner.svelte";
A webui/src/lib/components/ui/spinner/spinner.svelte +14 -0
@@ -0,0 +1,14 @@
1+<script lang="ts">
2+ import { cn } from "$lib/utils.js";
3+ import Loader2Icon from "@lucide/svelte/icons/loader-2";
4+ import type { ComponentProps } from "svelte";
5+
6+ let { class: className, ...restProps }: ComponentProps<typeof Loader2Icon> = $props();
7+</script>
8+
9+<Loader2Icon
10+ role="status"
11+ aria-label="Loading"
12+ class={cn("size-4 animate-spin", className)}
13+ {...restProps}
14+/>
@@ -0,0 +1,14 @@
1+<script lang="ts">
2+ import { cn } from "$lib/utils.js";
3+ import Loader2Icon from "@lucide/svelte/icons/loader-2";
4+ import type { ComponentProps } from "svelte";
5+
6+ let { class: className, ...restProps }: ComponentProps<typeof Loader2Icon> = $props();
7+</script>
8+
9+<Loader2Icon
10+ role="status"
11+ aria-label="Loading"
12+ class={cn("size-4 animate-spin", className)}
13+ {...restProps}
14+/>
A webui/src/lib/components/ui/table/index.ts +28 -0
@@ -0,0 +1,28 @@
1+import Root from "./table.svelte";
2+import Body from "./table-body.svelte";
3+import Caption from "./table-caption.svelte";
4+import Cell from "./table-cell.svelte";
5+import Footer from "./table-footer.svelte";
6+import Head from "./table-head.svelte";
7+import Header from "./table-header.svelte";
8+import Row from "./table-row.svelte";
9+
10+export {
11+ Root,
12+ Body,
13+ Caption,
14+ Cell,
15+ Footer,
16+ Head,
17+ Header,
18+ Row,
19+ //
20+ Root as Table,
21+ Body as TableBody,
22+ Caption as TableCaption,
23+ Cell as TableCell,
24+ Footer as TableFooter,
25+ Head as TableHead,
26+ Header as TableHeader,
27+ Row as TableRow,
28+};
@@ -0,0 +1,28 @@
1+import Root from "./table.svelte";
2+import Body from "./table-body.svelte";
3+import Caption from "./table-caption.svelte";
4+import Cell from "./table-cell.svelte";
5+import Footer from "./table-footer.svelte";
6+import Head from "./table-head.svelte";
7+import Header from "./table-header.svelte";
8+import Row from "./table-row.svelte";
9+
10+export {
11+ Root,
12+ Body,
13+ Caption,
14+ Cell,
15+ Footer,
16+ Head,
17+ Header,
18+ Row,
19+ //
20+ Root as Table,
21+ Body as TableBody,
22+ Caption as TableCaption,
23+ Cell as TableCell,
24+ Footer as TableFooter,
25+ Head as TableHead,
26+ Header as TableHeader,
27+ Row as TableRow,
28+};
A webui/src/lib/components/ui/table/table-body.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<tbody
14+ bind:this={ref}
15+ data-slot="table-body"
16+ class={cn("[&_tr:last-child]:border-0", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</tbody>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<tbody
14+ bind:this={ref}
15+ data-slot="table-body"
16+ class={cn("[&_tr:last-child]:border-0", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</tbody>
A webui/src/lib/components/ui/table/table-caption.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
11+</script>
12+
13+<caption
14+ bind:this={ref}
15+ data-slot="table-caption"
16+ class={cn("text-muted-foreground mt-4 text-sm", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</caption>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
11+</script>
12+
13+<caption
14+ bind:this={ref}
15+ data-slot="table-caption"
16+ class={cn("text-muted-foreground mt-4 text-sm", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</caption>
A webui/src/lib/components/ui/table/table-cell.svelte +23 -0
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLTdAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLTdAttributes> = $props();
11+</script>
12+
13+<td
14+ bind:this={ref}
15+ data-slot="table-cell"
16+ class={cn(
17+ "bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</td>
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLTdAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLTdAttributes> = $props();
11+</script>
12+
13+<td
14+ bind:this={ref}
15+ data-slot="table-cell"
16+ class={cn(
17+ "bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</td>
A webui/src/lib/components/ui/table/table-footer.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<tfoot
14+ bind:this={ref}
15+ data-slot="table-footer"
16+ class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</tfoot>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<tfoot
14+ bind:this={ref}
15+ data-slot="table-footer"
16+ class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</tfoot>
A webui/src/lib/components/ui/table/table-head.svelte +23 -0
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLThAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLThAttributes> = $props();
11+</script>
12+
13+<th
14+ bind:this={ref}
15+ data-slot="table-head"
16+ class={cn(
17+ "text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</th>
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLThAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLThAttributes> = $props();
11+</script>
12+
13+<th
14+ bind:this={ref}
15+ data-slot="table-head"
16+ class={cn(
17+ "text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</th>
A webui/src/lib/components/ui/table/table-header.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<thead
14+ bind:this={ref}
15+ data-slot="table-header"
16+ class={cn("[&_tr]:border-b", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</thead>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
11+</script>
12+
13+<thead
14+ bind:this={ref}
15+ data-slot="table-header"
16+ class={cn("[&_tr]:border-b", className)}
17+ {...restProps}
18+>
19+ {@render children?.()}
20+</thead>
A webui/src/lib/components/ui/table/table-row.svelte +23 -0
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
11+</script>
12+
13+<tr
14+ bind:this={ref}
15+ data-slot="table-row"
16+ class={cn(
17+ "hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</tr>
@@ -0,0 +1,23 @@
1+<script lang="ts">
2+ import { cn, type WithElementRef } from "$lib/utils.js";
3+ import type { HTMLAttributes } from "svelte/elements";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
11+</script>
12+
13+<tr
14+ bind:this={ref}
15+ data-slot="table-row"
16+ class={cn(
17+ "hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
18+ className
19+ )}
20+ {...restProps}
21+>
22+ {@render children?.()}
23+</tr>
A webui/src/lib/components/ui/table/table.svelte +22 -0
@@ -0,0 +1,22 @@
1+<script lang="ts">
2+ import type { HTMLTableAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLTableAttributes> = $props();
11+</script>
12+
13+<div data-slot="table-container" class="relative w-full overflow-x-auto">
14+ <table
15+ bind:this={ref}
16+ data-slot="table"
17+ class={cn("w-full caption-bottom text-sm", className)}
18+ {...restProps}
19+ >
20+ {@render children?.()}
21+ </table>
22+</div>
@@ -0,0 +1,22 @@
1+<script lang="ts">
2+ import type { HTMLTableAttributes } from "svelte/elements";
3+ import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ children,
9+ ...restProps
10+ }: WithElementRef<HTMLTableAttributes> = $props();
11+</script>
12+
13+<div data-slot="table-container" class="relative w-full overflow-x-auto">
14+ <table
15+ bind:this={ref}
16+ data-slot="table"
17+ class={cn("w-full caption-bottom text-sm", className)}
18+ {...restProps}
19+ >
20+ {@render children?.()}
21+ </table>
22+</div>
A webui/src/lib/components/ui/tabs/index.ts +16 -0
@@ -0,0 +1,16 @@
1+import Root from "./tabs.svelte";
2+import Content from "./tabs-content.svelte";
3+import List from "./tabs-list.svelte";
4+import Trigger from "./tabs-trigger.svelte";
5+
6+export {
7+ Root,
8+ Content,
9+ List,
10+ Trigger,
11+ //
12+ Root as Tabs,
13+ Content as TabsContent,
14+ List as TabsList,
15+ Trigger as TabsTrigger,
16+};
@@ -0,0 +1,16 @@
1+import Root from "./tabs.svelte";
2+import Content from "./tabs-content.svelte";
3+import List from "./tabs-list.svelte";
4+import Trigger from "./tabs-trigger.svelte";
5+
6+export {
7+ Root,
8+ Content,
9+ List,
10+ Trigger,
11+ //
12+ Root as Tabs,
13+ Content as TabsContent,
14+ List as TabsList,
15+ Trigger as TabsTrigger,
16+};
A webui/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
@@ -0,0 +1,17 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.ContentProps = $props();
10+</script>
11+
12+<TabsPrimitive.Content
13+ bind:ref
14+ data-slot="tabs-content"
15+ class={cn("flex-1 outline-none", className)}
16+ {...restProps}
17+/>
@@ -0,0 +1,17 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.ContentProps = $props();
10+</script>
11+
12+<TabsPrimitive.Content
13+ bind:ref
14+ data-slot="tabs-content"
15+ class={cn("flex-1 outline-none", className)}
16+ {...restProps}
17+/>
A webui/src/lib/components/ui/tabs/tabs-list.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.ListProps = $props();
10+</script>
11+
12+<TabsPrimitive.List
13+ bind:ref
14+ data-slot="tabs-list"
15+ class={cn(
16+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center border border-border p-[3px]",
17+ className
18+ )}
19+ {...restProps}
20+/>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.ListProps = $props();
10+</script>
11+
12+<TabsPrimitive.List
13+ bind:ref
14+ data-slot="tabs-list"
15+ class={cn(
16+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center border border-border p-[3px]",
17+ className
18+ )}
19+ {...restProps}
20+/>
A webui/src/lib/components/ui/tabs/tabs-trigger.svelte +20 -0
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.TriggerProps = $props();
10+</script>
11+
12+<TabsPrimitive.Trigger
13+ bind:ref
14+ data-slot="tabs-trigger"
15+ class={cn(
16+ "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17+ className
18+ )}
19+ {...restProps}
20+/>
@@ -0,0 +1,20 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ class: className,
8+ ...restProps
9+ }: TabsPrimitive.TriggerProps = $props();
10+</script>
11+
12+<TabsPrimitive.Trigger
13+ bind:ref
14+ data-slot="tabs-trigger"
15+ class={cn(
16+ "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17+ className
18+ )}
19+ {...restProps}
20+/>
A webui/src/lib/components/ui/tabs/tabs.svelte +19 -0
@@ -0,0 +1,19 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ value = $bindable(""),
8+ class: className,
9+ ...restProps
10+ }: TabsPrimitive.RootProps = $props();
11+</script>
12+
13+<TabsPrimitive.Root
14+ bind:ref
15+ bind:value
16+ data-slot="tabs"
17+ class={cn("flex flex-col gap-2", className)}
18+ {...restProps}
19+/>
@@ -0,0 +1,19 @@
1+<script lang="ts">
2+ import { Tabs as TabsPrimitive } from "bits-ui";
3+ import { cn } from "$lib/utils.js";
4+
5+ let {
6+ ref = $bindable(null),
7+ value = $bindable(""),
8+ class: className,
9+ ...restProps
10+ }: TabsPrimitive.RootProps = $props();
11+</script>
12+
13+<TabsPrimitive.Root
14+ bind:ref
15+ bind:value
16+ data-slot="tabs"
17+ class={cn("flex flex-col gap-2", className)}
18+ {...restProps}
19+/>
A webui/src/lib/utils.ts +13 -0
@@ -0,0 +1,13 @@
1+import { clsx, type ClassValue } from "clsx";
2+import { twMerge } from "tailwind-merge";
3+
4+export function cn(...inputs: ClassValue[]) {
5+ return twMerge(clsx(inputs));
6+}
7+
8+// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
10+// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
12+export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
13+export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
@@ -0,0 +1,13 @@
1+import { clsx, type ClassValue } from "clsx";
2+import { twMerge } from "tailwind-merge";
3+
4+export function cn(...inputs: ClassValue[]) {
5+ return twMerge(clsx(inputs));
6+}
7+
8+// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
10+// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
12+export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
13+export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close