69 lines
1.6 KiB
Svelte
69 lines
1.6 KiB
Svelte
<script lang="ts">
|
|
// 1-5 star rating. Read-only by default; with `name` it renders radio buttons for a form.
|
|
import Star from 'lucide-svelte/icons/star';
|
|
|
|
let {
|
|
value,
|
|
size = 14,
|
|
name,
|
|
onchange
|
|
}: {
|
|
value: number | null;
|
|
size?: number;
|
|
name?: string;
|
|
onchange?: (value: number | null) => void;
|
|
} = $props();
|
|
|
|
let hover = $state<number | null>(null);
|
|
const shown = $derived(hover ?? value ?? 0);
|
|
</script>
|
|
|
|
{#if name || onchange}
|
|
<span class="stars interactive" role="radiogroup" aria-label="Rating">
|
|
{#each [1, 2, 3, 4, 5] as n (n)}
|
|
<button
|
|
type={name ? 'submit' : 'button'}
|
|
{name}
|
|
value={value === n ? '' : String(n)}
|
|
role="radio"
|
|
aria-checked={value === n}
|
|
aria-label={`${n} star${n > 1 ? 's' : ''}${value === n ? ' (click to clear)' : ''}`}
|
|
onpointerenter={() => (hover = n)}
|
|
onpointerleave={() => (hover = null)}
|
|
onclick={() => onchange?.(value === n ? null : n)}
|
|
>
|
|
<Star {size} class={n <= shown ? 'on' : 'off'} />
|
|
</button>
|
|
{/each}
|
|
</span>
|
|
{:else if value}
|
|
<span class="stars" aria-label={`${value} out of 5 stars`} title={`${value}/5`}>
|
|
{#each [1, 2, 3, 4, 5] as n (n)}
|
|
<Star {size} class={n <= value ? 'on' : 'off'} aria-hidden="true" />
|
|
{/each}
|
|
</span>
|
|
{/if}
|
|
|
|
<style>
|
|
.stars {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 1px;
|
|
}
|
|
.stars :global(.on) {
|
|
fill: #f5b301;
|
|
color: #f5b301;
|
|
}
|
|
.stars :global(.off) {
|
|
color: color-mix(in srgb, var(--site-muted) 50%, transparent);
|
|
}
|
|
.interactive button {
|
|
padding: 2px;
|
|
border-radius: 4px;
|
|
line-height: 0;
|
|
}
|
|
.interactive button:hover {
|
|
transform: scale(1.12);
|
|
}
|
|
</style>
|