hackatime/app/javascript/pages/Home/signedIn/MultiSelect.svelte
Mahad Kalam ef3f36c829
Inertia migration/UI3 (#911)
* Inertia p1?

* Inertia'fied signed out homepage?

* Split up signed in page

* WIP signed in v2?

* Better signed in?

* Clean up extensions page!

* Fix currently hacking

* Better docs page?

* Docs update 2

* Clean up "What is Hackatime?" + get rid of that godawful green dev mode

* Better nav?

* Cleaner settings?

* Fix commit times

* Fix flashes + OS improv

* Setup v2

* Readd some of the syncers?

* Remove stray emdash

* Clean up Step 3

* Oops, remove .vite

* bye bye, /inertia-example

* bin/rubocop -A

* Fix docs vuln
2026-02-09 11:26:30 +00:00

131 lines
3.9 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
let {
label,
param,
values,
selected,
onchange,
}: {
label: string;
param: string;
values: string[];
selected: string[];
onchange: (selected: string[]) => void;
} = $props();
let open = $state(false);
let search = $state("");
let container: HTMLDivElement | undefined = $state();
let filtered = $derived(
search
? values.filter((v) => v.toLowerCase().includes(search.toLowerCase()))
: values,
);
let displayText = $derived(
selected.length === 0
? `Filter by ${label}...`
: selected.length === 1
? selected[0]
: `${selected.length} selected`,
);
function toggle(value: string) {
if (selected.includes(value)) {
onchange(selected.filter((s) => s !== value));
} else {
onchange([...selected, value]);
}
}
function clear(e: MouseEvent) {
e.stopPropagation();
onchange([]);
}
function handleClickOutside(e: MouseEvent) {
if (container && !container.contains(e.target as Node)) {
open = false;
}
}
$effect(() => {
if (open) {
document.addEventListener("click", handleClickOutside, true);
return () =>
document.removeEventListener("click", handleClickOutside, true);
}
});
$effect(() => {
if (!open) {
search = "";
}
});
</script>
<div class="filter relative" bind:this={container}>
<span class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">
{label}
</span>
<div class="group flex items-center border border-white/10 rounded-lg bg-darkless m-0 p-0 transition-all duration-200 hover:border-white/20">
<button
type="button"
class="flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-gray-300 m-0 bg-transparent flex items-center justify-between border-0 min-w-0"
onclick={() => (open = !open)}
>
<span class="truncate {selected.length === 0 ? 'text-secondary/60' : ''}">
{displayText}
</span>
<svg
class={`w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary ${open ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if selected.length > 0}
<button
type="button"
class="px-2.5 py-2 text-sm leading-none text-secondary/60 bg-transparent border-0 border-l border-white/10 cursor-pointer m-0 hover:text-red hover:bg-red/10 transition-colors duration-150"
onclick={clear}
>
×
</button>
{/if}
</div>
{#if open}
<div class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-white/10 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2">
<input
type="text"
placeholder="Search..."
class="w-full border border-white/10 px-3 py-2.5 mb-2 bg-dark text-white text-sm rounded-md h-auto placeholder:text-secondary/60 focus:outline-none focus:border-white/20"
bind:value={search}
/>
<div class="overflow-y-auto m-0 max-h-64">
{#each filtered as value}
<label class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-gray-300 m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
<input
type="checkbox"
checked={selected.includes(value)}
onchange={() => toggle(value)}
class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-white/20 rounded bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-white/40 transition-colors duration-150"
/>
{value}
</label>
{/each}
{#if filtered.length === 0}
<div class="px-3 py-2.5 text-sm text-secondary/60">No results</div>
{/if}
</div>
</div>
{/if}
</div>