feat: add billing and organization workflows
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { tick } from 'svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Command from '$lib/components/ui/command/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const emptyValue = '__form-combobox-empty__';
|
||||
|
||||
let {
|
||||
name,
|
||||
value = $bindable(''),
|
||||
options,
|
||||
placeholder = 'Select an option',
|
||||
searchPlaceholder = 'Search...',
|
||||
emptyMessage = 'No results found.',
|
||||
triggerProps,
|
||||
onValueChange
|
||||
}: {
|
||||
name: string;
|
||||
value: string;
|
||||
options: readonly Option[];
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
emptyMessage?: string;
|
||||
triggerProps?: ComponentProps<typeof Button>;
|
||||
onValueChange?: (value: string) => void;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||
const selectedLabel = $derived(
|
||||
options.find((option) => option.value === value)?.label ?? placeholder
|
||||
);
|
||||
|
||||
function closeAndFocusTrigger() {
|
||||
open = false;
|
||||
tick().then(() => triggerRef.focus());
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger bind:ref={triggerRef}>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
{...triggerProps}
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
class={cn(
|
||||
'w-full justify-between font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
triggerProps?.class
|
||||
)}
|
||||
>
|
||||
{selectedLabel}
|
||||
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-[var(--bits-popover-anchor-width)] p-0" align="start">
|
||||
<Command.Root>
|
||||
<Command.Input placeholder={searchPlaceholder} />
|
||||
<Command.List>
|
||||
<Command.Empty>{emptyMessage}</Command.Empty>
|
||||
<Command.Group>
|
||||
{#each options as option (option.value)}
|
||||
<Command.Item
|
||||
value={option.value || emptyValue}
|
||||
keywords={[option.label]}
|
||||
data-checked={value === option.value}
|
||||
onSelect={() => {
|
||||
value = option.value;
|
||||
onValueChange?.(option.value);
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
<input type="hidden" {name} {value} />
|
||||
Reference in New Issue
Block a user