style: modal'd search action on mobile

This commit is contained in:
Sam Chau
2026-01-29 02:49:02 +10:30
parent a2c7a2de4b
commit 53259bdcc0
4 changed files with 198 additions and 53 deletions

View File

@@ -2,7 +2,7 @@
export let className: string = '';
</script>
<div class="actions-bar flex {className}">
<div class="actions-bar flex w-fit mx-auto md:w-auto md:mx-0 {className}">
<slot />
</div>

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { onMount, onDestroy, tick } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { fade, fly } from 'svelte/transition';
import { Search, X } from 'lucide-svelte';
import type { SearchStore } from '$lib/client/stores/search';
import Badge from '$ui/badge/Badge.svelte';
@@ -7,11 +9,41 @@
export let searchStore: SearchStore;
export let placeholder: string = 'Search...';
export let activeQuery: string = '';
export let responsive: boolean = false;
const dispatch = createEventDispatcher<{ submit: string; clearQuery: void }>();
let inputRef: HTMLInputElement;
let modalInputRef: HTMLInputElement;
let isFocused = false;
let modalOpen = false;
// Mobile detection
let isMobile = false;
let mediaQuery: MediaQueryList | null = null;
onMount(() => {
if (responsive && typeof window !== 'undefined') {
mediaQuery = window.matchMedia('(max-width: 767px)');
isMobile = mediaQuery.matches;
mediaQuery.addEventListener('change', handleMediaChange);
}
});
onDestroy(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', handleMediaChange);
}
});
function handleMediaChange(e: MediaQueryListEvent) {
isMobile = e.matches;
if (!isMobile && modalOpen) {
modalOpen = false;
}
}
$: useMobileMode = responsive && isMobile;
// Reactive query binding
$: query = $searchStore.query;
@@ -24,68 +56,181 @@
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && query.trim()) {
dispatch('submit', query.trim());
if (modalOpen) modalOpen = false;
} else if (e.key === 'Backspace' && !query && activeQuery) {
dispatch('clearQuery');
} else if (e.key === 'Escape' && modalOpen) {
modalOpen = false;
}
}
function handleClear() {
searchStore.clear();
inputRef?.focus();
if (modalOpen) {
modalInputRef?.focus();
} else {
inputRef?.focus();
}
}
function handleClearQuery() {
dispatch('clearQuery');
inputRef?.focus();
if (modalOpen) {
modalInputRef?.focus();
} else {
inputRef?.focus();
}
}
async function openModal() {
modalOpen = true;
await tick();
modalInputRef?.focus();
}
function closeModal() {
modalOpen = false;
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
closeModal();
}
}
</script>
<div class="relative flex flex-1">
<div
class="relative flex h-10 w-full items-center border border-neutral-200 bg-white transition-all dark:border-neutral-700 dark:bg-neutral-800"
>
<!-- Search icon -->
<div class="pointer-events-none absolute left-3 flex items-center">
<Search size={18} class="text-neutral-500 dark:text-neutral-400" />
</div>
<!-- Active query badge -->
{#if activeQuery}
<div class="ml-10 flex h-full flex-shrink-0 items-center">
<Badge variant="accent" size="sm">{activeQuery}</Badge>
</div>
{/if}
<!-- Input -->
<input
bind:this={inputRef}
type="text"
value={query}
on:input={handleInput}
on:keydown={handleKeydown}
on:focus={() => (isFocused = true)}
on:blur={() => (isFocused = false)}
placeholder={activeQuery ? '' : placeholder}
class="h-full w-full bg-transparent pr-10 text-base sm:text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery
? 'pl-2'
: 'pl-10'}"
/>
<!-- Clear button -->
{#if query}
<button
on:click={handleClear}
class="absolute right-2 flex h-6 w-6 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={14} class="text-neutral-500 dark:text-neutral-400" />
</button>
{:else if activeQuery}
<button
on:click={handleClearQuery}
class="absolute right-2 flex h-6 w-6 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={14} class="text-neutral-500 dark:text-neutral-400" />
</button>
{/if}
{#if useMobileMode}
<!-- Mobile: Search button -->
<div class="relative flex">
<button
type="button"
on:click={openModal}
class="flex h-10 w-10 items-center justify-center border border-neutral-200 bg-white transition-colors hover:bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-neutral-700"
title="Search"
>
<Search size={20} class="text-neutral-700 dark:text-neutral-300" />
{#if query || activeQuery}
<span
class="absolute -top-1 -right-1 z-10 h-2.5 w-2.5 rounded-full bg-accent-500"
></span>
{/if}
</button>
</div>
</div>
<!-- Mobile: Search modal -->
{#if modalOpen}
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div
class="fixed inset-0 z-[100] flex items-start justify-center bg-black/50 pt-4 px-4"
on:click={handleBackdropClick}
transition:fade={{ duration: 150 }}
>
<div
class="w-full max-w-lg rounded-lg border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-800"
transition:fly={{ y: -20, duration: 200 }}
>
<div class="relative flex items-center p-3">
<!-- Search icon -->
<div class="pointer-events-none absolute left-6 flex items-center">
<Search size={18} class="text-neutral-500 dark:text-neutral-400" />
</div>
<!-- Active query badge -->
{#if activeQuery}
<div class="ml-10 flex flex-shrink-0 items-center pr-2">
<Badge variant="accent" size="sm">{activeQuery}</Badge>
</div>
{/if}
<!-- Input -->
<input
bind:this={modalInputRef}
type="text"
value={query}
on:input={handleInput}
on:keydown={handleKeydown}
{placeholder}
class="h-10 w-full rounded-md bg-transparent pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery
? 'pl-2'
: 'pl-10'}"
/>
<!-- Clear/Close button -->
{#if query}
<button
on:click={handleClear}
class="absolute right-6 flex h-8 w-8 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={18} class="text-neutral-500 dark:text-neutral-400" />
</button>
{:else if activeQuery}
<button
on:click={handleClearQuery}
class="absolute right-6 flex h-8 w-8 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={18} class="text-neutral-500 dark:text-neutral-400" />
</button>
{:else}
<button
on:click={closeModal}
class="absolute right-6 flex h-8 w-8 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={18} class="text-neutral-500 dark:text-neutral-400" />
</button>
{/if}
</div>
</div>
</div>
{/if}
{:else}
<!-- Desktop: Inline search -->
<div class="relative flex flex-1">
<div
class="relative flex h-10 w-full items-center border border-neutral-200 bg-white transition-all dark:border-neutral-700 dark:bg-neutral-800"
>
<!-- Search icon -->
<div class="pointer-events-none absolute left-3 flex items-center">
<Search size={18} class="text-neutral-500 dark:text-neutral-400" />
</div>
<!-- Active query badge -->
{#if activeQuery}
<div class="ml-10 flex h-full flex-shrink-0 items-center">
<Badge variant="accent" size="sm">{activeQuery}</Badge>
</div>
{/if}
<!-- Input -->
<input
bind:this={inputRef}
type="text"
value={query}
on:input={handleInput}
on:keydown={handleKeydown}
on:focus={() => (isFocused = true)}
on:blur={() => (isFocused = false)}
placeholder={activeQuery ? '' : placeholder}
class="h-full w-full bg-transparent pr-10 text-base sm:text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery
? 'pl-2'
: 'pl-10'}"
/>
<!-- Clear button -->
{#if query}
<button
on:click={handleClear}
class="absolute right-2 flex h-6 w-6 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={14} class="text-neutral-500 dark:text-neutral-400" />
</button>
{:else if activeQuery}
<button
on:click={handleClearQuery}
class="absolute right-2 flex h-6 w-6 items-center justify-center rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
>
<X size={14} class="text-neutral-500 dark:text-neutral-400" />
</button>
{/if}
</div>
</div>
{/if}

View File

@@ -110,7 +110,7 @@
<!-- Actions Bar -->
<ActionsBar>
<SearchAction searchStore={search} placeholder="Search custom formats..." />
<SearchAction searchStore={search} placeholder="Search custom formats..." responsive />
<ActionButton
icon={Plus}
on:click={() => goto(`/custom-formats/${data.currentDatabase.id}/new`)}

View File

@@ -372,7 +372,7 @@
<!-- Actions Bar -->
<ActionsBar>
<SearchAction searchStore={search} placeholder={searchPlaceholder} />
<SearchAction searchStore={search} placeholder={searchPlaceholder} responsive />
<ActionButton icon={Plus} on:click={() => (showAddModal = true)} />
<ActionButton
icon={Sliders}