diff options
| author | 2026-01-15 17:36:36 +0800 | |
|---|---|---|
| committer | 2026-01-15 17:36:41 +0800 | |
| commit | 31077dbd39a25eecd24a1dca0f8c9d1879265277 (patch) | |
| tree | 4cae8d216c3093421addaa0450bc8004c537e373 /ui | |
| parent | 76559c624f7d2418c2f25e4cb2d3c994f4218964 (diff) | |
| download | DropOut-31077dbd39a25eecd24a1dca0f8c9d1879265277.tar.gz DropOut-31077dbd39a25eecd24a1dca0f8c9d1879265277.zip | |
feat: Implement custom dropdown components for version selection in BottomBar and ModLoaderSelector, enhancing user interaction and UI consistency
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/src/app.css | 97 | ||||
| -rw-r--r-- | ui/src/components/BottomBar.svelte | 97 | ||||
| -rw-r--r-- | ui/src/components/CustomSelect.svelte | 136 | ||||
| -rw-r--r-- | ui/src/components/ModLoaderSelector.svelte | 150 | ||||
| -rw-r--r-- | ui/src/components/SettingsView.svelte | 33 | ||||
| -rw-r--r-- | ui/src/lib/GameConsole.svelte | 19 |
6 files changed, 443 insertions, 89 deletions
diff --git a/ui/src/app.css b/ui/src/app.css index 5d0404b..82aa72f 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -14,43 +14,48 @@ select { padding-right: 2rem; } -/* Custom scrollbar for dropdowns and lists */ -select, -.custom-select { - scrollbar-width: thin; - scrollbar-color: #3f3f46 #18181b; -} - -/* Webkit scrollbar for select (when expanded, browser-dependent) */ -select::-webkit-scrollbar { - width: 8px; +/* Option styling - works in WebView/Chromium */ +select option { + background-color: #18181b; + color: #e4e4e7; + padding: 12px 16px; + font-size: 13px; + border: none; } -select::-webkit-scrollbar-track { - background: #18181b; +select option:hover, +select option:focus { + background-color: #3730a3 !important; + color: white !important; } -select::-webkit-scrollbar-thumb { - background-color: #3f3f46; - border-radius: 4px; +select option:checked { + background: linear-gradient(0deg, #4f46e5 0%, #4f46e5 100%); + color: white; + font-weight: 500; } -select::-webkit-scrollbar-thumb:hover { - background-color: #52525b; +select option:disabled { + color: #52525b; + background-color: #18181b; } -/* Option styling (limited browser support but good for Tauri/WebView) */ -select option { +/* Optgroup styling */ +select optgroup { background-color: #18181b; - color: #e4e4e7; - padding: 8px 12px; + color: #a1a1aa; + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 8px 12px 4px; } -select option:hover, -select option:focus, -select option:checked { - background: linear-gradient(#3730a3, #3730a3); - color: white; +/* Select focus state */ +select:focus { + outline: none; + border-color: #6366f1; + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); } /* ==================== Custom Scrollbar (Global) ==================== */ @@ -118,3 +123,43 @@ input[type="number"]::-webkit-inner-spin-button { input[type="number"] { -moz-appearance: textfield; } + +/* ==================== Checkbox Styling ==================== */ + +input[type="checkbox"] { + appearance: none; + width: 16px; + height: 16px; + border: 1px solid #3f3f46; + border-radius: 4px; + background-color: #18181b; + cursor: pointer; + position: relative; + transition: all 0.15s ease; +} + +input[type="checkbox"]:hover { + border-color: #52525b; +} + +input[type="checkbox"]:checked { + background-color: #4f46e5; + border-color: #4f46e5; +} + +input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +input[type="checkbox"]:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3); +} diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte index 198d4e6..abb0b23 100644 --- a/ui/src/components/BottomBar.svelte +++ b/ui/src/components/BottomBar.svelte @@ -2,7 +2,39 @@ import { authState } from "../stores/auth.svelte"; import { gameState } from "../stores/game.svelte"; import { uiState } from "../stores/ui.svelte"; - import { Terminal, ChevronDown, Play, User } from 'lucide-svelte'; + import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte'; + + let isVersionDropdownOpen = $state(false); + let dropdownRef: HTMLDivElement; + + let versionOptions = $derived( + gameState.versions.length === 0 + ? [{ id: "loading", type: "loading", label: "Loading..." }] + : gameState.versions.map(v => ({ + ...v, + label: `${v.id}${v.type !== 'release' ? ` (${v.type})` : ''}` + })) + ); + + function selectVersion(id: string) { + if (id !== "loading") { + gameState.selectedVersion = id; + isVersionDropdownOpen = false; + } + } + + function handleClickOutside(e: MouseEvent) { + if (dropdownRef && !dropdownRef.contains(e.target as Node)) { + isVersionDropdownOpen = false; + } + } + + $effect(() => { + if (isVersionDropdownOpen) { + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + } + }); </script> <div @@ -60,23 +92,56 @@ <!-- Action Area --> <div class="flex items-center gap-4"> <div class="flex flex-col items-end mr-2"> - <div class="relative group"> - <select - id="version-select" - bind:value={gameState.selectedVersion} - class="appearance-none dark:bg-zinc-900 bg-zinc-50 dark:text-white text-gray-900 border dark:border-white/10 border-black/10 rounded-sm pl-4 pr-10 py-2 dark:hover:border-white/30 hover:border-black/30 transition-all cursor-pointer outline-none focus:ring-1 focus:ring-zinc-500 w-56 text-sm font-mono" + <!-- Custom Version Dropdown --> + <div class="relative" bind:this={dropdownRef}> + <button + type="button" + onclick={() => isVersionDropdownOpen = !isVersionDropdownOpen} + class="flex items-center justify-between gap-2 w-56 px-4 py-2.5 text-left + dark:bg-zinc-900 bg-zinc-50 border dark:border-zinc-700 border-zinc-300 rounded-md + text-sm font-mono dark:text-white text-gray-900 + dark:hover:border-zinc-600 hover:border-zinc-400 + focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 + transition-colors cursor-pointer outline-none" > - {#if gameState.versions.length === 0} - <option>Loading...</option> - {:else} - {#each gameState.versions as version} - <option value={version.id}>{version.id} {version.type !== 'release' ? `(${version.type})` : ''}</option> + <span class="truncate"> + {#if gameState.versions.length === 0} + Loading... + {:else} + {gameState.selectedVersion || "Select version"} + {/if} + </span> + <ChevronDown + size={14} + class="shrink-0 dark:text-zinc-500 text-zinc-400 transition-transform duration-200 {isVersionDropdownOpen ? 'rotate-180' : ''}" + /> + </button> + + {#if isVersionDropdownOpen} + <div + class="absolute z-50 w-full mt-1 py-1 dark:bg-zinc-900 bg-white border dark:border-zinc-700 border-zinc-300 rounded-md shadow-xl + max-h-72 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150 bottom-full mb-1" + > + {#each versionOptions as version} + <button + type="button" + onclick={() => selectVersion(version.id)} + disabled={version.id === "loading"} + class="w-full flex items-center justify-between px-3 py-2 text-sm font-mono text-left + transition-colors outline-none + {version.id === gameState.selectedVersion + ? 'bg-indigo-600 text-white' + : 'dark:text-zinc-300 text-gray-700 dark:hover:bg-zinc-800 hover:bg-zinc-100'} + {version.id === 'loading' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}" + > + <span class="truncate">{version.label}</span> + {#if version.id === gameState.selectedVersion} + <Check size={14} class="shrink-0 ml-2" /> + {/if} + </button> {/each} - {/if} - </select> - <div class="absolute right-3 top-1/2 -translate-y-1/2 dark:text-white/20 text-black/20 pointer-events-none"> - <ChevronDown size={14} /> - </div> + </div> + {/if} </div> </div> diff --git a/ui/src/components/CustomSelect.svelte b/ui/src/components/CustomSelect.svelte new file mode 100644 index 0000000..2e89c75 --- /dev/null +++ b/ui/src/components/CustomSelect.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import { ChevronDown, Check } from 'lucide-svelte'; + + interface Option { + value: string; + label: string; + disabled?: boolean; + } + + interface Props { + options: Option[]; + value: string; + placeholder?: string; + disabled?: boolean; + class?: string; + onchange?: (value: string) => void; + } + + let { + options, + value = $bindable(), + placeholder = "Select...", + disabled = false, + class: className = "", + onchange + }: Props = $props(); + + let isOpen = $state(false); + let containerRef: HTMLDivElement; + + let selectedOption = $derived(options.find(o => o.value === value)); + + function toggle() { + if (!disabled) { + isOpen = !isOpen; + } + } + + function select(option: Option) { + if (option.disabled) return; + value = option.value; + isOpen = false; + onchange?.(option.value); + } + + function handleKeydown(e: KeyboardEvent) { + if (disabled) return; + + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggle(); + } else if (e.key === 'Escape') { + isOpen = false; + } else if (e.key === 'ArrowDown' && isOpen) { + e.preventDefault(); + const currentIndex = options.findIndex(o => o.value === value); + const nextIndex = Math.min(currentIndex + 1, options.length - 1); + if (!options[nextIndex].disabled) { + value = options[nextIndex].value; + } + } else if (e.key === 'ArrowUp' && isOpen) { + e.preventDefault(); + const currentIndex = options.findIndex(o => o.value === value); + const prevIndex = Math.max(currentIndex - 1, 0); + if (!options[prevIndex].disabled) { + value = options[prevIndex].value; + } + } + } + + function handleClickOutside(e: MouseEvent) { + if (containerRef && !containerRef.contains(e.target as Node)) { + isOpen = false; + } + } + + $effect(() => { + if (isOpen) { + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + } + }); +</script> + +<div + bind:this={containerRef} + class="relative {className}" +> + <!-- Trigger Button --> + <button + type="button" + onclick={toggle} + onkeydown={handleKeydown} + {disabled} + class="w-full flex items-center justify-between gap-2 px-3 py-2 pr-8 text-left + bg-zinc-900 border border-zinc-700 rounded-md text-sm text-zinc-200 + hover:border-zinc-600 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 + transition-colors cursor-pointer outline-none + disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-zinc-700" + > + <span class="truncate {!selectedOption ? 'text-zinc-500' : ''}"> + {selectedOption?.label || placeholder} + </span> + <ChevronDown + size={14} + class="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" + /> + </button> + + <!-- Dropdown Menu --> + {#if isOpen} + <div + class="absolute z-50 w-full mt-1 py-1 bg-zinc-900 border border-zinc-700 rounded-md shadow-xl + max-h-60 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150" + > + {#each options as option} + <button + type="button" + onclick={() => select(option)} + disabled={option.disabled} + class="w-full flex items-center justify-between px-3 py-2 text-sm text-left + transition-colors outline-none + {option.value === value + ? 'bg-indigo-600 text-white' + : 'text-zinc-300 hover:bg-zinc-800'} + {option.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}" + > + <span class="truncate">{option.label}</span> + {#if option.value === value} + <Check size={14} class="shrink-0 ml-2" /> + {/if} + </button> + {/each} + </div> + {/if} +</div> diff --git a/ui/src/components/ModLoaderSelector.svelte b/ui/src/components/ModLoaderSelector.svelte index d0c1b59..cb949c5 100644 --- a/ui/src/components/ModLoaderSelector.svelte +++ b/ui/src/components/ModLoaderSelector.svelte @@ -6,7 +6,7 @@ ForgeVersion, ModLoaderType, } from "../types"; - import { Loader2, Download, AlertCircle, Check } from 'lucide-svelte'; + import { Loader2, Download, AlertCircle, Check, ChevronDown } from 'lucide-svelte'; interface Props { selectedGameVersion: string; @@ -23,10 +23,15 @@ // Fabric state let fabricLoaders = $state<FabricLoaderVersion[]>([]); let selectedFabricLoader = $state(""); + let isFabricDropdownOpen = $state(false); // Forge state let forgeVersions = $state<ForgeVersion[]>([]); let selectedForgeVersion = $state(""); + let isForgeDropdownOpen = $state(false); + + let fabricDropdownRef = $state<HTMLDivElement | null>(null); + let forgeDropdownRef = $state<HTMLDivElement | null>(null); // Load mod loader versions when game version changes $effect(() => { @@ -111,6 +116,44 @@ loadModLoaderVersions(); } } + + function handleFabricClickOutside(e: MouseEvent) { + if (fabricDropdownRef && !fabricDropdownRef.contains(e.target as Node)) { + isFabricDropdownOpen = false; + } + } + + function handleForgeClickOutside(e: MouseEvent) { + if (forgeDropdownRef && !forgeDropdownRef.contains(e.target as Node)) { + isForgeDropdownOpen = false; + } + } + + $effect(() => { + if (isFabricDropdownOpen) { + document.addEventListener('click', handleFabricClickOutside); + return () => document.removeEventListener('click', handleFabricClickOutside); + } + }); + + $effect(() => { + if (isForgeDropdownOpen) { + document.addEventListener('click', handleForgeClickOutside); + return () => document.removeEventListener('click', handleForgeClickOutside); + } + }); + + let selectedFabricLabel = $derived( + fabricLoaders.find(l => l.version === selectedFabricLoader) + ? `${selectedFabricLoader}${fabricLoaders.find(l => l.version === selectedFabricLoader)?.stable ? ' (stable)' : ''}` + : selectedFabricLoader || 'Select version' + ); + + let selectedForgeLabel = $derived( + forgeVersions.find(v => v.version === selectedForgeVersion) + ? `${selectedForgeVersion}${forgeVersions.find(v => v.version === selectedForgeVersion)?.recommended ? ' (Recommended)' : ''}` + : selectedForgeVersion || 'Select version' + ); </script> <div class="space-y-4"> @@ -163,18 +206,48 @@ <label for="fabric-loader-select" class="block text-[10px] uppercase font-bold text-zinc-500 mb-2" >Loader Version</label > - <div class="relative"> - <select - id="fabric-loader-select" - class="w-full bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md px-4 py-2.5 pr-10 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 text-gray-900 dark:text-white transition-colors cursor-pointer hover:border-zinc-400 dark:hover:border-zinc-600" - bind:value={selectedFabricLoader} + <!-- Custom Fabric Dropdown --> + <div class="relative" bind:this={fabricDropdownRef}> + <button + type="button" + onclick={() => isFabricDropdownOpen = !isFabricDropdownOpen} + class="w-full flex items-center justify-between gap-2 px-4 py-2.5 text-left + bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md + text-sm text-gray-900 dark:text-white + hover:border-zinc-400 dark:hover:border-zinc-600 + focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 + transition-colors cursor-pointer outline-none" > - {#each fabricLoaders as loader} - <option value={loader.version}> - {loader.version} {loader.stable ? "(stable)" : ""} - </option> - {/each} - </select> + <span class="truncate">{selectedFabricLabel}</span> + <ChevronDown + size={14} + class="shrink-0 text-zinc-400 dark:text-zinc-500 transition-transform duration-200 {isFabricDropdownOpen ? 'rotate-180' : ''}" + /> + </button> + + {#if isFabricDropdownOpen} + <div + class="absolute z-50 w-full mt-1 py-1 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md shadow-xl + max-h-48 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150" + > + {#each fabricLoaders as loader} + <button + type="button" + onclick={() => { selectedFabricLoader = loader.version; isFabricDropdownOpen = false; }} + class="w-full flex items-center justify-between px-3 py-2 text-sm text-left + transition-colors outline-none cursor-pointer + {loader.version === selectedFabricLoader + ? 'bg-indigo-600 text-white' + : 'text-gray-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800'}" + > + <span class="truncate">{loader.version} {loader.stable ? "(stable)" : ""}</span> + {#if loader.version === selectedFabricLoader} + <Check size={14} class="shrink-0 ml-2" /> + {/if} + </button> + {/each} + </div> + {/if} </div> </div> @@ -199,19 +272,48 @@ <label for="forge-version-select" class="block text-[10px] uppercase font-bold text-zinc-500 mb-2" >Forge Version</label > - <div class="relative"> - <select - id="forge-version-select" - class="w-full bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md px-4 py-2.5 pr-10 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 text-gray-900 dark:text-white transition-colors cursor-pointer hover:border-zinc-400 dark:hover:border-zinc-600" - bind:value={selectedForgeVersion} + <!-- Custom Forge Dropdown --> + <div class="relative" bind:this={forgeDropdownRef}> + <button + type="button" + onclick={() => isForgeDropdownOpen = !isForgeDropdownOpen} + class="w-full flex items-center justify-between gap-2 px-4 py-2.5 text-left + bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md + text-sm text-gray-900 dark:text-white + hover:border-zinc-400 dark:hover:border-zinc-600 + focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 + transition-colors cursor-pointer outline-none" > - {#each forgeVersions as version} - <option value={version.version}> - {version.version} - {version.recommended ? " (Recommended)" : ""} - </option> - {/each} - </select> + <span class="truncate">{selectedForgeLabel}</span> + <ChevronDown + size={14} + class="shrink-0 text-zinc-400 dark:text-zinc-500 transition-transform duration-200 {isForgeDropdownOpen ? 'rotate-180' : ''}" + /> + </button> + + {#if isForgeDropdownOpen} + <div + class="absolute z-50 w-full mt-1 py-1 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md shadow-xl + max-h-48 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150" + > + {#each forgeVersions as version} + <button + type="button" + onclick={() => { selectedForgeVersion = version.version; isForgeDropdownOpen = false; }} + class="w-full flex items-center justify-between px-3 py-2 text-sm text-left + transition-colors outline-none cursor-pointer + {version.version === selectedForgeVersion + ? 'bg-indigo-600 text-white' + : 'text-gray-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800'}" + > + <span class="truncate">{version.version} {version.recommended ? "(Recommended)" : ""}</span> + {#if version.version === selectedForgeVersion} + <Check size={14} class="shrink-0 ml-2" /> + {/if} + </button> + {/each} + </div> + {/if} </div> </div> diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte index 732f857..76d441b 100644 --- a/ui/src/components/SettingsView.svelte +++ b/ui/src/components/SettingsView.svelte @@ -1,11 +1,22 @@ <script lang="ts"> import { open } from "@tauri-apps/plugin-dialog"; import { settingsState } from "../stores/settings.svelte"; + import CustomSelect from "./CustomSelect.svelte"; // Use convertFileSrc directly from settingsState.backgroundUrl for cleaner approach // or use the imported one if passing raw path. import { convertFileSrc } from "@tauri-apps/api/core"; + const effectOptions = [ + { value: "saturn", label: "Saturn" }, + { value: "constellation", label: "Network (Constellation)" } + ]; + + const logServiceOptions = [ + { value: "paste.rs", label: "paste.rs (Free, No Account)" }, + { value: "pastebin.com", label: "pastebin.com (Requires API Key)" } + ]; + async function selectBackground() { try { const selected = await open({ @@ -116,15 +127,12 @@ <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="theme-effect-label">Theme Effect</h4> <p class="text-xs dark:text-white/40 text-black/50 mt-1">Select the active visual theme.</p> </div> - <select - aria-labelledby="theme-effect-label" + <CustomSelect + options={effectOptions} bind:value={settingsState.settings.active_effect} onchange={() => settingsState.saveSettings()} - class="dark:bg-zinc-900 bg-white dark:text-white text-black text-xs px-3 py-2 pr-8 rounded-lg border dark:border-zinc-700 border-gray-300 outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 cursor-pointer hover:border-zinc-600 transition-colors" - > - <option value="saturn">Saturn (Saturn)</option> - <option value="constellation">Network (Constellation)</option> - </select> + class="w-52" + /> </div> {/if} @@ -308,14 +316,11 @@ <div class="space-y-4"> <div> <label for="log-service" class="block text-sm font-medium text-white/70 mb-2">Log Upload Service</label> - <select - id="log-service" + <CustomSelect + options={logServiceOptions} bind:value={settingsState.settings.log_upload_service} - class="dark:bg-zinc-900 bg-white dark:text-white text-black w-full px-4 py-3 pr-10 rounded-xl border dark:border-zinc-700 border-gray-300 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none cursor-pointer hover:border-zinc-600 transition-colors" - > - <option value="paste.rs">paste.rs (Free, No Account)</option> - <option value="pastebin.com">pastebin.com (Requires API Key)</option> - </select> + class="w-full" + /> </div> {#if settingsState.settings.log_upload_service === 'pastebin.com'} diff --git a/ui/src/lib/GameConsole.svelte b/ui/src/lib/GameConsole.svelte index 1b1ab53..bc5edbc 100644 --- a/ui/src/lib/GameConsole.svelte +++ b/ui/src/lib/GameConsole.svelte @@ -6,6 +6,8 @@ import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-shell"; import { onMount, tick } from "svelte"; + import CustomSelect from "../components/CustomSelect.svelte"; + import { ChevronDown, Check } from 'lucide-svelte'; let consoleElement: HTMLDivElement; let autoScroll = $state(true); @@ -21,7 +23,10 @@ let selectedSource = $state("all"); // Get sorted sources for dropdown - let sortedSources = $derived([...logsState.sources].sort()); + let sourceOptions = $derived([ + { value: "all", label: "All Sources" }, + ...[...logsState.sources].sort().map(s => ({ value: s, label: s })) + ]); // Derived filtered logs let filteredLogs = $derived(logsState.logs.filter((log) => { @@ -151,15 +156,11 @@ <h3 class="font-bold text-zinc-100 uppercase tracking-wider px-2">Console</h3> <!-- Source Dropdown --> - <select + <CustomSelect + options={sourceOptions} bind:value={selectedSource} - class="bg-zinc-900 border border-zinc-700 rounded-md px-3 py-1.5 pr-8 text-zinc-300 text-xs focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500/30 cursor-pointer hover:border-zinc-600 transition-colors" - > - <option value="all">All Sources</option> - {#each sortedSources as source} - <option value={source}>{source}</option> - {/each} - </select> + class="w-36" + /> <!-- Level Filters --> <div class="flex items-center bg-[#1e1e1e] rounded border border-[#3e3e42] overflow-hidden"> |