1 | <script lang="ts"> |
2 | import type { HTMLInputAttributes } from 'svelte/elements'; |
3 | import type { Snippet } from 'svelte'; |
4 | import { circInOut } from 'svelte/easing'; |
5 | import { draw } from 'svelte/transition'; |
6 | import { twMerge } from 'tailwind-merge'; |
7 |
|
8 | type Props = HTMLInputAttributes & { |
9 | children?: Snippet; |
10 | class?: string; |
11 | disabled?: boolean; |
12 | label?: string; |
13 | }; |
14 |
|
15 | let { |
16 | checked = $bindable(false), |
17 | children, |
18 | class: className, |
19 | disabled = false, |
20 | label = '', |
21 | ...props |
22 | }: Props = $props(); |
23 | </script> |
24 |
|
25 | <div> |
26 | |
27 | <input class="hidden appearance-none" type="checkbox" bind:checked {disabled} {...props} /> |
28 |
|
29 | |
30 | <label class={twMerge('flex items-center w-fit text-fg cursor-pointer', disabled && 'text-disabled-fg')}> |
31 | |
32 | <button |
33 | class={twMerge( |
34 | 'place-content-center grid mr-2 min-w-6 min-h-6', // layout and positioning |
35 | 'outline-primary outline-offset-2 hover:outline focus:outline', // outline |
36 | 'cursor-pointer rounded border', // visual |
37 | checked && 'bg-primary', |
38 | disabled && 'bg-bg cursor-not-allowed outline-none' |
39 | )} |
40 | onclick={() => (checked = !checked)} |
41 | {disabled} |
42 | > |
43 | <div class="pointer-events-none"> |
44 | {#if checked} |
45 | |
46 | <span class={twMerge(disabled ? 'text-disabled-fg' : 'text-primary-fg')}> |
47 | {@render CheckMark()} |
48 | </span> |
49 | {:else} |
50 | |
51 | {@render CrossMark()} |
52 | {/if} |
53 | </div> |
54 | </button> |
55 |
|
56 | |
57 | {label} |
58 | </label> |
59 |
|
60 | {#if children} |
61 | |
62 | <div class={twMerge('ml-8 text-muted', disabled && 'text-disabled-fg')}> |
63 | {@render children?.()} |
64 | </div> |
65 | {/if} |
66 | </div> |
67 |
|
68 |
|
69 | {#snippet CheckMark()} |
70 | <svg class="size-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 32 31" fill="none"> |
71 | <path |
72 | in:draw={{ duration: 150, easing: circInOut }} |
73 | stroke-width="5" |
74 | d="M1 16L8 23L30.5 0.5" |
75 | stroke="currentColor" |
76 | /> |
77 | </svg> |
78 | {/snippet} |
79 |
|
80 |
|
81 | {#snippet CrossMark()} |
82 | <svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 25" fill="none"> |
83 | <path in:draw={{ duration: 100, easing: circInOut }} stroke-width="4" d="M0.5 1L23.5 24" stroke="currentColor" /> |
84 | <path |
85 | in:draw={{ delay: 100, duration: 100, easing: circInOut }} |
86 | stroke-width="4" |
87 | d="M23.5 1L0.5 24" |
88 | stroke="currentColor" |
89 | /> |
90 | </svg> |
91 | {/snippet} |
92 |
|