Badges: retire v1 render, badge print page kiosk UX improvements

- ae_comp__badge_obj_view.svelte (v1) removed — replaced by v2 everywhere
- print/+page.svelte: always uses v2, removed v1/v2 toggle
  Header redesigned for kiosk: name + Ready/Printed×N status chip,
  event title subtitle. Re-print shortcut only in trusted+edit_mode.
  Duplicate 'Print Now' header button removed (canonical is in right panel).
- ae_comp__badge_print_controls.svelte:
  Identity card added at top (name, badge type, badge ID).
  Pronouns moved to attendee-level section (was staff-only).
  'Staff adjustments' divider added before badge type section.
  Attendee info section header label added.
- ae_comp__badge_obj_view_v2.svelte: debug JSON blocks gated behind edit_mode.
- print_list/+page.svelte: updated import to v2.
This commit is contained in:
Scott Idem
2026-03-12 14:19:58 -04:00
parent c7063806b7
commit b92c0bdcf1
6 changed files with 147 additions and 1779 deletions

View File

@@ -110,20 +110,24 @@ Print page edit access needs to be opened to attendee-level permissions, not jus
The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte` The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte`
should be the reference. See Design Intent section above. should be the reference. See Design Intent section above.
### TASK 4.1: Auto-Scaling Badge Text v2 — IN PROGRESS (2026-03-12) ### TASK 4.1: Auto-Scaling Badge Text v2 — COMPLETE (2026-03-12)
**Files created:** **Files created/updated:**
- `src/lib/elements/action_fit_text.ts` — Svelte action: binary-search font scaling with - `src/lib/elements/action_fit_text.ts` — Svelte action
MutationObserver + ResizeObserver + requestAnimationFrame. - `src/lib/elements/element_fit_text.svelte` — Component wrapper
- `src/lib/elements/element_fit_text.svelte` — Component wrapper. Key prop: `height` (required - `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render (canonical)
for binary search to work — without it, offsetHeight == scrollHeight always). Debug blocks gated behind `$ae_loc.edit_mode` (hidden in production).
- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render using - `print/+page.svelte` — Always uses v2 now. v1/v2 toggle removed. Header redesigned for kiosk UX.
Element_fit_text for name/title/affiliations/location in display mode. - `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
`fit_heights` derived object provides layout-aware heights per field per badge layout. "Staff adjustments" divider before badge_type field.
`font_size_*` props default to `undefined` (auto-scale) rather than numeric defaults (v1 behavior). - `print_list/+page.svelte` — Updated to import v2.
Manual overrides from print controls still work — any number disables auto-scale for that field. - `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/gemini_trash/**
**Toggle:** `v1`/`v2` button in print page header. V1 preserved as fallback. **Kiosk UX improvements (2026-03-12):**
**Status:** Working — heights in `fit_heights` still need visual tuning with real badge stock. - Print page header: cleaner, shows name + "Ready"/"Printed N×" status chip, event name.
Header Print Now button removed (duplicate); only Re-print shortcut visible in trusted+edit mode.
- Controls right panel: identity card at top confirms who the badge belongs to before printing.
Pronouns field is now an attendee-level field (was trusted-only). Staff section labelled.
- Debug JSON blocks in v2 badge render hidden behind global edit_mode flag.
### ✅ TASK 3: Badge Print Controls Panel — COMPLETE (2026-03-02) ### ✅ TASK 3: Badge Print Controls Panel — COMPLETE (2026-03-02)

View File

@@ -929,21 +929,15 @@
<!-- End if for lq__event_badge_obj && lq__event_badge_template_obj --> <!-- End if for lq__event_badge_obj && lq__event_badge_template_obj -->
</section> </section>
<div> {#if $ae_loc.edit_mode}
<div class="print:hidden">
<h1 class="text-lg font-bold mt-4">Debug Information</h1> <h1 class="text-lg font-bold mt-4">Debug Information</h1>
<pre <pre
class="whitespace-pre-wrap break-words text-xs max-h-32 overflow-auto p-2 bg-surface-200 border border-surface-300 rounded class="whitespace-pre-wrap break-words text-xs max-h-32 overflow-auto p-2 bg-surface-200 border border-surface-300 rounded mt-4"
mt-4 >{JSON.stringify($lq__event_badge_obj, null, 2)}</pre>
print:hidden
">
{JSON.stringify($lq__event_badge_obj, null, 2)}
</pre>
<pre <pre
class="whitespace-pre-wrap break-words text-xs max-h-32 overflow-auto p-2 bg-surface-200 border border-surface-300 rounded class="whitespace-pre-wrap break-words text-xs max-h-32 overflow-auto p-2 bg-surface-200 border border-surface-300 rounded mt-4"
mt-4 >{JSON.stringify($lq__event_badge_template_obj, null, 2)}</pre>
print:hidden
">
{JSON.stringify($lq__event_badge_template_obj, null, 2)}
</pre>
</div> </div>
{/if}

View File

@@ -319,11 +319,26 @@
Main panel Main panel
============================================================ --> ============================================================ -->
<!-- Print button — canonical print action for v2. Increments print_count, fires <!-- 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+ 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). when not yet printed, OR when global AE Edit Mode is active (allows reprints). -->
The header "Print Now" button is a shortcut that calls window.print() only —
it does NOT track print count. Always use this button for the official print. -->
{#if can_print} {#if can_print}
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700"> <div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<button <button
@@ -359,6 +374,9 @@
<div class="space-y-2 text-sm"> <div class="space-y-2 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>
<!-- === NAME === --> <!-- === NAME === -->
<!-- Editable by Trusted+ only; all users have font controls --> <!-- 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="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
@@ -592,52 +610,62 @@
{/if} {/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="flex-1 min-w-0">
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">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>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
onclick={() => toggle_field('pronouns')}
aria-expanded={active_field === 'pronouns'}
aria-controls="field-form-pronouns"
title="Edit pronouns"
aria-label="Edit pronouns"
>
{#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>
{/if}
</div>
<!-- === TRUSTED-ONLY FIELDS === --> <!-- === TRUSTED-ONLY FIELDS === -->
{#if is_trusted} {#if is_trusted}
<!-- === PRONOUNS === --> <!-- Divider + label: visually separates attendee-editable fields from staff tools -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"> <div class="pt-2 pb-1">
<div class="flex items-start gap-2 px-3 pt-2 pb-2"> <p class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold px-1 flex items-center gap-1">
<div class="flex-1 min-w-0"> <span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
<p class="text-[10px] uppercase tracking-widest text-gray-400 font-semibold mb-0.5">Pronouns</p> Staff adjustments
{#if get_display('pronouns_override', 'pronouns')} <span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
<p class="leading-snug truncate">{get_display('pronouns_override', 'pronouns')}</p> </p>
{:else}
<p class="text-gray-400 italic text-xs">Not set — tap ✎ to add</p>
{/if}
</div>
<button
type="button"
class="btn btn-xs preset-tonal-surface shrink-0 mt-0.5"
onclick={() => toggle_field('pronouns')}
aria-expanded={active_field === 'pronouns'}
aria-controls="field-form-pronouns"
title="Edit pronouns"
aria-label="Edit pronouns"
>
{#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>
{/if}
</div> </div>
<!-- === BADGE TYPE === --> <!-- === BADGE TYPE === -->

View File

@@ -16,14 +16,9 @@
import { page } from '$app/state'; import { page } from '$app/state';
import { ArrowLeft, Eye, LoaderCircle, Mail, Printer } from 'lucide-svelte'; import { ArrowLeft, Eye, LoaderCircle, Mail, Printer } from 'lucide-svelte';
import Comp_badge_obj_view from '../ae_comp__badge_obj_view.svelte';
import Comp_badge_obj_view_v2 from '../ae_comp__badge_obj_view_v2.svelte'; import Comp_badge_obj_view_v2 from '../ae_comp__badge_obj_view_v2.svelte';
import Comp_badge_print_controls from '../ae_comp__badge_print_controls.svelte'; import Comp_badge_print_controls from '../ae_comp__badge_print_controls.svelte';
// V2 toggle: temporary — lets staff compare auto-scaling text (v2) vs heuristic sizing (v1).
// Remove once v2 is verified and v1 is retired.
let use_v2 = $state(false);
let event_badge_id = $derived(page.params.badge_id); let event_badge_id = $derived(page.params.badge_id);
let event_id = $derived(page.params.event_id); let event_id = $derived(page.params.event_id);
@@ -89,11 +84,7 @@
alert(`PLACEHOLDER: An email will be sent to ${name} at ${email}. Use that link to review your ${event_name} badge.`); alert(`PLACEHOLDER: An email will be sent to ${name} at ${email}. Use that link to review your ${event_name} badge.`);
} }
// Print mode: display only — ae_comp__badge_obj_view handles print count + window.print() // Font size overrides (px). null = auto-sizing (Element_fit_text binary search).
// The "Print Now" header button below only triggers window.print() for convenience;
// print count is incremented by the print button inside ae_comp__badge_obj_view.
// Font size overrides (px). null = auto-sizing from ae_comp__badge_obj_view.
// Controls live in Comp_badge_print_controls (right panel) via $bindable(). // Controls live in Comp_badge_print_controls (right panel) via $bindable().
// Constants and adjust logic are defined there; only the state lives here so // Constants and adjust logic are defined there; only the state lives here so
// the values can be forwarded to both the controls and the badge render. // the values can be forwarded to both the controls and the badge render.
@@ -150,10 +141,10 @@
{#if $lq__event_badge_obj && $lq__event_badge_obj.event_id && event_badge_id} {#if $lq__event_badge_obj && $lq__event_badge_obj.event_id && event_badge_id}
<!-- Print chrome: hidden when browser prints (print:hidden) --> <!-- Kiosk header: screen-only chrome. Minimal — the badge render is the focus. -->
<header class="print:hidden w-full flex flex-row flex-wrap gap-2 items-center justify-between border-b border-gray-300 mb-3 pb-2"> <header class="print:hidden w-full flex flex-row flex-wrap gap-2 items-center justify-between border-b border-gray-300 mb-3 pb-2">
<!-- Left: Back to Search + name + already-printed warning --> <!-- Left: Back + attendee name + status indicator -->
<div class="flex flex-row gap-2 items-center min-w-0"> <div class="flex flex-row gap-2 items-center min-w-0">
<a <a
href={`/events/${$lq__event_badge_obj.event_id}/badges`} href={`/events/${$lq__event_badge_obj.event_id}/badges`}
@@ -164,54 +155,49 @@
<span class="hidden sm:inline">Search</span> <span class="hidden sm:inline">Search</span>
</a> </a>
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<h2 class="text-base font-bold truncate"> <div class="flex flex-row gap-2 items-baseline flex-wrap">
{$lq__event_badge_obj.full_name_override ?? $lq__event_badge_obj.full_name ?? '— no name —'} <h2 class="text-base font-bold leading-tight truncate">
</h2> {$lq__event_badge_obj.full_name_override ?? $lq__event_badge_obj.full_name ?? '— no name —'}
{#if is_printed} </h2>
<p class="text-xs text-amber-600 font-medium"> {#if is_printed}
Printed {print_count}× <!-- Amber badge: clearly shows this has already been printed -->
{#if $lq__event_badge_obj.print_last_datetime} <span class="text-xs font-semibold text-amber-700 bg-amber-50 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-700 px-1.5 py-0.5 rounded-full whitespace-nowrap shrink-0">
— last {new Date($lq__event_badge_obj.print_last_datetime).toLocaleString()} Printed {print_count}×
{/if} </span>
{:else}
<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>
{: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()}
</p> </p>
{/if} {/if}
</div> </div>
</div> </div>
<!-- Right: Action buttons --> <!-- Right: secondary / staff-only actions -->
<div class="flex flex-row gap-1 items-center shrink-0"> <div class="flex flex-row gap-1 items-center shrink-0">
<!-- V1/V2 toggle: temporary comparison tool — remove once v2 is verified --> <!-- Re-print shortcut: only in edit mode when already printed.
<button Calls window.print() only — does NOT increment print_count.
type="button" Use the "Print Badge" button in the right panel for the canonical
class="btn btn-sm preset-tonal-surface flex items-center gap-1 font-mono" first print (tracks count + navigates back to search). -->
onclick={() => (use_v2 = !use_v2)} {#if is_trusted && is_printed && is_edit_mode}
title={use_v2 ? 'Switch to v1 (heuristic sizing)' : 'Switch to v2 (auto-scaling text)'}
>
{use_v2 ? 'v2' : 'v1'}
</button>
<!-- 1. Print Now (header shortcut): calls window.print() only — does NOT track
print_count. The canonical "Print Badge" button in the right panel controls
increments the count, fires window.print(), then redirects to badge search.
Use this header button only as a quick re-trigger of the print dialog. -->
<!-- Trusted+, not printed OR global Edit Mode active (reprint) -->
{#if is_trusted && (!is_printed || is_edit_mode)}
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-primary flex items-center gap-1" class="btn btn-sm preset-tonal-surface flex items-center gap-1"
onclick={() => window.print()} onclick={() => window.print()}
title={is_printed ? `Reprint badge (printed ${print_count}×)` : 'Print badge now'} title={`Re-trigger print dialog — already printed ${print_count}×`}
> >
<Printer size="1em" /> <Printer size="1em" />
{#if is_printed} <span class="hidden sm:inline text-xs">Re-print</span>
<span class="font-bold text-xs">{print_count}×</span>
{/if}
<span class="hidden sm:inline">Print Now</span>
</button> </button>
{/if} {/if}
<!-- 2. Direct Review link: Trusted + Edit Mode --> <!-- Review page: Trusted + Edit Mode -->
{#if is_trusted && is_edit_mode} {#if is_trusted && is_edit_mode}
<a <a
href={build_review_url()} href={build_review_url()}
@@ -223,7 +209,7 @@
</a> </a>
{/if} {/if}
<!-- 3. Email Review Link: all if not printed; Trusted+Edit if printed <!-- Email review link: available before first print, or staff+edit for reprints.
TODO: replace alert with actual email API call --> TODO: replace alert with actual email API call -->
{#if !is_printed || (is_trusted && is_edit_mode)} {#if !is_printed || (is_trusted && is_edit_mode)}
<button <button
@@ -240,38 +226,22 @@
</div> </div>
</header> </header>
<!-- Badge render — controls panel is fixed to the right viewport edge (out of flow), <!-- Badge render area.
so the badge gets the full available width on any screen size. pr-64 prevents the badge from hiding under the fixed right controls panel.
pr-64 offsets the badge area so it's not hidden under the fixed controls panel. On print: pr-0 restores full width, controls panel is hidden. -->
On print, pr-64 is cleared and the fixed controls panel is hidden. -->
<div class="print:pr-0 pr-64"> <div class="print:pr-0 pr-64">
{#if use_v2} <!-- null → Element_fit_text auto-scales; a number → manual size override from controls. -->
<!-- V2: pass null directly (not ?? undefined) so auto-scaling is active by default. <Comp_badge_obj_view_v2
null → Element_fit_text auto-scales; a number → manual override from print controls. --> event_id={$lq__event_badge_obj.event_id as string}
<Comp_badge_obj_view_v2 event_badge_id={event_badge_id as string}
event_id={$lq__event_badge_obj.event_id as string} {lq__event_badge_obj}
event_badge_id={event_badge_id as string} {lq__event_badge_template_obj}
{lq__event_badge_obj} is_review_mode={false}
{lq__event_badge_template_obj} font_size_name={font_size_name}
is_review_mode={false} font_size_title={font_size_title}
font_size_name={font_size_name} font_size_affiliations={font_size_affiliations}
font_size_title={font_size_title} font_size_location={font_size_location}
font_size_affiliations={font_size_affiliations} />
font_size_location={font_size_location}
/>
{:else}
<Comp_badge_obj_view
event_id={$lq__event_badge_obj.event_id as string}
event_badge_id={event_badge_id as string}
{lq__event_badge_obj}
{lq__event_badge_template_obj}
is_review_mode={false}
font_size_name={font_size_name ?? undefined}
font_size_title={font_size_title ?? undefined}
font_size_affiliations={font_size_affiliations ?? undefined}
font_size_location={font_size_location ?? undefined}
/>
{/if}
</div> </div>
<!-- Controls panel: fixed to the right edge of the viewport — screen-only. <!-- Controls panel: fixed to the right edge of the viewport — screen-only.

View File

@@ -3,7 +3,7 @@
import { events_func } from '$lib/ae_events_functions'; import { events_func } from '$lib/ae_events_functions';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import { events_slct } from '$lib/stores/ae_events_stores'; import { events_slct } from '$lib/stores/ae_events_stores';
import Comp_badge_obj_view from '../[badge_id]/ae_comp__badge_obj_view.svelte'; import Comp_badge_obj_view from '../[badge_id]/ae_comp__badge_obj_view_v2.svelte';
import type { ae_EventBadge } from '$lib/types/ae_types'; import type { ae_EventBadge } from '$lib/types/ae_types';
interface Props { interface Props {