alpha
Login
or
Join now
alice.pds.demo.boltless.dev
/
core
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
2
Pulls
1
Pipelines
web: add Toggle component
author
eti
date
2 weeks ago
(Jul 28, 2026, 7:06 PM +0200)
commit
de1180ee
de1180ee0e8e256c498041934d91229d96ac14b8
parent
504c7b1c
504c7b1c9587a6f112bb2e6348291af3adb967ce
+101
2 changed files
Expand all
Collapse all
Unified
Split
web
src
lib
components
ui
Toggle.stories.svelte
Toggle.svelte
+44
web/src/lib/components/ui/Toggle.stories.svelte
View file
Reviewed
···
1
1
+
<script module lang="ts">
2
2
+
import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
+
import Toggle from "./Toggle.svelte";
4
4
+
5
5
+
const { Story } = defineMeta({
6
6
+
title: "UI/Toggle",
7
7
+
component: Toggle,
8
8
+
tags: ["autodocs"],
9
9
+
argTypes: {
10
10
+
checked: { control: "boolean" },
11
11
+
disabled: { control: "boolean" },
12
12
+
layout: { control: "inline-radio", options: ["inline", "block"] }
13
13
+
},
14
14
+
args: {
15
15
+
checked: false,
16
16
+
disabled: false,
17
17
+
layout: "inline"
18
18
+
}
19
19
+
});
20
20
+
</script>
21
21
+
22
22
+
<!-- Toggle content comes through its `children` snippet, provided here as Story slot content. -->
23
23
+
<Story name="Default">Label</Story>
24
24
+
<Story name="Checked" args={{ checked: true }}>Label</Story>
25
25
+
<Story name="Disabled" args={{ disabled: true }}>Label</Story>
26
26
+
<Story name="Disabled checked" args={{ disabled: true, checked: true }}>Label</Story>
27
27
+
28
28
+
<Story name="Without label" />
29
29
+
30
30
+
<Story name="States">
31
31
+
<div class="flex flex-col gap-3">
32
32
+
<Toggle>Unchecked</Toggle>
33
33
+
<Toggle checked>Checked</Toggle>
34
34
+
<Toggle disabled>Disabled</Toggle>
35
35
+
<Toggle disabled checked>Disabled checked</Toggle>
36
36
+
</div>
37
37
+
</Story>
38
38
+
39
39
+
<Story name="Block layout">
40
40
+
<div class="flex w-64 flex-col gap-3">
41
41
+
<Toggle layout="block">Label pushed to the far edge</Toggle>
42
42
+
<Toggle layout="block" checked>Another one, wrapping when the text runs long</Toggle>
43
43
+
</div>
44
44
+
</Story>
+57
web/src/lib/components/ui/Toggle.svelte
View file
Reviewed
···
1
1
+
<script lang="ts">
2
2
+
import type { Snippet } from "svelte";
3
3
+
import type { HTMLInputAttributes } from "svelte/elements";
4
4
+
5
5
+
interface Props extends Omit<HTMLInputAttributes, "type" | "class" | "checked" | "children"> {
6
6
+
checked?: boolean;
7
7
+
disabled?: boolean;
8
8
+
/** `inline` hugs the label, `block` stretches it to fill the row. */
9
9
+
layout?: "inline" | "block";
10
10
+
class?: string;
11
11
+
children?: Snippet;
12
12
+
}
13
13
+
14
14
+
let {
15
15
+
checked = $bindable(false),
16
16
+
disabled = false,
17
17
+
layout = "inline",
18
18
+
class: className,
19
19
+
children,
20
20
+
...rest
21
21
+
}: Props = $props();
22
22
+
23
23
+
const labelClasses = $derived(
24
24
+
[
25
25
+
"items-center gap-2",
26
26
+
layout === "block" ? "flex w-full" : "inline-flex",
27
27
+
disabled ? "cursor-not-allowed text-foreground-disabled" : "cursor-pointer",
28
28
+
className ?? ""
29
29
+
].join(" ")
30
30
+
);
31
31
+
32
32
+
const inputClasses = [
33
33
+
"peer absolute inset-0 size-full appearance-none rounded-full bg-background-muted",
34
34
+
"transition-colors duration-150",
35
35
+
"checked:bg-foreground-default",
36
36
+
"enabled:hover:bg-background-inset checked:enabled:hover:bg-foreground-muted",
37
37
+
"focus-visible:ring-2 focus-visible:ring-border-strong focus-visible:outline-none",
38
38
+
"disabled:cursor-not-allowed disabled:bg-background-inset checked:disabled:bg-foreground-disabled"
39
39
+
].join(" ");
40
40
+
41
41
+
const thumbClasses = [
42
42
+
"pointer-events-none absolute top-px left-px size-4 rounded-full bg-foreground-on-emphasis",
43
43
+
"transition-transform duration-150 peer-checked:translate-x-[15px]"
44
44
+
].join(" ");
45
45
+
46
46
+
const textClasses = $derived(layout === "block" ? "min-w-0 flex-1" : "whitespace-nowrap");
47
47
+
</script>
48
48
+
49
49
+
<label class={labelClasses}>
50
50
+
<span class="relative inline-flex h-[18px] w-[33px] shrink-0">
51
51
+
<input type="checkbox" role="switch" bind:checked {disabled} class={inputClasses} {...rest} />
52
52
+
<span class={thumbClasses} aria-hidden="true"></span>
53
53
+
</span>
54
54
+
{#if children}
55
55
+
<span class={textClasses}>{@render children()}</span>
56
56
+
{/if}
57
57
+
</label>