Files
website/src/lib/books/Stars.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>