Badges: controls panel UX polish — remove identity card, elevate print btn, style buttons

This commit is contained in:
Scott Idem
2026-03-12 14:46:08 -04:00
parent b92c0bdcf1
commit 961b05c5e4
2 changed files with 289 additions and 235 deletions

View File

@@ -239,49 +239,50 @@
Field card snippets
============================================================ -->
<!-- Reusable font size controls row -->
<!-- Compact font size row. "Font" text label removed (sr-only for a11y).
Only +//value/reset visible — saves vertical space in the tight panel. -->
{#snippet font_ctrl(key: 'name' | 'title' | 'affiliations' | 'location')}
{@const cur =
key === 'name' ? font_size_name :
key === 'title' ? font_size_title :
key === 'affiliations' ? font_size_affiliations : font_size_location}
<div class="flex items-center gap-1 border-t border-gray-100 dark:border-gray-800 px-3 py-1.5"
<div class="flex items-center gap-1 border-t border-gray-100 dark:border-gray-800 px-2 py-1"
role="group"
aria-label="{key} font size controls"
>
<span class="text-[10px] text-gray-400 uppercase w-8 shrink-0">Font</span>
<span class="sr-only">Font size for {key}:</span>
<button
type="button"
class="btn btn-xs px-2 min-h-0 h-7 text-base leading-none"
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
onclick={() => font_size_adjust(key, -FONT_SIZE_STEP)}
disabled={cur !== null && cur <= FONT_SIZE_MIN[key]}
title="Decrease {key} font size"
title="Smaller font"
aria-label="Decrease {key} font size"
></button>
<span class="font-mono text-xs w-12 text-center tabular-nums" aria-live="polite">
<span class="font-mono text-[11px] w-9 text-center tabular-nums text-gray-500" aria-live="polite">
{cur !== null ? `${cur}px` : 'Auto'}
</span>
<button
type="button"
class="btn btn-xs px-2 min-h-0 h-7 text-base leading-none"
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
onclick={() => font_size_adjust(key, FONT_SIZE_STEP)}
disabled={cur !== null && cur >= FONT_SIZE_MAX[key]}
title="Increase {key} font size"
title="Larger font"
aria-label="Increase {key} font size"
>+</button>
{#if cur !== null}
<button
type="button"
class="btn btn-xs px-1.5 min-h-0 h-7 opacity-60"
class="btn btn-xs preset-tonal-surface px-1.5 min-h-0 h-6 opacity-60"
onclick={() => font_size_reset(key)}
title="Reset {key} font size to auto"
title="Reset to auto"
aria-label="Reset {key} font size to auto"
></button>
{/if}
</div>
{/snippet}
<!-- Reusable save/cancel buttons (used inside accordion edit forms) -->
<!-- Save/cancel row (inside accordion edit forms) -->
{#snippet field_actions(field_key: string, on_save: () => void, on_cancel: () => void)}
<div class="flex gap-2 mt-2">
<button
@@ -292,7 +293,7 @@
class:preset-tonal-error={field_save_status[field_key] === 'error'}
disabled={field_save_status[field_key] === 'saving'}
onclick={on_save}
title="Save changes to this field"
title="Save changes"
aria-label="Save changes"
>
{#if field_save_status[field_key] === 'saving'}
@@ -309,7 +310,7 @@
type="button"
class="btn btn-sm preset-tonal-surface"
onclick={on_cancel}
title="Cancel editing"
title="Cancel"
aria-label="Cancel editing"
><X size="14" /></button>
</div>
@@ -319,31 +320,14 @@
Main panel
============================================================ -->
<!-- Identity card: quick visual confirmation of who this badge is for.
Helps volunteers confirm they have the right badge before printing. -->
{#if $lq__event_badge_obj}
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-1">Badge Station</p>
<p class="font-bold text-sm leading-snug truncate">
{$lq__event_badge_obj.full_name_override ?? $lq__event_badge_obj.full_name ?? '—'}
</p>
{#if badge_type_display}
<p class="text-xs text-gray-500 mt-0.5">{badge_type_display}</p>
{/if}
<p class="text-[10px] font-mono text-gray-300 dark:text-gray-600 mt-0.5 uppercase tracking-wider">
#{$lq__event_badge_obj.event_badge_id}
</p>
</div>
{/if}
<!-- Print button — canonical print action. Increments print_count, fires
window.print(), then navigates back to badge search. Only shown to Trusted+
when not yet printed, OR when global AE Edit Mode is active (allows reprints). -->
<!-- Print button — canonical action: increments print_count, fires window.print(),
then navigates back to badge search. Trusted+ only; edit mode allows reprints. -->
{#if can_print}
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<!-- Tinted card visually lifts the print action above the field list -->
<div class="mb-3 p-2 rounded-lg bg-primary-50/40 dark:bg-primary-950/20 border border-primary-200/50 dark:border-primary-800/30">
<button
type="button"
class="btn btn-sm w-full flex items-center justify-center gap-2"
class="btn w-full flex items-center justify-center gap-2"
class:preset-filled-primary={print_status === 'idle'}
class:preset-tonal-surface={print_status === 'loading'}
class:preset-filled-success={print_status === 'done'}
@@ -372,27 +356,27 @@
</div>
{/if}
<div class="space-y-2 text-sm">
<div class="space-y-1.5 text-sm">
<!-- Section header: fields the attendee can review/edit at the kiosk -->
<p class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold px-1 pb-0.5">Attendee info</p>
<p class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold px-0.5 pb-0.5">Attendee info</p>
<!-- === NAME === -->
<!-- Editable by Trusted+ only; all users have font controls -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<!-- === NAME ===
Font controls: always visible (all can adjust for readability at kiosk).
Edit form: Trusted+ only — accordion opens only when is_trusted. -->
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'name'}>
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Name</p>
<p class="field-label">Name</p>
{#if get_display('full_name_override', 'full_name')}
<p class="font-bold leading-snug truncate">{get_display('full_name_override', 'full_name')}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set{is_trusted ? ' — tap ✎ to add' : ''}</p>
<p class="text-gray-400 italic text-xs">{is_trusted ? 'Tap ✎ to add' : 'Not set'}</p>
{/if}
</div>
{#if is_trusted}
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('name')}
aria-expanded={active_field === 'name'}
aria-controls="field-form-name"
@@ -404,43 +388,48 @@
{/if}
</div>
{@render font_ctrl('name')}
{#if active_field === 'name' && is_trusted}
<div id="field-form-name" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-full-name" class="text-xs text-gray-500 block mb-1">
Name override <span class="text-gray-400">(empty = use original)</span>
</label>
<input
id="ctrl-full-name"
name="ctrl-full-name"
type="text"
class="input w-full"
bind:value={edit_full_name_override}
placeholder={$lq__event_badge_obj?.full_name ?? 'Full name'}
/>
{@render field_actions(
'name',
() => save_field('name', { full_name_override: edit_full_name_override || null }),
() => cancel_field('name')
)}
<!-- Accordion drawer: grid-template-rows 0fr→1fr animates height smoothly.
Always in DOM, only opens if is_trusted (edit button present above). -->
<div class="ctrl-accordion" class:open={active_field === 'name' && is_trusted}>
<div class="ctrl-accordion-inner">
<div id="field-form-name" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
{#if is_global_edit_mode}
<label for="ctrl-full-name" class="text-[10px] text-gray-400 block mb-1">
Name override — empty clears back to original
</label>
{/if}
<input
id="ctrl-full-name"
name="ctrl-full-name"
type="text"
class="input w-full"
bind:value={edit_full_name_override}
placeholder={$lq__event_badge_obj?.full_name ?? 'Full name'}
/>
{@render field_actions(
'name',
() => save_field('name', { full_name_override: edit_full_name_override || null }),
() => cancel_field('name')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === PROFESSIONAL TITLE === -->
<!-- Editable by all authenticated users -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'title'}>
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Professional Title</p>
<p class="field-label">Title</p>
{#if get_display('professional_title_override', 'professional_title')}
<p class="leading-snug truncate">{get_display('professional_title_override', 'professional_title')}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to add</p>
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('title')}
aria-expanded={active_field === 'title'}
aria-controls="field-form-title"
@@ -451,43 +440,44 @@
</button>
</div>
{@render font_ctrl('title')}
{#if active_field === 'title'}
<div id="field-form-title" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-pro-title" class="text-xs text-gray-500 block mb-1">
Professional title override
</label>
<input
id="ctrl-pro-title"
name="ctrl-pro-title"
type="text"
class="input w-full"
bind:value={edit_professional_title_override}
placeholder={$lq__event_badge_obj?.professional_title ?? 'Professional title'}
/>
{@render field_actions(
'title',
() => save_field('title', { professional_title_override: edit_professional_title_override || null }),
() => cancel_field('title')
)}
<div class="ctrl-accordion" class:open={active_field === 'title'}>
<div class="ctrl-accordion-inner">
<div id="field-form-title" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
{#if is_global_edit_mode}
<label for="ctrl-pro-title" class="text-[10px] text-gray-400 block mb-1">Professional title</label>
{/if}
<input
id="ctrl-pro-title"
name="ctrl-pro-title"
type="text"
class="input w-full"
bind:value={edit_professional_title_override}
placeholder={$lq__event_badge_obj?.professional_title ?? 'Professional title'}
/>
{@render field_actions(
'title',
() => save_field('title', { professional_title_override: edit_professional_title_override || null }),
() => cancel_field('title')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === AFFILIATIONS === -->
<!-- Editable by all authenticated users; textarea (multi-line) -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'affiliations'}>
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Affiliations</p>
<p class="field-label">Affiliations</p>
{#if get_display('affiliations_override', 'affiliations')}
<p class="leading-snug line-clamp-2">{get_display('affiliations_override', 'affiliations')}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to add</p>
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('affiliations')}
aria-expanded={active_field === 'affiliations'}
aria-controls="field-form-affiliations"
@@ -498,43 +488,44 @@
</button>
</div>
{@render font_ctrl('affiliations')}
{#if active_field === 'affiliations'}
<div id="field-form-affiliations" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-affiliations" class="text-xs text-gray-500 block mb-1">
Affiliations override
</label>
<textarea
id="ctrl-affiliations"
name="ctrl-affiliations"
class="textarea w-full"
rows="2"
bind:value={edit_affiliations_override}
placeholder={$lq__event_badge_obj?.affiliations ?? 'Organization / affiliations'}
></textarea>
{@render field_actions(
'affiliations',
() => save_field('affiliations', { affiliations_override: edit_affiliations_override || null }),
() => cancel_field('affiliations')
)}
<div class="ctrl-accordion" class:open={active_field === 'affiliations'}>
<div class="ctrl-accordion-inner">
<div id="field-form-affiliations" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
{#if is_global_edit_mode}
<label for="ctrl-affiliations" class="text-[10px] text-gray-400 block mb-1">Affiliations / organization</label>
{/if}
<textarea
id="ctrl-affiliations"
name="ctrl-affiliations"
class="textarea w-full"
rows="2"
bind:value={edit_affiliations_override}
placeholder={$lq__event_badge_obj?.affiliations ?? 'Organization / affiliations'}
></textarea>
{@render field_actions(
'affiliations',
() => save_field('affiliations', { affiliations_override: edit_affiliations_override || null }),
() => cancel_field('affiliations')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === LOCATION === -->
<!-- Editable by all authenticated users -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'location'}>
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Location</p>
<p class="field-label">Location</p>
{#if get_display('location_override', 'location')}
<p class="leading-snug truncate">{get_display('location_override', 'location')}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to add</p>
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('location')}
aria-expanded={active_field === 'location'}
aria-controls="field-form-location"
@@ -545,86 +536,88 @@
</button>
</div>
{@render font_ctrl('location')}
{#if active_field === 'location'}
<div id="field-form-location" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-location" class="text-xs text-gray-500 block mb-1">
Location override
</label>
<input
id="ctrl-location"
name="ctrl-location"
type="text"
class="input w-full"
bind:value={edit_location_override}
placeholder={$lq__event_badge_obj?.location ?? 'City, State / Country'}
/>
{@render field_actions(
'location',
() => save_field('location', { location_override: edit_location_override || null }),
() => cancel_field('location')
)}
<div class="ctrl-accordion" class:open={active_field === 'location'}>
<div class="ctrl-accordion-inner">
<div id="field-form-location" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
{#if is_global_edit_mode}
<label for="ctrl-location" class="text-[10px] text-gray-400 block mb-1">City / State / Country</label>
{/if}
<input
id="ctrl-location"
name="ctrl-location"
type="text"
class="input w-full"
bind:value={edit_location_override}
placeholder={$lq__event_badge_obj?.location ?? 'City, State / Country'}
/>
{@render field_actions(
'location',
() => save_field('location', { location_override: edit_location_override || null }),
() => cancel_field('location')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === ALLOW TRACKING === -->
<!-- Editable by all authenticated users -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<!-- === ALLOW TRACKING (Lead Scanning) === -->
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'allow_tracking'}>
<div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Lead Scanning</p>
<p class="field-label">Lead Scanning</p>
<p class="leading-snug text-xs">
{$lq__event_badge_obj?.allow_tracking ? 'Allowed' : 'Not allowed'}
</p>
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('allow_tracking')}
aria-expanded={active_field === 'allow_tracking'}
aria-controls="field-form-allow-tracking"
title="Edit lead scanning preference"
aria-label="Edit lead scanning preference"
aria-label="Edit lead scanning"
>
{#if active_field === 'allow_tracking'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button>
</div>
{#if active_field === 'allow_tracking'}
<div id="field-form-allow-tracking" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input
id="ctrl-allow-tracking"
name="ctrl-allow-tracking"
type="checkbox"
class="checkbox"
bind:checked={edit_allow_tracking}
/>
<span class="text-xs">Allow exhibitor lead scanning</span>
</label>
{@render field_actions(
'allow_tracking',
() => save_field('allow_tracking', { allow_tracking: edit_allow_tracking }),
() => cancel_field('allow_tracking')
)}
<div class="ctrl-accordion" class:open={active_field === 'allow_tracking'}>
<div class="ctrl-accordion-inner">
<div id="field-form-allow-tracking" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input
id="ctrl-allow-tracking"
name="ctrl-allow-tracking"
type="checkbox"
class="checkbox"
bind:checked={edit_allow_tracking}
/>
<span class="text-xs">Allow exhibitor lead scanning</span>
</label>
{@render field_actions(
'allow_tracking',
() => save_field('allow_tracking', { allow_tracking: edit_allow_tracking }),
() => cancel_field('allow_tracking')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === PRONOUNS === -->
<!-- Attendee-level field: shown to all authenticated users, not just staff -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-2">
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'pronouns'}>
<div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Pronouns</p>
<p class="field-label">Pronouns</p>
{#if get_display('pronouns_override', 'pronouns')}
<p class="leading-snug truncate">{get_display('pronouns_override', 'pronouns')}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to add</p>
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('pronouns')}
aria-expanded={active_field === 'pronouns'}
aria-controls="field-form-pronouns"
@@ -634,56 +627,52 @@
{#if active_field === 'pronouns'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button>
</div>
{#if active_field === 'pronouns'}
<div id="field-form-pronouns" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-pronouns" class="text-xs text-gray-500 block mb-1">
Pronouns override
</label>
<input
id="ctrl-pronouns"
name="ctrl-pronouns"
type="text"
class="input w-full"
bind:value={edit_pronouns_override}
placeholder={$lq__event_badge_obj?.pronouns ?? 'e.g. they/them'}
/>
{@render field_actions(
'pronouns',
() => save_field('pronouns', { pronouns_override: edit_pronouns_override || null }),
() => cancel_field('pronouns')
)}
<div class="ctrl-accordion" class:open={active_field === 'pronouns'}>
<div class="ctrl-accordion-inner">
<div id="field-form-pronouns" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<input
id="ctrl-pronouns"
name="ctrl-pronouns"
type="text"
class="input w-full"
bind:value={edit_pronouns_override}
placeholder={$lq__event_badge_obj?.pronouns ?? 'e.g. they/them'}
/>
{@render field_actions(
'pronouns',
() => save_field('pronouns', { pronouns_override: edit_pronouns_override || null }),
() => cancel_field('pronouns')
)}
</div>
</div>
{/if}
</div>
</div>
<!-- === TRUSTED-ONLY FIELDS === -->
<!-- === STAFF-ONLY FIELDS === -->
{#if is_trusted}
<!-- Divider + label: visually separates attendee-editable fields from staff tools -->
<div class="pt-2 pb-1">
<p class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold px-1 flex items-center gap-1">
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
Staff adjustments
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
</p>
<!-- Divider between attendee and staff fields -->
<div class="pt-1.5 pb-0.5 flex items-center gap-1.5">
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
<span class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold whitespace-nowrap">Staff adjustments</span>
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
</div>
<!-- === BADGE TYPE === -->
<!-- Only shown when template has a badge_type_list defined -->
<!-- === BADGE TYPE === (only when template defines badge_type_list) -->
{#if badge_type_code_li.length > 0}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div class="flex items-start gap-2 px-3 pt-2 pb-2">
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'badge_type'}>
<div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Badge Type</p>
<p class="field-label">Badge Type</p>
{#if badge_type_display}
<p class="leading-snug truncate">{badge_type_display}</p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to assign</p>
<p class="text-gray-400 italic text-xs">Tap ✎ to assign</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
onclick={() => toggle_field('badge_type')}
aria-expanded={active_field === 'badge_type'}
aria-controls="field-form-badge-type"
@@ -693,39 +682,92 @@
{#if active_field === 'badge_type'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button>
</div>
{#if active_field === 'badge_type'}
<div id="field-form-badge-type" class="px-3 pb-3 pt-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<label for="ctrl-badge-type" class="text-xs text-gray-500 block mb-1">
Badge type override
</label>
<select
id="ctrl-badge-type"
name="ctrl-badge-type"
class="select w-full"
bind:value={edit_badge_type_code}
>
<option value={null}> Select badge type —</option>
{#each badge_type_code_li as item (item.code)}
<option value={item.code}>{item.name}</option>
{/each}
</select>
{@render field_actions(
'badge_type',
() => save_field('badge_type', {
badge_type_code_override: edit_badge_type_code,
// Keep badge_type_override in sync (name from template list).
// See edge-case note in PROJECT__AE_Events_Badges_Review_Print.md.
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
}),
() => cancel_field('badge_type')
)}
<div class="ctrl-accordion" class:open={active_field === 'badge_type'}>
<div class="ctrl-accordion-inner">
<div id="field-form-badge-type" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<select
id="ctrl-badge-type"
name="ctrl-badge-type"
class="select w-full"
bind:value={edit_badge_type_code}
>
<option value={null}> Select badge type —</option>
{#each badge_type_code_li as item (item.code)}
<option value={item.code}>{item.name}</option>
{/each}
</select>
{@render field_actions(
'badge_type',
() => save_field('badge_type', {
badge_type_code_override: edit_badge_type_code,
// Keep badge_type_override in sync with the name.
// See PROJECT__AE_Events_Badges_Review_Print.md for edge-case notes.
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
}),
() => cancel_field('badge_type')
)}
</div>
</div>
{/if}
</div>
</div>
{/if}
{/if}<!-- end is_trusted -->
</div>
<style>
/* ---- Field card: visual "zoom" (elevation + ring) on the active card ----
box-shadow is used so the effect doesn't affect layout (no reflow).
The ring is rendered as an outer glow — does NOT require removing overflow:hidden.
transition-* properties animate the change smoothly when toggling. */
.field-card {
border: 1px solid rgb(229 231 235); /* gray-200 */
transition:
border-color 160ms ease-out,
box-shadow 160ms ease-out;
}
:global(.dark) .field-card {
border-color: rgb(55 65 81); /* gray-700 */
}
.field-card--active {
border-color: rgb(99 102 241 / 0.45); /* indigo-500, muted */
box-shadow:
0 0 0 3px rgb(99 102 241 / 0.18), /* soft outer glow — the "zoom ring" */
0 4px 14px rgb(0 0 0 / 0.11); /* elevation shadow */
}
:global(.dark) .field-card--active {
border-color: rgb(129 140 248 / 0.45); /* indigo-400 */
box-shadow:
0 0 0 3px rgb(129 140 248 / 0.14),
0 4px 14px rgb(0 0 0 / 0.35);
}
/* Compact section label used on each field row header */
.field-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgb(156 163 175); /* gray-400 */
font-weight: 600;
line-height: 1.2;
}
/* ---- Smooth accordion: CSS grid-template-rows trick ----
0fr → the row collapses to 0 (hidden).
1fr → the row expands to its natural content height.
The inner wrapper needs overflow:hidden so min-content is truly 0 when collapsed. */
.ctrl-accordion {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 185ms ease-out;
}
.ctrl-accordion.open {
grid-template-rows: 1fr;
}
.ctrl-accordion-inner {
overflow: hidden;
}
</style>

View File

@@ -58,6 +58,16 @@
let print_count = $derived($lq__event_badge_obj?.print_count ?? 0);
let is_printed = $derived(print_count >= 1);
// Badge type for header — simplified chain (no template list lookup).
// Full lookup with template list is in ae_comp__badge_print_controls.
let badge_type_display = $derived(
$lq__event_badge_obj?.badge_type_override
?? $lq__event_badge_obj?.badge_type
?? $lq__event_badge_obj?.badge_type_code_override
?? $lq__event_badge_obj?.badge_type_code
?? ''
);
function build_review_url(): string {
// TODO: append ?passcode=... when person_passcode is added to the event_badge schema
return `/events/${$lq__event_badge_obj?.event_id}/badges/${$lq__event_badge_obj?.event_badge_id}/review`;
@@ -168,8 +178,10 @@
<span class="text-xs font-medium text-green-600 dark:text-green-400 whitespace-nowrap shrink-0">Ready</span>
{/if}
</div>
{#if $events_loc?.title}
<p class="text-xs text-gray-500 truncate">{$events_loc.title}</p>
{#if badge_type_display || $events_loc?.title}
<p class="text-xs text-gray-500 truncate leading-tight">
{badge_type_display}{badge_type_display && $events_loc?.title ? ' · ' : ''}{$events_loc?.title ?? ''}
</p>
{:else if is_printed && $lq__event_badge_obj.print_last_datetime}
<p class="text-xs text-gray-400">
Last printed {new Date($lq__event_badge_obj.print_last_datetime).toLocaleString()}