From 5f6f1b408b63bbf373307b2b3032eab8d41f7bba Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 12 Mar 2026 14:54:26 -0400 Subject: [PATCH] Badges: live preview while typing, accordion entrance animation, register tailwindcss-animate --- src/app.css | 1 + .../ae_comp__badge_obj_view_v2.svelte | 33 ++++++--- .../ae_comp__badge_print_controls.svelte | 70 +++++++++++++++++++ .../badges/[badge_id]/print/+page.svelte | 8 +++ 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/app.css b/src/app.css index 1c20cb4a..d6bb39d5 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@plugin 'tailwindcss-animate'; /* Enable class-based dark mode for Tailwind v4. Without this, Tailwind v4 defaults to @media (prefers-color-scheme: dark), diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte index d3d8036d..644c720f 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte @@ -32,6 +32,12 @@ font_size_affiliations?: number | null; /** Optional px override for location font size. */ font_size_location?: number | null; + /** + * Real-time preview overrides from the controls panel. + * Merged on top of lq__event_badge_obj so the badge renders typed + * values immediately without waiting for Save + liveQuery round-trip. + */ + preview_overrides?: Record | null; log_lvl?: number; } @@ -45,6 +51,7 @@ font_size_title, font_size_affiliations, font_size_location, + preview_overrides = null, }: Props = $props(); // Badge layout CSS — compiled in, hot-reloads in dev. @@ -73,25 +80,35 @@ } }); + // Effective badge object: merge liveQuery data with preview_overrides when a + // field is being edited in the controls panel. Enables real-time preview while + // the user types — no save required. preview_overrides is null when no field + // is open, so the spread is skipped and this is a zero-cost identity. + let eff_badge = $derived( + (preview_overrides && $lq__event_badge_obj) + ? { ...$lq__event_badge_obj, ...preview_overrides } + : $lq__event_badge_obj + ); + // --- Effective display values (override ?? base) --- - // $derived keeps these reactive to liveQuery updates from the right panel saves. + // Use eff_badge (not lq__event_badge_obj) so preview changes reflect immediately. let display_name = $derived( - $lq__event_badge_obj?.full_name_override ?? $lq__event_badge_obj?.full_name ?? '' + eff_badge?.full_name_override ?? eff_badge?.full_name ?? '' ); let display_title = $derived( - $lq__event_badge_obj?.professional_title_override ?? $lq__event_badge_obj?.professional_title ?? '' + eff_badge?.professional_title_override ?? eff_badge?.professional_title ?? '' ); let display_affiliations = $derived( - $lq__event_badge_obj?.affiliations_override ?? $lq__event_badge_obj?.affiliations ?? '' + eff_badge?.affiliations_override ?? eff_badge?.affiliations ?? '' ); let display_location = $derived( - $lq__event_badge_obj?.location_override ?? $lq__event_badge_obj?.location ?? '' + eff_badge?.location_override ?? eff_badge?.location ?? '' ); // Effective badge type code — CSS class hook for per-event stylesheets. // Priority: badge_type_code_override → badge_type_code let effective_badge_type_code = $derived( - $lq__event_badge_obj?.badge_type_code_override ?? $lq__event_badge_obj?.badge_type_code ?? '' + eff_badge?.badge_type_code_override ?? eff_badge?.badge_type_code ?? '' ); // Human-readable badge type name — printed on the badge footer. @@ -101,9 +118,9 @@ // A special-case attendee can share a code (and CSS styling) with "Member" but have a // custom displayed name like "Life Member" stored in badge_type_override. let badge_type_name = $derived.by(() => { - const override_name = $lq__event_badge_obj?.badge_type_override; + const override_name = eff_badge?.badge_type_override; if (override_name) return override_name; - const base_name = $lq__event_badge_obj?.badge_type; + const base_name = eff_badge?.badge_type; if (base_name) return base_name; const found = (badge_type_code_li as { code: string; name: string }[]) .find(item => item.code === effective_badge_type_code); diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte index 9ae31c01..429c5513 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte @@ -13,6 +13,9 @@ * Font sizes flow back to the parent via $bindable() props so the badge render * (ae_comp__badge_obj_view.svelte) stays in sync without prop-drilling through * a third component. + * + * preview_overrides: merged on top of the saved badge object before passing to + * the badge render — gives real-time preview as the user types without saving. */ import type { key_val } from '$lib/stores/ae_stores'; @@ -34,6 +37,12 @@ font_size_affiliations?: number | null; /** Font size for location (px). null = auto. */ font_size_location?: number | null; + /** + * Real-time preview overrides. Set by this component while a field is open; + * the parent passes it straight to the badge render so the badge updates live + * as the user types — no save required. + */ + preview_overrides?: Record | null; log_lvl?: number; } @@ -46,6 +55,7 @@ font_size_title = $bindable(null), font_size_affiliations = $bindable(null), font_size_location = $bindable(null), + preview_overrides = $bindable(null), log_lvl = 0 }: Props = $props(); @@ -224,6 +234,46 @@ return $lq__event_badge_obj?.[override_key] ?? $lq__event_badge_obj?.[base_key] ?? ''; } + // Real-time preview: keep preview_overrides in sync with whatever field is currently + // open and being edited. The badge render merges this on top of the saved badge object + // so changes show up immediately as the user types. Clears when no field is open. + $effect(() => { + if (!active_field) { + preview_overrides = null; + return; + } + switch (active_field) { + case 'name': + preview_overrides = { full_name_override: edit_full_name_override || null }; + break; + case 'title': + preview_overrides = { professional_title_override: edit_professional_title_override || null }; + break; + case 'affiliations': + preview_overrides = { affiliations_override: edit_affiliations_override || null }; + break; + case 'location': + preview_overrides = { location_override: edit_location_override || null }; + break; + case 'pronouns': + preview_overrides = { pronouns_override: edit_pronouns_override || null }; + break; + case 'allow_tracking': + preview_overrides = { allow_tracking: edit_allow_tracking }; + break; + case 'badge_type': + preview_overrides = { + badge_type_code_override: edit_badge_type_code, + badge_type_override: edit_badge_type_code + ? (badge_type_code_li.find(item => item.code === edit_badge_type_code)?.name ?? edit_badge_type_code) + : null + }; + break; + default: + preview_overrides = null; + } + }); + // Effective badge type display name: priority matches ae_comp__badge_obj_view.svelte let badge_type_display = $derived( $lq__event_badge_obj?.badge_type_override @@ -770,4 +820,24 @@ .ctrl-accordion-inner { overflow: hidden; } + + /* ---- Entrance animation for form content ---- + Triggered each time .open is applied to the parent accordion. + Pairs with the height animation: content fades + zooms in from + slightly above while the card expands downward smoothly. + Similar in effect to tailwindcss-animate `animate-in zoom-in-95 fade-in-0 + slide-in-from-top-2`. Uses scoped @keyframes so no plugin needed. */ + .ctrl-accordion.open .ctrl-accordion-inner > div { + animation: field-form-enter 200ms ease-out both; + } + @keyframes field-form-enter { + from { + opacity: 0; + transform: scale(0.97) translateY(-5px); + } + to { + opacity: 1; + transform: none; + } + } diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte index 39906005..1b3af299 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte @@ -102,6 +102,11 @@ let font_size_title: number | null = $state(null); let font_size_affiliations: number | null = $state(null); let font_size_location: number | null = $state(null); + + // Real-time preview: the controls panel sets this to the current edit field + // value on every keystroke. The badge render merges it over the saved badge + // object so the user can see changes as they type, before saving. + let preview_overrides: Record | null = $state(null); @@ -243,6 +248,7 @@ On print: pr-0 restores full width, controls panel is hidden. -->
+
@@ -271,6 +278,7 @@ bind:font_size_title bind:font_size_affiliations bind:font_size_location + bind:preview_overrides />