add ActionSelect component

This commit is contained in:
Elijah Duffy
2025-07-07 14:10:01 -07:00
parent b815ddf287
commit 2f2b4fcba2
6 changed files with 148 additions and 108 deletions

109
src/lib/ActionSelect.svelte Normal file
View File

@@ -0,0 +1,109 @@
<script lang="ts" module>
export type ButtonSelectOption = {
icon?:
| { component: Component; props: Record<string, any> }
| Snippet<[opt: ButtonSelectOption]>;
title: string | Snippet<[opt: ButtonSelectOption]>;
onchoose?: (opt: ButtonSelectOption) => void;
};
</script>
<script lang="ts">
import { Select } from 'melt/builders';
import type { Component, Snippet } from 'svelte';
import Label from './Label.svelte';
import Button from './Button.svelte';
import { CaretDown, Check } from 'phosphor-svelte';
import type { ClassValue } from 'svelte/elements';
interface Props {
label?: string;
placeholder?: string;
stateless?: boolean;
options: ButtonSelectOption[];
class?: ClassValue | null | undefined;
}
let {
label,
placeholder = 'Choose an action',
stateless = true,
options,
class: classValue
}: Props = $props();
const select = new Select<ButtonSelectOption>({
onValueChange: (value) => {
value?.onchoose?.(value);
}
});
</script>
<div>
{#if label}
<Label {...select.label}>{label}</Label>
{/if}
<Button class={['flex w-full items-center rounded-xl!', classValue]} {...select.trigger}>
{#if stateless || !select.value}
{placeholder}
{:else}
{@render icon(select.value)}
{@render title(select.value)}
{/if}
<CaretDown
weight="bold"
size="1.1em"
class={[select.open && '-scale-y-100', 'ml-auto transition-transform']}
/>
</Button>
<div class={['border-sui-accent space-y-1 rounded-xl border p-2 shadow-md']} {...select.content}>
{#each options as option}
<button
class={[
'flex w-full items-center gap-3 rounded px-5 py-2 text-base font-medium transition-colors',
stateless
? [select.highlighted?.title === option.title && 'bg-sui-text-100', 'cursor-pointer']
: [
select.highlighted?.title === option.title &&
'bg-sui-accent/80 text-sui-background',
select.value?.title === option.title && 'bg-sui-accent text-sui-background'
]
]}
{...select.getOption(option, typeof option.title === 'string' ? option.title : undefined)}
>
{@render icon(option)}
{@render title(option)}
{#if !stateless}
<Check
size="1.1em"
weight="bold"
class={[
select.value?.title === option.title ? 'opacity-100' : 'opacity-0',
'ml-auto transition-opacity'
]}
/>
{/if}
</button>
{/each}
</div>
</div>
{#snippet title(opt: ButtonSelectOption)}
{#if typeof opt.title === 'string'}
{opt.title}
{:else}
{@render opt.title(opt)}
{/if}
{/snippet}
{#snippet icon(opt: ButtonSelectOption)}
{#if opt.icon && typeof opt.icon === 'object'}
<opt.icon.component {...opt.icon.props} />
{:else if opt.icon}
{@render opt.icon(opt)}
{/if}
{/snippet}