refactor(workshop): use the shared select component for theme filters

The sort, announcement audience and report category controls were still native select elements. They now use the same shadcn-svelte Select as the moderation filters for consistent styling and keyboard behavior.
This commit is contained in:
HorizonCode committed 2026-11-03 19:18:31 +01:00
1 parent 918f8651dd
commit d555ac366f
1 file changed
+65 -24
@@ -202,6 +202,13 @@
{ id: 'broken', label: 'Broken / unusable' }, { id: 'broken', label: 'Broken / unusable' },
{ id: 'other', label: 'Other' }, { id: 'other', label: 'Other' },
]; ];
const SORT_OPTIONS: Array<{ id: WorkshopSort; label: string }> = [
{ id: 'relevance', label: 'Best match' },
{ id: 'popular', label: 'Most Popular' },
{ id: 'recent', label: 'Most Recent' },
{ id: 'likes', label: 'Most Liked' },
{ id: 'downloads', label: 'Most Downloaded' },
];
const REPORT_STATUS_OPTIONS: Array<{ id: ThemeReportStatus | 'all'; label: string }> = [ const REPORT_STATUS_OPTIONS: Array<{ id: ThemeReportStatus | 'all'; label: string }> = [
{ id: 'open', label: 'Open reports' }, { id: 'open', label: 'Open reports' },
{ id: 'resolved', label: 'Resolved reports' }, { id: 'resolved', label: 'Resolved reports' },
@@ -1492,17 +1499,26 @@
<!-- Controls Right: Sort, Search, Moderation, Trash & Refresh --> <!-- Controls Right: Sort, Search, Moderation, Trash & Refresh -->
<div class="flex shrink-0 items-center gap-2"> <div class="flex shrink-0 items-center gap-2">
<select <Select.Root
class="h-7.5 rounded-lg border border-theme-800 bg-theme-950/80 px-2.5 text-[11px] text-muted-foreground outline-none transition-colors hover:text-white focus:border-primary/50" type="single"
value={selectedSort} value={selectedSort}
onchange={(e) => handleSortChange(parseWorkshopSort(e.currentTarget.value))} onValueChange={(value) => {
if (!value) return;
handleSortChange(parseWorkshopSort(value));
}}
> >
<option value="relevance">Best match</option> <Select.Trigger
<option value="popular">Most Popular</option> class="h-7.5 w-36 border-theme-800 bg-theme-950/80 px-2.5 text-[11px] text-muted-foreground transition-colors hover:text-white"
<option value="recent">Most Recent</option> aria-label="Sort themes"
<option value="likes">Most Liked</option> >
<option value="downloads">Most Downloaded</option> {SORT_OPTIONS.find((option) => option.id === selectedSort)?.label ?? 'Best match'}
</select> </Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
{#each SORT_OPTIONS as option (option.id)}
<Select.Item value={option.id} label={option.label} />
{/each}
</Select.Content>
</Select.Root>
<div class="relative w-48"> <div class="relative w-48">
<Search <Search
@@ -2888,18 +2904,30 @@
{announcementMessage.trim().length}/1000 · minimum 10 {announcementMessage.trim().length}/1000 · minimum 10
</span> </span>
</label> </label>
<label class="block space-y-1.5"> <div class="block space-y-1.5">
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase" <span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
>Audience</span >Audience</span
> >
<select <Select.Root
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50" type="single"
bind:value={announcementAudience} value={announcementAudience}
onValueChange={(value) => {
if (!value) return;
announcementAudience = value as 'all' | 'staff';
}}
> >
<option value="all">Everyone</option> <Select.Trigger
<option value="staff">Staff only</option> class="h-8 border-theme-800 bg-theme-900/60 text-[11px] text-white"
</select> aria-label="Announcement audience"
</label> >
{announcementAudience === 'staff' ? 'Staff only' : 'Everyone'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
<Select.Item value="all" label="Everyone" />
<Select.Item value="staff" label="Staff only" />
</Select.Content>
</Select.Root>
</div>
{#if announcementError} {#if announcementError}
<p <p
class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300" class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300"
@@ -3313,19 +3341,32 @@
</WorkshopTooltip> </WorkshopTooltip>
</div> </div>
<div class="space-y-3 p-4"> <div class="space-y-3 p-4">
<label class="block space-y-1.5"> <div class="block space-y-1.5">
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase" <span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
>Category</span >Category</span
> >
<select <Select.Root
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50" type="single"
bind:value={reportCategory} value={reportCategory}
onValueChange={(value) => {
if (!value) return;
reportCategory = value as ThemeReportCategory;
}}
> >
<Select.Trigger
class="h-8 border-theme-800 bg-theme-900/60 text-[11px] text-white"
aria-label="Report category"
>
{REPORT_CATEGORIES.find((category) => category.id === reportCategory)?.label ??
'Other'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
{#each REPORT_CATEGORIES as category (category.id)} {#each REPORT_CATEGORIES as category (category.id)}
<option value={category.id}>{category.label}</option> <Select.Item value={category.id} label={category.label} />
{/each} {/each}
</select> </Select.Content>
</label> </Select.Root>
</div>
<label class="block space-y-1.5"> <label class="block space-y-1.5">
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase" <span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
>Details</span >Details</span