mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-04 05:29:06 +00:00
Initial commit
This commit is contained in:
93
lib/components/ui/toggle-pill/toggle-group.svelte
Normal file
93
lib/components/ui/toggle-pill/toggle-group.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* ToggleGroup - A pill-style toggle for switching between multiple values (2+).
|
||||
* Extends ToggleSwitch to support any number of options.
|
||||
* Perfect for settings like scanner selection: "None / Trivy / Grype / Both"
|
||||
*
|
||||
* Keyboard navigation:
|
||||
* - Tab: Focus the selected option (or first if none selected)
|
||||
* - Arrow Left/Right: Navigate between options
|
||||
* - Space/Enter: Select the focused option
|
||||
*/
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label?: string;
|
||||
icon?: Component;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
options: Option[];
|
||||
disabled?: boolean;
|
||||
onchange?: (value: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(),
|
||||
options,
|
||||
disabled = false,
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
let buttons = $state<HTMLButtonElement[]>([]);
|
||||
|
||||
function select(optionValue: string) {
|
||||
if (disabled || value === optionValue) return;
|
||||
value = optionValue;
|
||||
onchange?.(value);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
let nextIndex = index;
|
||||
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
nextIndex = (index + 1) % options.length;
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
nextIndex = (index - 1 + options.length) % options.length;
|
||||
} else if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
select(options[index].value);
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move focus and select the new option
|
||||
buttons[nextIndex]?.focus();
|
||||
select(options[nextIndex].value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="inline-flex items-center rounded-md bg-muted p-0.5 text-xs font-medium {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
|
||||
role="radiogroup"
|
||||
>
|
||||
{#each options as option, i}
|
||||
{@const isSelected = value === option.value}
|
||||
{@const displayLabel = option.label ?? option.value}
|
||||
<button
|
||||
bind:this={buttons[i]}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={isSelected}
|
||||
tabindex={isSelected ? 0 : -1}
|
||||
class="px-2 py-1 rounded transition-colors flex items-center gap-1.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 {isSelected
|
||||
? 'bg-slate-300 text-slate-900 shadow-sm dark:bg-amber-900/50 dark:text-amber-100'
|
||||
: 'text-muted-foreground hover:text-foreground'} {disabled ? 'cursor-not-allowed' : ''}"
|
||||
onclick={() => select(option.value)}
|
||||
onkeydown={(e) => handleKeydown(e, i)}
|
||||
{disabled}
|
||||
>
|
||||
{#if option.icon}
|
||||
<svelte:component this={option.icon} class="w-3 h-3" />
|
||||
{/if}
|
||||
{displayLabel}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
Reference in New Issue
Block a user