mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-31 06:40:50 +01:00
style: modal'd search action on mobile
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user