From 9d904446d49520b545ad43b7bd578091f3ba893c Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 4 Jun 2026 18:33:59 -0400 Subject: [PATCH] feat(badges): search filter polish and result limit stepper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace show_hidden checkbox with visibility_filter select (Default / Show Hidden / Show Disabled+Hidden) — collapses two orphaned boolean fields (show_hidden, show_not_enabled) into one purpose-built value; wires disabled-badge filter through to both IDB and API paths - Add max-results stepper (edit mode only): steps of 25 up to 250, steps of 100 up to 2550; tier-capped (trusted=250, manager=2550); stepper uses pure reactivity — no handle_search_trigger() call needed - Fix fallback liveQuery (SCENARIO 2): was hardcoded .limit(50); now reads qry_result_limit in outer $derived.by so Svelte tracks it and stepper updates the no-text browse list immediately - Fix Search button disabled state: replace pointer-events-none + class:opacity-50 with HTML disabled attribute + disabled:cursor-not-allowed so hover cursor reflects disabled state correctly - Global placeholder fix (app.css): add italic + opacity-0.6 rule for .input/.textarea ::placeholder in light mode; add italic to dark rule — prevents placeholder text from reading as typed content Co-Authored-By: Claude Sonnet 4.6 --- src/app.css | 9 +- .../ae_events_stores__badges_defaults.ts | 9 +- .../[event_id]/(badges)/badges/+page.svelte | 20 +++- .../badges/ae_comp__badge_obj_li.svelte | 7 +- .../badges/ae_comp__badge_search.svelte | 97 +++++++++++++++---- 5 files changed, 110 insertions(+), 32 deletions(-) diff --git a/src/app.css b/src/app.css index 00fa408d..ea6460f6 100644 --- a/src/app.css +++ b/src/app.css @@ -163,9 +163,16 @@ html.light { background-color: rgb(55 65 81); /* gray-700 */ border-color: rgb(75 85 99); /* gray-600 */ } +.input::placeholder, +.textarea::placeholder { + font-style: italic; + opacity: 0.6; +} .dark .input::placeholder, .dark .textarea::placeholder { - color: rgb(156 163 175); /* gray-400 — legible at reduced opacity */ + color: rgb(156 163 175); /* gray-400 */ + font-style: italic; + opacity: 0.8; /* gray-400 is already dim; subtle additional fade */ } /* Option elements in dark selects — forces browser native dark chrome */ .dark .select option { diff --git a/src/lib/stores/ae_events_stores__badges_defaults.ts b/src/lib/stores/ae_events_stores__badges_defaults.ts index c86978bf..e8ab0eb3 100644 --- a/src/lib/stores/ae_events_stores__badges_defaults.ts +++ b/src/lib/stores/ae_events_stores__badges_defaults.ts @@ -97,8 +97,8 @@ export const default_trusted_can_edit: string[] = [ // --------------------------------------------------------------------------- export interface BadgesLocState { auto_view: boolean; - show_hidden: boolean; - show_not_enabled: boolean; + // 'default' = hide hidden+disabled | 'show_hidden' = show hidden | 'show_all' = show hidden+disabled + visibility_filter: 'default' | 'show_hidden' | 'show_all'; show_printed: boolean; allow_reprint: boolean; show_element__cfg: boolean; @@ -110,6 +110,7 @@ export interface BadgesLocState { qry_printed_status: string; // 'all' | 'printed' | 'not_printed' qry_affiliations: string | null; qry_sort_order: string; + qry_result_limit: number; // UI override for max results (edit mode only; tier max enforced by stepper) status_qry__search: string | null; use_id_li: boolean; search_status: string | null; @@ -158,8 +159,7 @@ export interface BadgesSessState { export const badges_loc_defaults: BadgesLocState = { auto_view: true, - show_hidden: false, // Hidden (archived) badges are excluded from the main list. - show_not_enabled: false, + visibility_filter: 'default', // 'default' | 'show_hidden' | 'show_all' show_printed: false, allow_reprint: false, @@ -178,6 +178,7 @@ export const badges_loc_defaults: BadgesLocState = { qry_printed_status: 'all', // 'all' | 'printed' | 'not_printed' qry_affiliations: null, // null = no affiliation filter qry_sort_order: '', // '' = default sort order + qry_result_limit: 25, status_qry__search: null, use_id_li: true, diff --git a/src/routes/events/[event_id]/(badges)/badges/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/+page.svelte index 821c9ca4..6a314103 100644 --- a/src/routes/events/[event_id]/(badges)/badges/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/+page.svelte @@ -101,6 +101,9 @@ $effect(() => { let lq__event_badge_obj_li = $derived.by(() => { const ids = event_badge_id_li; const event_id = $events_slct?.event_id; + // Read in outer scope so Svelte tracks it — liveQuery async callbacks are not tracked. + // In edit mode the stepper controls the limit; otherwise fall back to 50. + const fallback_limit = $ae_loc.edit_mode ? badges_loc.current.qry_result_limit : 50; return liveQuery(async () => { // SCENARIO 1: Specific IDs provided (Search Results) @@ -128,7 +131,7 @@ let lq__event_badge_obj_li = $derived.by(() => { return await db_events.badge .where('event_id') .equals(event_id) - .limit(50) + .limit(fallback_limit) .sortBy('given_name'); } @@ -169,10 +172,13 @@ let search_params = $derived({ printed: badges_loc.current.qry_printed_status, aff: (badges_loc.current.qry_affiliations ?? '').toLowerCase().trim(), sort: badges_loc.current.qry_sort_order, - show_hidden: badges_loc.current.show_hidden, + visibility_filter: badges_loc.current.visibility_filter, event_id: $events_slct?.event_id, remote_first: badges_loc.current.qry__remote_first, - result_limit: effective_search_limits.result_limit, + // In edit mode the stepper overrides the server-configured tier limit. + result_limit: $ae_loc.edit_mode + ? badges_loc.current.qry_result_limit + : effective_search_limits.result_limit, min_chars: effective_search_limits.min_chars }); @@ -219,7 +225,9 @@ async function handle_search_refresh(params: any) { const result_limit = params.result_limit; const min_chars = params.min_chars; - const show_hidden = params.show_hidden; + const visibility_filter = params.visibility_filter as 'default' | 'show_hidden' | 'show_all'; + const show_hidden = visibility_filter !== 'default'; + const show_all = visibility_filter === 'show_all'; // Defense-in-depth: enforce min_chars even if the search component lets one through. // Exception: if the user has set a non-default filter or sort, that is explicit intent — @@ -243,8 +251,9 @@ async function handle_search_refresh(params: any) { .where('event_id') .equals(event_id) .filter((badge) => { - // Exclude hidden badges unless show_hidden is active + // Exclude hidden/disabled badges unless the visibility filter allows them if (!show_hidden && badge.hide) return false; + if (!show_all && badge.enable === false) return false; if (type_code && badge.badge_type_code !== type_code) return false; @@ -423,6 +432,7 @@ async function handle_search_refresh(params: any) { type_code: type_code || null, printed_status: printed_status, affiliations_qry_str: aff_str || null, + enabled: show_all ? 'all' : 'enabled', hidden: show_hidden ? 'all' : 'not_hidden', order_by_li: order_by_li, limit: result_limit, diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte index 373ec351..c4598a57 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte @@ -101,9 +101,11 @@ let visible_badge_obj_li = $derived( if (list === undefined || list === null) return null; if (!Array.isArray(list)) return []; - // show_hidden requires trusted + edit_mode — it's an admin override, not a + const vf = badges_loc.current.visibility_filter; + // visibility_filter requires trusted + edit_mode — it's an admin override, not a // persistent search filter. Turning off edit mode reverts to hiding hidden badges. - const show_hidden_badges = badges_loc.current.show_hidden && is_trusted && is_edit_mode; + const show_hidden_badges = vf !== 'default' && is_trusted && is_edit_mode; + const show_disabled_badges = vf === 'show_all' && is_trusted && is_edit_mode; const filtered = list.filter((item: any) => { if (!item) return false; @@ -112,6 +114,7 @@ let visible_badge_obj_li = $derived( // Filter state persists across edit mode toggles — intentional. Staff set // their filter and it stays regardless of whether edit mode is on or off. const hide_ok = show_hidden_badges || !item.hide; + if (!show_disabled_badges && item.enable === false) return false; const ps = badges_loc.current.qry_printed_status; if (ps === 'printed') return (item.print_count ?? 0) >= 1 && hide_ok; if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && hide_ok; diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte index b4980a94..68543caf 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte @@ -10,12 +10,12 @@ let { event_id, log_lvl = 0 }: Props = $props(); import { // Library, LoaderCircle, + Minus, + Plus, QrCode, RemoveFormatting, Search, - StepForward - } from '@lucide/svelte'; import { fade, slide } from 'svelte/transition'; import { ae_loc, ae_api } from '$lib/stores/ae_stores'; @@ -46,6 +46,19 @@ let badge_type_code_li = [ { code: 'nonmember', name: 'Non-Member' }, ]; +// Steps of 25 up to 250, then steps of 100 up to 2500 — tier max enforced by capping the array. +let limit_steps = $derived.by(() => { + const small = Array.from({ length: 10 }, (_, i) => (i + 1) * 25); // 25…250 + const large = Array.from({ length: 23 }, (_, i) => 350 + i * 100); // 350…2500 + if ($ae_loc.manager_access) return [...small, ...large]; + if ($ae_loc.trusted_access) return small; + return [25]; +}); +let limit_idx = $derived.by(() => { + const idx = limit_steps.indexOf(badges_loc.current.qry_result_limit); + return idx >= 0 ? idx : 0; +}); + // Resolve the minimum characters required for the current user's access tier. // Mirrors the 3-tier logic in +page.svelte so both enforce the same threshold. let effective_min_chars = $derived.by(() => { @@ -102,6 +115,17 @@ function handle_qr_scan_result(event: { {#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $ae_loc.manager_access}
+ + - - {/if} {#if $ae_loc.edit_mode} + {#if limit_steps.length > 1} +
+ + + {badges_loc.current.qry_result_limit} + + +
+ {:else} + + Max: 25 + + {/if} + + {#if $ae_loc.trusted_access} + {/if} {/if}