Skip to content

Commit

Permalink
choicebox draft
Browse files Browse the repository at this point in the history
  • Loading branch information
shyakadavis committed Oct 5, 2024
1 parent 7b4f61e commit 25cfd69
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
"type": "module",
"dependencies": {
"bits-ui": "1.0.0-next.8",
"bits-ui": "1.0.0-next.9",
"clsx": "^2.1.1",
"runed": "^0.15.3",
"tailwind-merge": "^2.4.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions src/lib/components/ui/choicebox/choicebox-checkbox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
import { Icons } from '$lib/assets/icons';
import { cn } from '$lib/utils.js';
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte';
type Props = WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & {
title: string;
children?: Snippet;
'aria-labelledby'?: string;
description?: string;
};
let {
class: class_name = undefined,
checked = $bindable(false),
id = undefined,
title,
description,
'aria-labelledby': aria_labelledby = undefined,
children,
...rest
}: Props = $props();
</script>

<label
id={aria_labelledby}
for={id}
class={cn(
'inline-flex w-full cursor-pointer items-center justify-between gap-2 rounded-md border p-3 transition-colors',
"hover:border-gray-500 hover:bg-gray-100 hover:has-[button[aria-checked='true']]:bg-blue-200",
"has-[button[aria-checked='true']]:border-blue-600 has-[button[aria-checked='true']]:bg-blue-100",
// disabled
"has-[button[disabled='']]:cursor-not-allowed has-[button[disabled='']]:hover:border-gray-400 has-[button[disabled='']]:hover:bg-transparent"
)}
>
<CheckboxPrimitive.Root
{id}
class={cn(
'peer order-2 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',
// checked
'data-[state=checked]:border-blue-900 data-[state=checked]:bg-blue-900 data-[state=checked]:text-background-200',
// disabled
'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-[disabled=true]:bg-gray-100 data-[disabled=true]:text-gray-500',
// disabled & checked
'data-[state=checked]:data-[disabled=true]:border-gray-600 data-[state=checked]:data-[disabled=true]:bg-gray-600 data-[state=checked]:data-[disabled=true]:text-background-200',
class_name
)}
bind:checked
{...rest}
>
{#snippet children({ checked })}
<div class={cn('flex size-4 items-center justify-center p-0.5 text-current')}>
{#if checked === true}
<Icons.Check aria-hidden="true" class="size-4" />
{:else if checked === 'indeterminate'}
<Icons.Minus aria-hidden="true" class="size-4" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
<div
class="grid grow gap-2 text-sm transition-colors peer-disabled:text-gray-500 peer-aria-checked:text-blue-900 peer-disabled:[&_p]:text-gray-500 peer-aria-checked:[&_p]:text-blue-900"
>
<p class="font-medium">{title}</p>
{#if description}
<p class="text-gray-900">{description}</p>
{/if}
</div>
</label>
73 changes: 73 additions & 0 deletions src/lib/components/ui/choicebox/choicebox-radio.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RadioGroup as ChoiceboxRadioPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte';
type Props = WithoutChildrenOrChild<ChoiceboxRadioPrimitive.ItemProps> & {
title: string;
description?: string;
children?: Snippet;
};
let {
title,
description,
class: class_name = undefined,
value,
children,
...rest
}: Props = $props();
</script>

<!-- TODO: Move focus ring from the dot to the parent -->
<label
for={value}
class={cn(
'group flex w-full cursor-pointer items-center gap-2 rounded-md border p-3 transition-colors',
"hover:border-gray-500 hover:bg-gray-100 hover:has-[button[aria-checked='true']]:bg-blue-200",
"has-[button[aria-checked='true']]:border-blue-600 has-[button[aria-checked='true']]:bg-blue-100",
// disabled
"has-[button[disabled='']]:cursor-not-allowed has-[button[disabled='']]:hover:border-gray-400 has-[button[disabled='']]:hover:bg-transparent"
)}
>
<ChoiceboxRadioPrimitive.Item
id={value}
{value}
class={cn(
'peer order-2 aspect-square size-4 cursor-pointer rounded-full border border-gray-500 text-gray-1000 transition-[color,background-color] ease-in-out focus: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:text-gray-500 disabled:hover:bg-transparent aria-checked:border-blue-900',
'group-hover:bg-background-200',
class_name
)}
{...rest}
>
{#snippet children({ checked })}
<div class="flex items-center justify-center">
{#if checked === true}
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-2.5 fill-blue-900 text-blue-900"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
{/if}
</div>
{/snippet}
</ChoiceboxRadioPrimitive.Item>
<div
class="grid grow gap-2 text-sm transition-colors peer-disabled:text-gray-500 peer-aria-checked:text-blue-900 peer-disabled:[&_p]:text-gray-500 peer-aria-checked:[&_p]:text-blue-900"
>
<p class="font-medium">{title}</p>
{#if description}
<p class="text-gray-900">{description}</p>
{/if}
</div>
</label>

<!-- TODO: Add a custom component prop here. Only issue is, how to access `checked` outside of the designated snippet -->
57 changes: 57 additions & 0 deletions src/lib/components/ui/choicebox/choicebox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { RadioGroup as ChoiceboxGroupPrimitive } from 'bits-ui';
// TODO: This is poor implementation of the choicebox. Needs to be re-written.
type Props = ChoiceboxGroupPrimitive.RootProps & {
direction?: 'row' | 'column';
type: 'radio' | 'checkbox';
label?: string;
};
let {
class: class_name = undefined,
value = $bindable(undefined),
direction = 'column',
label = undefined,
type,
children,
...rest
}: Props = $props();
</script>

<div class="grid gap-2">
{#if label}
<span class="text-xs text-gray-700">{label}</span>
{/if}
{#if type === 'radio'}
<ChoiceboxGroupPrimitive.Root
bind:value
class={cn(
'flex gap-3',
{
'flex-col': direction === 'column',
'flex-row': direction === 'row'
},
class_name
)}
{...rest}
>
{@render children?.()}
</ChoiceboxGroupPrimitive.Root>
{:else if type === 'checkbox'}
<div
class={cn(
'flex gap-3',
{
'flex-col': direction === 'column',
'flex-row': direction === 'row'
},
class_name
)}
{...rest}
>
{@render children?.()}
</div>
{/if}
</div>
12 changes: 12 additions & 0 deletions src/lib/components/ui/choicebox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Checkbox from './choicebox-checkbox.svelte';
import Radio from './choicebox-radio.svelte';
import Root from './choicebox.svelte';

export {
Checkbox,
Checkbox as ChoiceboxCheckbox,
Root as ChoiceboxGroup,
Radio as ChoiceboxGroupRadio,
Radio,
Root
};
1 change: 0 additions & 1 deletion src/lib/components/ui/radio/radio-item.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
class_name
)}
{...rest}
on:click
>
{#snippet children({ checked })}
<div class="flex items-center justify-center">
Expand Down
2 changes: 1 addition & 1 deletion src/lib/config/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const aside_items: Aside = {
{
title: 'Choicebox',
href: '/choicebox',
status: 'soon'
status: 'draft'
},
{
title: 'Code Block',
Expand Down
27 changes: 26 additions & 1 deletion src/routes/choicebox/+page.svelte
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
<h1>choicebox</h1>
<script lang="ts">
import Demo from '$lib/components/shared/demo.svelte';
import PageWrapper from '$lib/components/shared/page-wrapper.svelte';
import Disabled from './disabled.svelte';
import disabled_code from './disabled.svelte?raw';
import MultiSelect from './multi-select.svelte';
import multi_select_code from './multi-select.svelte?raw';
import SingleSelect from './single-select.svelte';
import single_select_code from './single-select.svelte?raw';
let { data } = $props();
</script>

<PageWrapper title={data.title} description={data.description}>
<Demo id="single-select" class="space-y-2" code={single_select_code}>
<SingleSelect />
</Demo>

<Demo id="multi-select" class="space-y-2" code={multi_select_code}>
<MultiSelect />
</Demo>

<Demo id="disabled" class="space-y-2" code={disabled_code}>
<Disabled />
</Demo>
</PageWrapper>
22 changes: 22 additions & 0 deletions src/routes/choicebox/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { MetaTagsProps } from 'svelte-meta-tags';

export function load() {
const title = 'Choicebox';
const description =
'A larger form of Radio or Checkbox, where the user has a larger tap target and more details.';

const pageMetaTags = Object.freeze({
title,
description,
openGraph: {
title,
description
}
}) satisfies MetaTagsProps;

return {
pageMetaTags,
title,
description
};
}
26 changes: 26 additions & 0 deletions src/routes/choicebox/disabled.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import * as ChoiceboxGroup from '$lib/components/ui/choicebox';
</script>

<div class="grid gap-6">
<ChoiceboxGroup.Root
direction="row"
disabled
label="Choicebox group disabled"
type="radio"
value=""
>
<ChoiceboxGroup.Radio description="Free for two weeks" title="Pro Trial" value="trial_2" />
<ChoiceboxGroup.Radio description="Get started now" title="Pro" value="pro_2" />
</ChoiceboxGroup.Root>

<ChoiceboxGroup.Root direction="row" label="Single input disabled" type="checkbox">
<ChoiceboxGroup.Checkbox
description="Free for two weeks"
disabled
title="Pro Trial"
value="trial_2"
/>
<ChoiceboxGroup.Checkbox description="Get started now" title="Pro" value="pro_2" />
</ChoiceboxGroup.Root>
</div>
8 changes: 8 additions & 0 deletions src/routes/choicebox/multi-select.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import * as ChoiceboxGroup from '$lib/components/ui/choicebox';
</script>

<ChoiceboxGroup.Root direction="row" type="checkbox">
<ChoiceboxGroup.Checkbox description="Free for two weeks" title="Pro Trial" value="trial" />
<ChoiceboxGroup.Checkbox description="Get started now" title="Pro" value="pro" />
</ChoiceboxGroup.Root>
8 changes: 8 additions & 0 deletions src/routes/choicebox/single-select.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import * as ChoiceboxGroup from '$lib/components/ui/choicebox';
</script>

<ChoiceboxGroup.Root value="trial" direction="row" type="radio">
<ChoiceboxGroup.Radio description="Free for two weeks" title="Pro Trial" value="trial" />
<ChoiceboxGroup.Radio description="Get started now" title="Pro" value="pro" />
</ChoiceboxGroup.Root>

0 comments on commit 25cfd69

Please sign in to comment.