From 7319c680c10ed6d0c6b9bff19bd85f43b41ebf66 Mon Sep 17 00:00:00 2001
From: SHYAKA Davis <87414827+shyakadavis@users.noreply.github.com>
Date: Wed, 17 Jul 2024 21:53:55 +0200
Subject: [PATCH] component: Checkbox (#9)

Co-authored-by: Davis SHYAKA <87414827+davis-shyaka@users.noreply.github.com>
---
 src/lib/assets/icons/index.ts                 |  2 +
 src/lib/assets/icons/minus.svg                |  3 ++
 .../components/ui/checkbox/checkbox.svelte    | 48 +++++++++++++++++++
 src/lib/components/ui/checkbox/index.ts       |  6 +++
 src/lib/components/ui/label/index.ts          |  7 +++
 src/lib/components/ui/label/label.svelte      | 21 ++++++++
 src/lib/config/sitemap.ts                     |  2 +-
 src/routes/checkbox/+page.svelte              | 27 ++++++++++-
 src/routes/checkbox/+page.ts                  | 21 ++++++++
 src/routes/checkbox/default.svelte            |  7 +++
 src/routes/checkbox/disabled.svelte           |  9 ++++
 src/routes/checkbox/indeterminate.svelte      |  5 ++
 12 files changed, 156 insertions(+), 2 deletions(-)
 create mode 100644 src/lib/assets/icons/minus.svg
 create mode 100644 src/lib/components/ui/checkbox/checkbox.svelte
 create mode 100644 src/lib/components/ui/checkbox/index.ts
 create mode 100644 src/lib/components/ui/label/index.ts
 create mode 100644 src/lib/components/ui/label/label.svelte
 create mode 100644 src/routes/checkbox/+page.ts
 create mode 100644 src/routes/checkbox/default.svelte
 create mode 100644 src/routes/checkbox/disabled.svelte
 create mode 100644 src/routes/checkbox/indeterminate.svelte

diff --git a/src/lib/assets/icons/index.ts b/src/lib/assets/icons/index.ts
index 13c209d..9600c25 100644
--- a/src/lib/assets/icons/index.ts
+++ b/src/lib/assets/icons/index.ts
@@ -34,6 +34,7 @@ import LogoNext from './logo-next.svg?component';
 import LogoTurborepo from './logo-turborepo.svg?component';
 import LogoV0 from './logo-v0.svg?component';
 import LogoVercel from './logo-vercel.svg?component';
+import Minus from './minus.svg?component';
 import Notification from './notification.svg?component';
 import Paperclip from './paperclip.svg?component';
 import PencilEdit from './pencil-edit.svg?component';
@@ -82,6 +83,7 @@ export const Icons = {
 	LogoTurborepo,
 	LogoV0,
 	LogoVercel,
+	Minus,
 	Notification,
 	Paperclip,
 	PencilEdit,
diff --git a/src/lib/assets/icons/minus.svg b/src/lib/assets/icons/minus.svg
new file mode 100644
index 0000000..924aaff
--- /dev/null
+++ b/src/lib/assets/icons/minus.svg
@@ -0,0 +1,3 @@
+<svg stroke-linejoin="round" color="currentColor" viewBox="0 0 16 16">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M2 7.25H2.75H13.25H14V8.75H13.25H2.75H2V7.25Z" fill="currentColor"></path>
+</svg>
\ No newline at end of file
diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte
new file mode 100644
index 0000000..8d69a96
--- /dev/null
+++ b/src/lib/components/ui/checkbox/checkbox.svelte
@@ -0,0 +1,48 @@
+<script lang="ts">
+	import { Icons } from '$lib/assets/icons';
+	import { cn } from '$lib/utils.js';
+	import { Checkbox as CheckboxPrimitive } from 'bits-ui';
+	import { Label } from '../label';
+
+	type $$Props = CheckboxPrimitive.Props;
+	type $$Events = CheckboxPrimitive.Events;
+
+	let className: $$Props['class'] = undefined;
+	export let checked: $$Props['checked'] = false;
+	export let id: $$Props['id'] = undefined;
+	export let aria_labelledby: $$Props['aria-labelledby'] = undefined;
+	export { className as class };
+</script>
+
+<!-- 
+TODO: How to properly document these styles? 
+Here are considerations: there are base classes for the likes such as focus, but the ugly ones are the data-[state=checked] and data-[disabled=true] classes.
+Some are for individual states, e.g disabled, checked, while others are combinations of states, e.g checked and disabled.
+  -->
+<div class="inline-flex items-center gap-2">
+	<CheckboxPrimitive.Root
+		class={cn(
+			'peer box-content size-4 shrink-0 items-center gap-2 rounded-[4px] border border-gray-700 bg-background-100 ring-offset-background-200 transition-[border-color,background,box-shadow] delay-0 duration-200 ease-in-out hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-color focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:border-gray-500 disabled:bg-gray-100 disabled:text-gray-500 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:border-gray-500 data-[state=checked]:border-gray-1000 data-[state=checked]:data-[disabled=true]:border-gray-600 data-[disabled=true]:bg-gray-100 data-[state=checked]:bg-gray-1000 data-[state=checked]:data-[disabled=true]:bg-gray-600 data-[disabled=true]:text-gray-500 data-[state=checked]:data-[disabled=true]:text-background-200 data-[state=checked]:text-background-200',
+			className
+		)}
+		{id}
+		bind:checked
+		{...$$restProps}
+		on:click
+	>
+		<CheckboxPrimitive.Indicator
+			class={cn('flex size-4 items-center justify-center p-0.5 text-current')}
+			let:isChecked
+			let:isIndeterminate
+		>
+			{#if isChecked}
+				<Icons.Check aria-hidden="true" class="size-4" />
+			{:else if isIndeterminate}
+				<Icons.Minus aria-hidden="true" class="size-4" />
+			{/if}
+		</CheckboxPrimitive.Indicator>
+	</CheckboxPrimitive.Root>
+	<Label id={aria_labelledby} for={id}>
+		<slot></slot>
+	</Label>
+</div>
diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts
new file mode 100644
index 0000000..5c27671
--- /dev/null
+++ b/src/lib/components/ui/checkbox/index.ts
@@ -0,0 +1,6 @@
+import Root from './checkbox.svelte';
+export {
+	Root,
+	//
+	Root as Checkbox
+};
diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts
new file mode 100644
index 0000000..808d141
--- /dev/null
+++ b/src/lib/components/ui/label/index.ts
@@ -0,0 +1,7 @@
+import Root from './label.svelte';
+
+export {
+	Root,
+	//
+	Root as Label
+};
diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte
new file mode 100644
index 0000000..1916bde
--- /dev/null
+++ b/src/lib/components/ui/label/label.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+	import { cn } from '$lib/utils.js';
+	import { Label as LabelPrimitive } from 'bits-ui';
+
+	type $$Props = LabelPrimitive.Props;
+	type $$Events = LabelPrimitive.Events;
+
+	let className: $$Props['class'] = undefined;
+	export { className as class };
+</script>
+
+<LabelPrimitive.Root
+	class={cn(
+		'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:text-gray-600',
+		className
+	)}
+	{...$$restProps}
+	on:mousedown
+>
+	<slot />
+</LabelPrimitive.Root>
diff --git a/src/lib/config/sitemap.ts b/src/lib/config/sitemap.ts
index 615d8e2..143ef51 100644
--- a/src/lib/config/sitemap.ts
+++ b/src/lib/config/sitemap.ts
@@ -80,7 +80,7 @@ export const aside_items: Aside = {
 		{
 			title: 'Checkbox',
 			href: '/checkbox',
-			status: 'soon'
+			status: 'draft'
 		},
 		{
 			title: 'Choicebox',
diff --git a/src/routes/checkbox/+page.svelte b/src/routes/checkbox/+page.svelte
index 1769534..4338fe8 100644
--- a/src/routes/checkbox/+page.svelte
+++ b/src/routes/checkbox/+page.svelte
@@ -1 +1,26 @@
-<h1>checkbox</h1>
+<script lang="ts">
+	import Demo from '$lib/components/shared/demo.svelte';
+	import PageWrapper from '$lib/components/shared/page-wrapper.svelte';
+	import Default from './default.svelte';
+	import default_code from './default.svelte?raw';
+	import Disabled from './disabled.svelte';
+	import disabled_code from './disabled.svelte?raw';
+	import Indeterminate from './indeterminate.svelte';
+	import indeterminate_code from './indeterminate.svelte?raw';
+
+	export let data;
+</script>
+
+<PageWrapper title={data.title} description={data.description}>
+	<Demo id="default" class="space-y-2" code={default_code}>
+		<Default />
+	</Demo>
+
+	<Demo id="disabled" class="space-y-2" code={disabled_code}>
+		<Disabled />
+	</Demo>
+
+	<Demo id="indeterminate" class="space-y-2" code={indeterminate_code}>
+		<Indeterminate />
+	</Demo>
+</PageWrapper>
diff --git a/src/routes/checkbox/+page.ts b/src/routes/checkbox/+page.ts
new file mode 100644
index 0000000..c4f8995
--- /dev/null
+++ b/src/routes/checkbox/+page.ts
@@ -0,0 +1,21 @@
+import type { MetaTagsProps } from 'svelte-meta-tags';
+
+export function load() {
+	const title = 'Checkbox';
+	const description = 'A control that toggles between two options, checked or unchecked.';
+
+	const pageMetaTags = Object.freeze({
+		title,
+		description,
+		openGraph: {
+			title,
+			description
+		}
+	}) satisfies MetaTagsProps;
+
+	return {
+		pageMetaTags,
+		title,
+		description
+	};
+}
diff --git a/src/routes/checkbox/default.svelte b/src/routes/checkbox/default.svelte
new file mode 100644
index 0000000..8db8a0a
--- /dev/null
+++ b/src/routes/checkbox/default.svelte
@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Checkbox } from '$lib/components/ui/checkbox';
+
+	let checked = false;
+</script>
+
+<Checkbox id="option-1" bind:checked aria-labelledby="options-label">Option 1</Checkbox>
diff --git a/src/routes/checkbox/disabled.svelte b/src/routes/checkbox/disabled.svelte
new file mode 100644
index 0000000..5cc7225
--- /dev/null
+++ b/src/routes/checkbox/disabled.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+	import { Checkbox } from '$lib/components/ui/checkbox';
+</script>
+
+<div class="flex flex-col gap-4">
+	<Checkbox disabled>Disabled</Checkbox>
+	<Checkbox checked disabled>Disabled Checked</Checkbox>
+	<Checkbox disabled checked="indeterminate">Disabled Indeterminate</Checkbox>
+</div>
diff --git a/src/routes/checkbox/indeterminate.svelte b/src/routes/checkbox/indeterminate.svelte
new file mode 100644
index 0000000..2006724
--- /dev/null
+++ b/src/routes/checkbox/indeterminate.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+	import { Checkbox } from '$lib/components/ui/checkbox';
+</script>
+
+<Checkbox id="option-2" checked="indeterminate" aria-labelledby="options-label">Option 2</Checkbox>