jedmund-svelte/src/lib/components/admin/Select.svelte
Justin Edmund c67dbeaf38 fix(admin): make filters reactive in Svelte 5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 07:30:01 -07:00

226 lines
4.1 KiB
Svelte

<script lang="ts">
import type { HTMLSelectAttributes } from 'svelte/elements'
import ChevronDownIcon from '$icons/chevron-down.svg?raw'
interface Option {
value: string
label: string
}
interface Props extends Omit<HTMLSelectAttributes, 'size'> {
options: Option[]
value?: string
size?: 'small' | 'medium' | 'large'
variant?: 'default' | 'minimal'
fullWidth?: boolean
pill?: boolean
onchange?: (event: Event) => void
oninput?: (event: Event) => void
onfocus?: (event: FocusEvent) => void
onblur?: (event: FocusEvent) => void
}
let {
options,
value = $bindable(),
size = 'medium',
variant = 'default',
fullWidth = false,
pill = true,
onchange,
oninput,
onfocus,
onblur,
class: className = '',
...restProps
}: Props = $props()
</script>
<div class="select-wrapper">
<select
bind:value
class="select select-{size} select-{variant} {className}"
class:select-full-width={fullWidth}
class:select-pill={pill}
onchange={(e) => onchange?.(e)}
oninput={(e) => oninput?.(e)}
onfocus={(e) => onfocus?.(e)}
onblur={(e) => onblur?.(e)}
{...restProps}
>
{#each options as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<div class="select-icon">
{@html ChevronDownIcon}
</div>
</div>
<style lang="scss">
.select-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.select {
box-sizing: border-box;
color: $gray-20;
cursor: pointer;
font-family: inherit;
transition: all 0.2s ease;
appearance: none;
padding-right: 36px;
width: 100%;
&:focus {
outline: none;
}
&:disabled {
color: $gray-60;
cursor: not-allowed;
opacity: 0.6;
}
// Default variant
&.select-default {
border: 1px solid $gray-80;
background: white;
font-weight: 500;
&:focus {
border-color: $blue-50;
box-shadow: 0 0 0 3px rgba(20, 130, 193, 0.1);
}
&:disabled {
background: $gray-90;
}
}
// Minimal variant
&.select-minimal {
border: none;
background: $gray-90;
font-weight: 500;
&:hover {
background: $gray-80;
}
&:focus {
background: $gray-80;
}
&:disabled {
background: transparent;
}
}
// Size variants for default variant (accounting for border)
&.select-default {
&.select-small {
padding: calc($unit - 1px) calc($unit * 1.5);
font-size: 13px;
min-height: 28px;
min-width: 120px;
}
&.select-medium {
padding: calc($unit - 1px) $unit-2x;
font-size: 14px;
min-height: 36px;
min-width: 160px;
}
&.select-large {
padding: calc($unit * 1.5 - 1px) $unit-3x;
font-size: 15px;
min-height: 44px;
min-width: 180px;
}
}
// Size variants for minimal variant (no border, card-sized border radius)
&.select-minimal {
&.select-small {
padding: $unit calc($unit * 1.5);
font-size: 13px;
min-height: 28px;
min-width: 120px;
border-radius: $corner-radius;
}
&.select-medium {
padding: $unit $unit-2x;
font-size: 14px;
min-height: 36px;
min-width: 160px;
border-radius: $corner-radius;
}
&.select-large {
padding: calc($unit * 1.5) $unit-3x;
font-size: 15px;
min-height: 44px;
min-width: 180px;
border-radius: $card-corner-radius;
}
}
// Shape variants for default variant only (minimal already has card radius)
&.select-default.select-pill {
&.select-small {
border-radius: 20px;
}
&.select-medium {
border-radius: 24px;
}
&.select-large {
border-radius: 28px;
}
}
&.select-default:not(.select-pill) {
&.select-small {
border-radius: 6px;
}
&.select-medium {
border-radius: 8px;
}
&.select-large {
border-radius: 10px;
}
}
// Width variants
&.select-full-width {
width: 100%;
min-width: auto;
}
}
.select-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
color: $gray-60;
display: flex;
align-items: center;
justify-content: center;
:global(svg) {
width: 16px;
height: 16px;
}
}
// Full width handling for wrapper
.select-wrapper:has(.select-full-width) {
width: 100%;
}
</style>