diff options
| author | 2026-01-15 17:36:36 +0800 | |
|---|---|---|
| committer | 2026-01-15 17:36:41 +0800 | |
| commit | 31077dbd39a25eecd24a1dca0f8c9d1879265277 (patch) | |
| tree | 4cae8d216c3093421addaa0450bc8004c537e373 /ui/src/components/ModLoaderSelector.svelte | |
| 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/src/components/ModLoaderSelector.svelte')
| -rw-r--r-- | ui/src/components/ModLoaderSelector.svelte | 150 |
1 files changed, 126 insertions, 24 deletions
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> |