feat(badges): smooth transitions + polish for badge search UI

- Adds fade/slide transitions throughout the search form: form mount/unmount,
  filter row, QR scan button, QR scanner panel, Show Hidden, Remote First labels
- Min-chars hint switches from class:invisible to opacity-0/opacity-50 +
  transition-opacity so it fades instead of snapping
- Clear button switches from class:hidden to opacity-0 + pointer-events-none
  + transition-all so it fades without causing layout shifts
- "Start Here" button gets transition-opacity for smooth dim on first keystroke
- Replaces FileSearch with UserSearch icon in the empty state
- Adds w-full to empty state div to prevent subtle page-width shift between
  no-results and results states

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-02 16:25:31 -04:00
parent 60bdd2fdba
commit 3773758eb5
2 changed files with 22 additions and 13 deletions

View File

@@ -27,14 +27,14 @@ import {
Check,
Eye,
EyeOff,
FileSearch,
Link,
LoaderCircle,
Mail,
MapPin,
Printer,
Tags,
User
User,
UserSearch
} from '@lucide/svelte';
// Track per-badge copy state for the "Review Link" clipboard button
let copy_status: Record<string, 'idle' | 'copied'> = $state({});
@@ -408,8 +408,8 @@ let visible_badge_obj_li = $derived(
</ul>
{:else}
<div
class="flex flex-col items-center justify-center p-20 text-center opacity-50">
<FileSearch size="3em" class="mx-auto mb-2 opacity-20" />
class="flex w-full flex-col items-center justify-center p-20 text-center opacity-50">
<UserSearch size="3em" class="mx-auto mb-2 opacity-20" />
{#if !is_trusted && !(badges_loc.current.fulltext_search_qry_str ?? '').trim()}
<p>Enter your name above to find your badge.</p>
{:else}

View File

@@ -17,6 +17,7 @@ import {
StepForward
} from '@lucide/svelte';
import { fade, slide } from 'svelte/transition';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_sess } from '$lib/stores/ae_events_stores';
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
@@ -87,6 +88,7 @@ function handle_qr_scan_result(event: {
class="ae_group filters_and_search flex w-full flex-col items-center justify-center gap-2">
{#if $events_sess.badges.show_form__search}
<form
transition:fade={{ duration: 200 }}
onsubmit={prevent_default(() => {
handle_search_trigger();
})}
@@ -95,7 +97,7 @@ function handle_qr_scan_result(event: {
<div
class="flex grow flex-col xl:flex-row flex-wrap items-center justify-center gap-2">
{#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $ae_loc.manager_access}
<div class="flex flex-row flex-wrap items-center justify-center gap-1">
<div transition:slide={{ duration: 200 }} class="flex flex-row flex-wrap items-center justify-center gap-1">
<span class="flex flex-row flex-wrap items-center justify-center gap-1">
<select
bind:value={badges_loc.current.search_badge_type_code}
@@ -159,6 +161,7 @@ function handle_qr_scan_result(event: {
{#if (badges_loc.current.enable_search_qr && $ae_loc.edit_mode)}
<button
transition:fade={{ duration: 150 }}
type="button"
onclick={() => {
$events_sess.badges.show_form__search = false;
@@ -177,7 +180,7 @@ function handle_qr_scan_result(event: {
onclick={() => document.getElementById('badge_fulltext_search_qry_str')?.focus()}
aria-label="Start here focus search field"
data-testid="badge-start-btn"
class="btn btn-sm preset-filled-secondary-200-800 font-semibold"
class="btn btn-sm preset-filled-secondary-200-800 font-semibold transition-opacity duration-300"
class:opacity-30={!!badges_loc.current.fulltext_search_qry_str}
class:hidden={$ae_loc.trusted_access}
>
@@ -218,11 +221,11 @@ function handle_qr_scan_result(event: {
</div>
</div>
{#if (badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}
<p class="w-full text-center text-xs opacity-50">
Enter at least {effective_min_chars} character{effective_min_chars === 1 ? '' : 's'} to search
</p>
{/if}
<p class="w-full text-center text-xs transition-opacity duration-200"
class:opacity-0={(badges_loc.current.fulltext_search_qry_str ?? '').trim().length >= effective_min_chars}
class:opacity-50={(badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}>
Enter at least {effective_min_chars} character{effective_min_chars === 1 ? '' : 's'} to search
</p>
<div class="flex flex-row items-center justify-center gap-1">
<button
@@ -241,7 +244,10 @@ function handle_qr_scan_result(event: {
<button
type="button"
class:hidden={!badges_loc.current.fulltext_search_qry_str &&
class:opacity-0={!badges_loc.current.fulltext_search_qry_str &&
!badges_loc.current.search_badge_type_code &&
badges_loc.current.qry_printed_status === 'all'}
class:pointer-events-none={!badges_loc.current.fulltext_search_qry_str &&
!badges_loc.current.search_badge_type_code &&
badges_loc.current.qry_printed_status === 'all'}
onclick={() => {
@@ -254,7 +260,7 @@ function handle_qr_scan_result(event: {
document.getElementById('badge_fulltext_search_qry_str')?.focus();
}}
class="
hover:text-tertiary-800-200 hover:bg-tertiary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-sm font-bold transition-all duration-1000 hover:duration-300 min-w-0 preset-outlined-tertiary rounded-lg border border-tertiary-200-800
hover:text-tertiary-800-200 hover:bg-tertiary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-sm font-bold transition-all duration-200 hover:duration-100 min-w-0 preset-outlined-tertiary rounded-lg border border-tertiary-200-800
"
title="Clear search query">
@@ -264,6 +270,7 @@ function handle_qr_scan_result(event: {
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<label
transition:fade={{ duration: 150 }}
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Show Hidden </span>
<input
@@ -275,6 +282,7 @@ function handle_qr_scan_result(event: {
{/if}
{#if $ae_loc.edit_mode}
<label
transition:fade={{ duration: 150 }}
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Remote First </span>
<input
@@ -288,6 +296,7 @@ function handle_qr_scan_result(event: {
</form>
{:else if $events_sess.badges.show_form__scan}
<div
transition:fade={{ duration: 200 }}
class="bg-surface-100-900 mx-auto w-full max-w-2xl rounded-lg p-4 shadow-lg">
<Element_qr_scanner
bind:start_qr_scanner={$events_sess.badges.qr_scan_start}