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

View File

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