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:
@@ -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)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
@@ -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 === -->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user