Badges: live preview while typing, accordion entrance animation, register tailwindcss-animate

This commit is contained in:
Scott Idem
2026-03-12 14:54:26 -04:00
parent 961b05c5e4
commit 5f6f1b408b
4 changed files with 104 additions and 8 deletions

View File

@@ -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),

View File

@@ -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<string, any> | 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);

View File

@@ -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<string, any> | 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;
}
}
</style>

View File

@@ -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<string, any> | null = $state(null);
</script>
<svelte:head>
@@ -243,6 +248,7 @@
On print: pr-0 restores full width, controls panel is hidden. -->
<div class="print:pr-0 pr-64">
<!-- null → Element_fit_text auto-scales; a number → manual size override from controls. -->
<!-- preview_overrides: non-null while a field card is open in controls — gives live preview. -->
<Comp_badge_obj_view_v2
event_id={$lq__event_badge_obj.event_id as string}
event_badge_id={event_badge_id as string}
@@ -253,6 +259,7 @@
font_size_title={font_size_title}
font_size_affiliations={font_size_affiliations}
font_size_location={font_size_location}
{preview_overrides}
/>
</div>
@@ -271,6 +278,7 @@
bind:font_size_title
bind:font_size_affiliations
bind:font_size_location
bind:preview_overrides
/>
</div>