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 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')} {#snippet font_ctrl(key: 'name' | 'title' | 'affiliations' | 'location')}
{@const cur = {@const cur =
key === 'name' ? font_size_name : key === 'name' ? font_size_name :
key === 'title' ? font_size_title : key === 'title' ? font_size_title :
key === 'affiliations' ? font_size_affiliations : font_size_location} 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" role="group"
aria-label="{key} font size controls" 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 <button
type="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)} onclick={() => font_size_adjust(key, -FONT_SIZE_STEP)}
disabled={cur !== null && cur <= FONT_SIZE_MIN[key]} disabled={cur !== null && cur <= FONT_SIZE_MIN[key]}
title="Decrease {key} font size" title="Smaller font"
aria-label="Decrease {key} font size" aria-label="Decrease {key} font size"
></button> ></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'} {cur !== null ? `${cur}px` : 'Auto'}
</span> </span>
<button <button
type="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)} onclick={() => font_size_adjust(key, FONT_SIZE_STEP)}
disabled={cur !== null && cur >= FONT_SIZE_MAX[key]} disabled={cur !== null && cur >= FONT_SIZE_MAX[key]}
title="Increase {key} font size" title="Larger font"
aria-label="Increase {key} font size" aria-label="Increase {key} font size"
>+</button> >+</button>
{#if cur !== null} {#if cur !== null}
<button <button
type="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)} onclick={() => font_size_reset(key)}
title="Reset {key} font size to auto" title="Reset to auto"
aria-label="Reset {key} font size to auto" aria-label="Reset {key} font size to auto"
></button> ></button>
{/if} {/if}
</div> </div>
{/snippet} {/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)} {#snippet field_actions(field_key: string, on_save: () => void, on_cancel: () => void)}
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2">
<button <button
@@ -292,7 +293,7 @@
class:preset-tonal-error={field_save_status[field_key] === 'error'} class:preset-tonal-error={field_save_status[field_key] === 'error'}
disabled={field_save_status[field_key] === 'saving'} disabled={field_save_status[field_key] === 'saving'}
onclick={on_save} onclick={on_save}
title="Save changes to this field" title="Save changes"
aria-label="Save changes" aria-label="Save changes"
> >
{#if field_save_status[field_key] === 'saving'} {#if field_save_status[field_key] === 'saving'}
@@ -309,7 +310,7 @@
type="button" type="button"
class="btn btn-sm preset-tonal-surface" class="btn btn-sm preset-tonal-surface"
onclick={on_cancel} onclick={on_cancel}
title="Cancel editing" title="Cancel"
aria-label="Cancel editing" aria-label="Cancel editing"
><X size="14" /></button> ><X size="14" /></button>
</div> </div>
@@ -319,31 +320,14 @@
Main panel Main panel
============================================================ --> ============================================================ -->
<!-- Identity card: quick visual confirmation of who this badge is for. <!-- Print button — canonical action: increments print_count, fires window.print(),
Helps volunteers confirm they have the right badge before printing. --> then navigates back to badge search. Trusted+ only; edit mode allows reprints. -->
{#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). -->
{#if can_print} {#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 <button
type="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-filled-primary={print_status === 'idle'}
class:preset-tonal-surface={print_status === 'loading'} class:preset-tonal-surface={print_status === 'loading'}
class:preset-filled-success={print_status === 'done'} class:preset-filled-success={print_status === 'done'}
@@ -372,27 +356,27 @@
</div> </div>
{/if} {/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-0.5 pb-0.5">Attendee info</p>
<p class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold px-1 pb-0.5">Attendee info</p>
<!-- === NAME === --> <!-- === NAME ===
<!-- Editable by Trusted+ only; all users have font controls --> Font controls: always visible (all can adjust for readability at kiosk).
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> Edit form: Trusted+ only — accordion opens only when is_trusted. -->
<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 === 'name'}>
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex-1 min-w-0"> <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')} {#if get_display('full_name_override', 'full_name')}
<p class="font-bold leading-snug truncate">{get_display('full_name_override', 'full_name')}</p> <p class="font-bold leading-snug truncate">{get_display('full_name_override', 'full_name')}</p>
{:else} {: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} {/if}
</div> </div>
{#if is_trusted} {#if is_trusted}
<button <button
type="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')} onclick={() => toggle_field('name')}
aria-expanded={active_field === 'name'} aria-expanded={active_field === 'name'}
aria-controls="field-form-name" aria-controls="field-form-name"
@@ -404,43 +388,48 @@
{/if} {/if}
</div> </div>
{@render font_ctrl('name')} {@render font_ctrl('name')}
{#if active_field === 'name' && is_trusted} <!-- Accordion drawer: grid-template-rows 0fr→1fr animates height smoothly.
<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"> Always in DOM, only opens if is_trusted (edit button present above). -->
<label for="ctrl-full-name" class="text-xs text-gray-500 block mb-1"> <div class="ctrl-accordion" class:open={active_field === 'name' && is_trusted}>
Name override <span class="text-gray-400">(empty = use original)</span> <div class="ctrl-accordion-inner">
</label> <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">
<input {#if is_global_edit_mode}
id="ctrl-full-name" <label for="ctrl-full-name" class="text-[10px] text-gray-400 block mb-1">
name="ctrl-full-name" Name override — empty clears back to original
type="text" </label>
class="input w-full" {/if}
bind:value={edit_full_name_override} <input
placeholder={$lq__event_badge_obj?.full_name ?? 'Full name'} id="ctrl-full-name"
/> name="ctrl-full-name"
{@render field_actions( type="text"
'name', class="input w-full"
() => save_field('name', { full_name_override: edit_full_name_override || null }), bind:value={edit_full_name_override}
() => cancel_field('name') 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> </div>
{/if} </div>
</div> </div>
<!-- === PROFESSIONAL TITLE === --> <!-- === PROFESSIONAL TITLE === -->
<!-- Editable by all authenticated users --> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'title'}>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="flex-1 min-w-0"> <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')} {#if get_display('professional_title_override', 'professional_title')}
<p class="leading-snug truncate">{get_display('professional_title_override', 'professional_title')}</p> <p class="leading-snug truncate">{get_display('professional_title_override', 'professional_title')}</p>
{:else} {: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} {/if}
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('title')}
aria-expanded={active_field === 'title'} aria-expanded={active_field === 'title'}
aria-controls="field-form-title" aria-controls="field-form-title"
@@ -451,43 +440,44 @@
</button> </button>
</div> </div>
{@render font_ctrl('title')} {@render font_ctrl('title')}
{#if active_field === 'title'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label for="ctrl-pro-title" class="text-xs text-gray-500 block mb-1"> <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">
Professional title override {#if is_global_edit_mode}
</label> <label for="ctrl-pro-title" class="text-[10px] text-gray-400 block mb-1">Professional title</label>
<input {/if}
id="ctrl-pro-title" <input
name="ctrl-pro-title" id="ctrl-pro-title"
type="text" name="ctrl-pro-title"
class="input w-full" type="text"
bind:value={edit_professional_title_override} class="input w-full"
placeholder={$lq__event_badge_obj?.professional_title ?? 'Professional title'} bind:value={edit_professional_title_override}
/> placeholder={$lq__event_badge_obj?.professional_title ?? 'Professional title'}
{@render field_actions( />
'title', {@render field_actions(
() => save_field('title', { professional_title_override: edit_professional_title_override || null }), 'title',
() => cancel_field('title') () => save_field('title', { professional_title_override: edit_professional_title_override || null }),
)} () => cancel_field('title')
)}
</div>
</div> </div>
{/if} </div>
</div> </div>
<!-- === AFFILIATIONS === --> <!-- === AFFILIATIONS === -->
<!-- Editable by all authenticated users; textarea (multi-line) --> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'affiliations'}>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="flex-1 min-w-0"> <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')} {#if get_display('affiliations_override', 'affiliations')}
<p class="leading-snug line-clamp-2">{get_display('affiliations_override', 'affiliations')}</p> <p class="leading-snug line-clamp-2">{get_display('affiliations_override', 'affiliations')}</p>
{:else} {: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} {/if}
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('affiliations')}
aria-expanded={active_field === 'affiliations'} aria-expanded={active_field === 'affiliations'}
aria-controls="field-form-affiliations" aria-controls="field-form-affiliations"
@@ -498,43 +488,44 @@
</button> </button>
</div> </div>
{@render font_ctrl('affiliations')} {@render font_ctrl('affiliations')}
{#if active_field === 'affiliations'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label for="ctrl-affiliations" class="text-xs text-gray-500 block mb-1"> <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">
Affiliations override {#if is_global_edit_mode}
</label> <label for="ctrl-affiliations" class="text-[10px] text-gray-400 block mb-1">Affiliations / organization</label>
<textarea {/if}
id="ctrl-affiliations" <textarea
name="ctrl-affiliations" id="ctrl-affiliations"
class="textarea w-full" name="ctrl-affiliations"
rows="2" class="textarea w-full"
bind:value={edit_affiliations_override} rows="2"
placeholder={$lq__event_badge_obj?.affiliations ?? 'Organization / affiliations'} bind:value={edit_affiliations_override}
></textarea> placeholder={$lq__event_badge_obj?.affiliations ?? 'Organization / affiliations'}
{@render field_actions( ></textarea>
'affiliations', {@render field_actions(
() => save_field('affiliations', { affiliations_override: edit_affiliations_override || null }), 'affiliations',
() => cancel_field('affiliations') () => save_field('affiliations', { affiliations_override: edit_affiliations_override || null }),
)} () => cancel_field('affiliations')
)}
</div>
</div> </div>
{/if} </div>
</div> </div>
<!-- === LOCATION === --> <!-- === LOCATION === -->
<!-- Editable by all authenticated users --> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'location'}>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="flex-1 min-w-0"> <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')} {#if get_display('location_override', 'location')}
<p class="leading-snug truncate">{get_display('location_override', 'location')}</p> <p class="leading-snug truncate">{get_display('location_override', 'location')}</p>
{:else} {: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} {/if}
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('location')}
aria-expanded={active_field === 'location'} aria-expanded={active_field === 'location'}
aria-controls="field-form-location" aria-controls="field-form-location"
@@ -545,86 +536,88 @@
</button> </button>
</div> </div>
{@render font_ctrl('location')} {@render font_ctrl('location')}
{#if active_field === 'location'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label for="ctrl-location" class="text-xs text-gray-500 block mb-1"> <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">
Location override {#if is_global_edit_mode}
</label> <label for="ctrl-location" class="text-[10px] text-gray-400 block mb-1">City / State / Country</label>
<input {/if}
id="ctrl-location" <input
name="ctrl-location" id="ctrl-location"
type="text" name="ctrl-location"
class="input w-full" type="text"
bind:value={edit_location_override} class="input w-full"
placeholder={$lq__event_badge_obj?.location ?? 'City, State / Country'} bind:value={edit_location_override}
/> placeholder={$lq__event_badge_obj?.location ?? 'City, State / Country'}
{@render field_actions( />
'location', {@render field_actions(
() => save_field('location', { location_override: edit_location_override || null }), 'location',
() => cancel_field('location') () => save_field('location', { location_override: edit_location_override || null }),
)} () => cancel_field('location')
)}
</div>
</div> </div>
{/if} </div>
</div> </div>
<!-- === ALLOW TRACKING === --> <!-- === ALLOW TRACKING (Lead Scanning) === -->
<!-- Editable by all authenticated users --> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'allow_tracking'}>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex items-start gap-2 px-3 pt-2 pb-1.5">
<div class="flex-1 min-w-0"> <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"> <p class="leading-snug text-xs">
{$lq__event_badge_obj?.allow_tracking ? 'Allowed' : 'Not allowed'} {$lq__event_badge_obj?.allow_tracking ? 'Allowed' : 'Not allowed'}
</p> </p>
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('allow_tracking')}
aria-expanded={active_field === 'allow_tracking'} aria-expanded={active_field === 'allow_tracking'}
aria-controls="field-form-allow-tracking" aria-controls="field-form-allow-tracking"
title="Edit lead scanning preference" 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} {#if active_field === 'allow_tracking'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button> </button>
</div> </div>
{#if active_field === 'allow_tracking'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label class="flex items-center gap-2 cursor-pointer select-none"> <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">
<input <label class="flex items-center gap-2 cursor-pointer select-none">
id="ctrl-allow-tracking" <input
name="ctrl-allow-tracking" id="ctrl-allow-tracking"
type="checkbox" name="ctrl-allow-tracking"
class="checkbox" type="checkbox"
bind:checked={edit_allow_tracking} class="checkbox"
/> bind:checked={edit_allow_tracking}
<span class="text-xs">Allow exhibitor lead scanning</span> />
</label> <span class="text-xs">Allow exhibitor lead scanning</span>
{@render field_actions( </label>
'allow_tracking', {@render field_actions(
() => save_field('allow_tracking', { allow_tracking: edit_allow_tracking }), 'allow_tracking',
() => cancel_field('allow_tracking') () => save_field('allow_tracking', { allow_tracking: edit_allow_tracking }),
)} () => cancel_field('allow_tracking')
)}
</div>
</div> </div>
{/if} </div>
</div> </div>
<!-- === PRONOUNS === --> <!-- === PRONOUNS === -->
<!-- Attendee-level field: shown to all authenticated users, not just staff --> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'pronouns'}>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex items-start gap-2 px-3 pt-2 pb-2">
<div class="flex-1 min-w-0"> <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')} {#if get_display('pronouns_override', 'pronouns')}
<p class="leading-snug truncate">{get_display('pronouns_override', 'pronouns')}</p> <p class="leading-snug truncate">{get_display('pronouns_override', 'pronouns')}</p>
{:else} {: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} {/if}
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('pronouns')}
aria-expanded={active_field === 'pronouns'} aria-expanded={active_field === 'pronouns'}
aria-controls="field-form-pronouns" aria-controls="field-form-pronouns"
@@ -634,56 +627,52 @@
{#if active_field === 'pronouns'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if} {#if active_field === 'pronouns'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button> </button>
</div> </div>
{#if active_field === 'pronouns'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label for="ctrl-pronouns" class="text-xs text-gray-500 block mb-1"> <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">
Pronouns override <input
</label> id="ctrl-pronouns"
<input name="ctrl-pronouns"
id="ctrl-pronouns" type="text"
name="ctrl-pronouns" class="input w-full"
type="text" bind:value={edit_pronouns_override}
class="input w-full" placeholder={$lq__event_badge_obj?.pronouns ?? 'e.g. they/them'}
bind:value={edit_pronouns_override} />
placeholder={$lq__event_badge_obj?.pronouns ?? 'e.g. they/them'} {@render field_actions(
/> 'pronouns',
{@render field_actions( () => save_field('pronouns', { pronouns_override: edit_pronouns_override || null }),
'pronouns', () => cancel_field('pronouns')
() => save_field('pronouns', { pronouns_override: edit_pronouns_override || null }), )}
() => cancel_field('pronouns') </div>
)}
</div> </div>
{/if} </div>
</div> </div>
<!-- === TRUSTED-ONLY FIELDS === --> <!-- === STAFF-ONLY FIELDS === -->
{#if is_trusted} {#if is_trusted}
<!-- Divider + label: visually separates attendee-editable fields from staff tools --> <!-- Divider between attendee and staff fields -->
<div class="pt-2 pb-1"> <div class="pt-1.5 pb-0.5 flex items-center gap-1.5">
<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>
<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>
Staff adjustments <span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
</p>
</div> </div>
<!-- === BADGE TYPE === --> <!-- === BADGE TYPE === (only when template defines badge_type_list) -->
<!-- Only shown when template has a badge_type_list defined -->
{#if badge_type_code_li.length > 0} {#if badge_type_code_li.length > 0}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'badge_type'}>
<div class="flex items-start gap-2 px-3 pt-2 pb-2"> <div class="flex items-center gap-2 px-2 py-1.5">
<div class="flex-1 min-w-0"> <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} {#if badge_type_display}
<p class="leading-snug truncate">{badge_type_display}</p> <p class="leading-snug truncate">{badge_type_display}</p>
{:else} {: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} {/if}
</div> </div>
<button <button
type="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')} onclick={() => toggle_field('badge_type')}
aria-expanded={active_field === 'badge_type'} aria-expanded={active_field === 'badge_type'}
aria-controls="field-form-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} {#if active_field === 'badge_type'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
</button> </button>
</div> </div>
{#if active_field === 'badge_type'} <div class="ctrl-accordion" class:open={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"> <div class="ctrl-accordion-inner">
<label for="ctrl-badge-type" class="text-xs text-gray-500 block mb-1"> <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">
Badge type override <select
</label> id="ctrl-badge-type"
<select name="ctrl-badge-type"
id="ctrl-badge-type" class="select w-full"
name="ctrl-badge-type" bind:value={edit_badge_type_code}
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={null}> Select badge type —</option> <option value={item.code}>{item.name}</option>
{#each badge_type_code_li as item (item.code)} {/each}
<option value={item.code}>{item.name}</option> </select>
{/each} {@render field_actions(
</select> 'badge_type',
{@render field_actions( () => save_field('badge_type', {
'badge_type', badge_type_code_override: edit_badge_type_code,
() => save_field('badge_type', { // Keep badge_type_override in sync with the name.
badge_type_code_override: edit_badge_type_code, // See PROJECT__AE_Events_Badges_Review_Print.md for edge-case notes.
// Keep badge_type_override in sync (name from template list). badge_type_override: edit_badge_type_code
// See edge-case note in PROJECT__AE_Events_Badges_Review_Print.md. ? (badge_type_code_li.find(item => item.code === edit_badge_type_code)?.name ?? edit_badge_type_code)
badge_type_override: edit_badge_type_code : null
? (badge_type_code_li.find(item => item.code === edit_badge_type_code)?.name ?? edit_badge_type_code) }),
: null () => cancel_field('badge_type')
}), )}
() => cancel_field('badge_type') </div>
)}
</div> </div>
{/if} </div>
</div> </div>
{/if} {/if}
{/if}<!-- end is_trusted --> {/if}<!-- end is_trusted -->
</div> </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 print_count = $derived($lq__event_badge_obj?.print_count ?? 0);
let is_printed = $derived(print_count >= 1); 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 { function build_review_url(): string {
// TODO: append ?passcode=... when person_passcode is added to the event_badge schema // 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`; 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> <span class="text-xs font-medium text-green-600 dark:text-green-400 whitespace-nowrap shrink-0">Ready</span>
{/if} {/if}
</div> </div>
{#if $events_loc?.title} {#if badge_type_display || $events_loc?.title}
<p class="text-xs text-gray-500 truncate">{$events_loc.title}</p> <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} {:else if is_printed && $lq__event_badge_obj.print_last_datetime}
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
Last printed {new Date($lq__event_badge_obj.print_last_datetime).toLocaleString()} Last printed {new Date($lq__event_badge_obj.print_last_datetime).toLocaleString()}