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`
should be the reference. See Design Intent section above.
### TASK 4.1: Auto-Scaling Badge Text v2 — IN PROGRESS (2026-03-12)
**Files created:**
- `src/lib/elements/action_fit_text.ts` — Svelte action: binary-search font scaling with
MutationObserver + ResizeObserver + requestAnimationFrame.
- `src/lib/elements/element_fit_text.svelte` — Component wrapper. Key prop: `height` (required
for binary search to work — without it, offsetHeight == scrollHeight always).
- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render using
Element_fit_text for name/title/affiliations/location in display mode.
`fit_heights` derived object provides layout-aware heights per field per badge layout.
`font_size_*` props default to `undefined` (auto-scale) rather than numeric defaults (v1 behavior).
Manual overrides from print controls still work — any number disables auto-scale for that field.
### TASK 4.1: Auto-Scaling Badge Text v2 — COMPLETE (2026-03-12)
**Files created/updated:**
- `src/lib/elements/action_fit_text.ts` — Svelte action
- `src/lib/elements/element_fit_text.svelte` — Component wrapper
- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render (canonical)
Debug blocks gated behind `$ae_loc.edit_mode` (hidden in production).
- `print/+page.svelte` — Always uses v2 now. v1/v2 toggle removed. Header redesigned for kiosk UX.
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
"Staff adjustments" divider before badge_type field.
- `print_list/+page.svelte` — Updated to import v2.
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/gemini_trash/**
**Toggle:** `v1`/`v2` button in print page header. V1 preserved as fallback.
**Status:** Working — heights in `fit_heights` still need visual tuning with real badge stock.
**Kiosk UX improvements (2026-03-12):**
- 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)

View File

@@ -929,21 +929,15 @@
<!-- End if for lq__event_badge_obj && lq__event_badge_template_obj -->
</section>
<div>
{#if $ae_loc.edit_mode}
<div class="print:hidden">
<h1 class="text-lg font-bold mt-4">Debug Information</h1>
<pre
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
print:hidden
">
{JSON.stringify($lq__event_badge_obj, null, 2)}
</pre>
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"
>{JSON.stringify($lq__event_badge_obj, null, 2)}</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
mt-4
print:hidden
">
{JSON.stringify($lq__event_badge_template_obj, null, 2)}
</pre>
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"
>{JSON.stringify($lq__event_badge_template_obj, null, 2)}</pre>
</div>
{/if}

View File

@@ -319,11 +319,26 @@
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+
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. -->
when not yet printed, OR when global AE Edit Mode is active (allows reprints). -->
{#if can_print}
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<button
@@ -359,6 +374,9 @@
<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 === -->
<!-- Editable by Trusted+ only; all users have font controls -->
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
@@ -592,52 +610,62 @@
{/if}
</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 === -->
{#if is_trusted}
<!-- === PRONOUNS === -->
<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}
<!-- 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>
</div>
<!-- === BADGE TYPE === -->

View File

@@ -16,14 +16,9 @@
import { page } from '$app/state';
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_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_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.`);
}
// Print mode: display only — ae_comp__badge_obj_view handles print count + window.print()
// 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.
// Font size overrides (px). null = auto-sizing (Element_fit_text binary search).
// Controls live in Comp_badge_print_controls (right panel) via $bindable().
// 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.
@@ -150,10 +141,10 @@
{#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">
<!-- 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">
<a
href={`/events/${$lq__event_badge_obj.event_id}/badges`}
@@ -164,54 +155,49 @@
<span class="hidden sm:inline">Search</span>
</a>
<div class="flex flex-col min-w-0">
<h2 class="text-base font-bold truncate">
{$lq__event_badge_obj.full_name_override ?? $lq__event_badge_obj.full_name ?? '— no name —'}
</h2>
{#if is_printed}
<p class="text-xs text-amber-600 font-medium">
Printed {print_count}×
{#if $lq__event_badge_obj.print_last_datetime}
— last {new Date($lq__event_badge_obj.print_last_datetime).toLocaleString()}
{/if}
<div class="flex flex-row gap-2 items-baseline flex-wrap">
<h2 class="text-base font-bold leading-tight truncate">
{$lq__event_badge_obj.full_name_override ?? $lq__event_badge_obj.full_name ?? '— no name —'}
</h2>
{#if is_printed}
<!-- Amber badge: clearly shows this has already been printed -->
<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">
Printed {print_count}×
</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>
{/if}
</div>
</div>
<!-- Right: Action buttons -->
<!-- Right: secondary / staff-only actions -->
<div class="flex flex-row gap-1 items-center shrink-0">
<!-- V1/V2 toggle: temporary comparison tool — remove once v2 is verified -->
<button
type="button"
class="btn btn-sm preset-tonal-surface flex items-center gap-1 font-mono"
onclick={() => (use_v2 = !use_v2)}
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)}
<!-- Re-print shortcut: only in edit mode when already printed.
Calls window.print() only — does NOT increment print_count.
Use the "Print Badge" button in the right panel for the canonical
first print (tracks count + navigates back to search). -->
{#if is_trusted && is_printed && is_edit_mode}
<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()}
title={is_printed ? `Reprint badge (printed ${print_count}×)` : 'Print badge now'}
title={`Re-trigger print dialog — already printed ${print_count}×`}
>
<Printer size="1em" />
{#if is_printed}
<span class="font-bold text-xs">{print_count}×</span>
{/if}
<span class="hidden sm:inline">Print Now</span>
<span class="hidden sm:inline text-xs">Re-print</span>
</button>
{/if}
<!-- 2. Direct Review link: Trusted + Edit Mode -->
<!-- Review page: Trusted + Edit Mode -->
{#if is_trusted && is_edit_mode}
<a
href={build_review_url()}
@@ -223,7 +209,7 @@
</a>
{/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 -->
{#if !is_printed || (is_trusted && is_edit_mode)}
<button
@@ -240,38 +226,22 @@
</div>
</header>
<!-- Badge render — controls panel is fixed to the right viewport edge (out of flow),
so the badge gets the full available width on any screen size.
pr-64 offsets the badge area so it's not hidden under the fixed controls panel.
On print, pr-64 is cleared and the fixed controls panel is hidden. -->
<!-- Badge render area.
pr-64 prevents the badge from hiding under the fixed right controls panel.
On print: pr-0 restores full width, controls panel is hidden. -->
<div class="print:pr-0 pr-64">
{#if use_v2}
<!-- V2: pass null directly (not ?? undefined) so auto-scaling is active by default.
null → Element_fit_text auto-scales; a number → manual override from print controls. -->
<Comp_badge_obj_view_v2
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}
font_size_title={font_size_title}
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}
<!-- null → Element_fit_text auto-scales; a number → manual size override from controls. -->
<Comp_badge_obj_view_v2
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}
font_size_title={font_size_title}
font_size_affiliations={font_size_affiliations}
font_size_location={font_size_location}
/>
</div>
<!-- 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 { ae_api } from '$lib/stores/ae_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';
interface Props {