Started melt combobox implementation

This commit is contained in:
WardPearce
2026-03-13 10:54:38 +13:00
parent caad66656f
commit c20e199e5e
4 changed files with 106 additions and 13 deletions
@@ -0,0 +1,77 @@
<script lang="ts">
import { _ } from '$lib/i18n';
import { mergeAttrs } from 'melt';
import { Combobox } from 'melt/builders';
type ComboOption = { label: string; value: string };
let {
options,
label,
defaultValue = undefined,
onChange = undefined
}: {
options: ComboOption[];
label: string;
defaultValue?: string;
onChange?: (value: string) => void;
} = $props();
let initialValueChange = true;
const combobox = new Combobox<string>({
onValueChange: (value) => {
if (initialValueChange && typeof defaultValue !== 'undefined') {
initialValueChange = false;
return;
}
if (value) onChange?.(value);
}
});
if (defaultValue) combobox.select(defaultValue);
let valueToLabel: Record<string, string> = {};
setValuesToLabel();
function setValuesToLabel() {
for (const option of options) {
valueToLabel[option.value] = option.label;
}
}
$effect(() => {
setValuesToLabel();
});
const filtered = $derived.by(() => {
if (!combobox.touched) return options;
return options.filter((o) =>
o.label.toLowerCase().includes(combobox.inputValue.trim().toLowerCase())
);
});
</script>
<div class="field label suffix surface-container-highest">
<input
{...mergeAttrs(combobox.input, {
value: valueToLabel[combobox.input.value]
})}
/>
<label {...combobox.label}>{label}</label>
<i>keyboard_arrow_down</i>
<div {...combobox.content}>
{#each filtered as option (option)}
<div {...combobox.getOption(option.value)}>
{option.label}
{#if combobox.isSelected(option.value)}
<i>check</i>
{/if}
</div>
{:else}
<span>{$_('noResult')}</span>
{/each}
</div>
</div>
@@ -35,6 +35,7 @@
} from '../../store';
import { tick } from 'svelte';
import { isOwnBackend } from '$lib/shared';
import ComboBox from '../ComboBox.svelte';
let invidiousInstance = $state(get(invidiousInstanceStore));
let region = $state(get(interfaceRegionStore));
@@ -81,10 +82,8 @@
location.reload();
}
async function setBackend(event: Event) {
const select = event.target as HTMLSelectElement;
backendInUseStore.set(select.value as 'ivg' | 'yt');
async function setBackend(backend: string) {
backendInUseStore.set(backend as 'ivg' | 'yt');
await timeout(100);
location.reload();
}
@@ -120,14 +119,15 @@
</script>
{#if isUnrestrictedPlatform()}
<div class="field label suffix surface-container-highest">
<select name="backend-in-use" onchange={setBackend}>
<option selected={$backendInUseStore === 'ivg'} value="ivg">Invidious</option>
<option selected={$backendInUseStore === 'yt'} value="yt">YouTube (Experimental)</option>
</select>
<label for="backend-in-use">{$_('backend')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('backend')}
defaultValue={$backendInUseStore}
onChange={setBackend}
options={[
{ label: 'Invidious', value: 'ivg' },
{ label: 'YouTube (Experimental)', value: 'yt' }
]}
/>
{#if $backendInUseStore === 'ivg'}
<form onsubmit={setInstance}>
+16
View File
@@ -194,6 +194,22 @@ menu.player-settings {
transform: translate(-50%, -50%);
}
[data-melt-combobox-content] {
background-color: var(--surface-container-high);
padding: 1rem;
font-size: 1rem;
border-radius: var(--border-radius);
}
[data-melt-combobox-option] {
padding: 0.25rem;
cursor: pointer;
}
[data-highlighted] {
background-color: var(--surface-container-highest);
}
@media screen and (max-width: 1000px) {
menu.mobile {
position: fixed !important;
File diff suppressed because one or more lines are too long