feat: badge print controls — quick print btn, compacted spacing, collapsible sections, overflow fix
- Add Quick Print button (30%) alongside canonical Print Badge (70%): calls window.print() only — no count increment, no navigation back to search - Compact panel spacing: reduce space-y, pt/pb on card headers, standalone row py, font_ctrl py - Add collapsible Attendee/Staff section groups reusing ctrl-accordion CSS pattern; attendee starts open, staff starts collapsed — auto-collapses the other on expand - Add overflow-x-hidden to print page panel container to kill horizontal scrollbar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import { browser } from '$app/environment';
|
||||
import { Check, ChevronDown, Eye, EyeOff, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte';
|
||||
import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte';
|
||||
interface Props {
|
||||
event_id: string;
|
||||
event_badge_id: string;
|
||||
@@ -98,6 +98,22 @@
|
||||
// — used here to allow reprinting an already-printed badge when global edit mode is active.
|
||||
let is_global_edit_mode = $derived($ae_loc.edit_mode === true);
|
||||
|
||||
// --- Section collapse state ---
|
||||
// Attendee fields start open (primary use); staff starts closed to save space.
|
||||
// Toggling one collapses the other (auto-collapse, same pattern as launcher sections).
|
||||
let attendee_open = $state(true);
|
||||
let staff_open = $state(false);
|
||||
|
||||
function toggle_section(which: 'attendee' | 'staff') {
|
||||
if (which === 'attendee') {
|
||||
attendee_open = !attendee_open;
|
||||
if (attendee_open) staff_open = false;
|
||||
} else {
|
||||
staff_open = !staff_open;
|
||||
if (staff_open) attendee_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Print ---
|
||||
let print_count = $derived($lq__event_badge_obj?.print_count ?? 0);
|
||||
let is_printed = $derived(print_count >= 1);
|
||||
@@ -405,7 +421,7 @@
|
||||
key === 'name' ? font_size_name :
|
||||
key === 'title' ? font_size_title :
|
||||
key === 'affiliations' ? font_size_affiliations : font_size_location}
|
||||
<div class="flex items-center gap-1 border-t border-gray-100 dark:border-gray-800 px-2 py-1"
|
||||
<div class="flex items-center gap-1 border-t border-gray-100 dark:border-gray-800 px-2 py-0.5"
|
||||
role="group"
|
||||
aria-label="{key} font size controls"
|
||||
>
|
||||
@@ -505,34 +521,52 @@
|
||||
Main panel
|
||||
============================================================ -->
|
||||
|
||||
<!-- Print button — canonical action: increments print_count, fires window.print(),
|
||||
then navigates back to badge search. Trusted+ only; edit mode allows reprints. -->
|
||||
<!-- Print button row — canonical (70%) + quick print (30%).
|
||||
Canonical: increments print_count, fires window.print(), navigates back to search.
|
||||
Quick print: just window.print() — no count increment, no navigation.
|
||||
Both: Trusted+ only; edit mode allows reprints. -->
|
||||
{#if can_print}
|
||||
<!-- Tinted card visually lifts the print action above the field list -->
|
||||
<div class="mb-3 p-2 rounded-lg bg-primary-50/40 dark:bg-primary-950/20 border border-primary-200/50 dark:border-primary-800/30">
|
||||
<button
|
||||
type="button"
|
||||
class="btn w-full flex items-center justify-center gap-2"
|
||||
class:preset-filled-primary={print_status === 'idle'}
|
||||
class:preset-tonal-surface={print_status === 'loading'}
|
||||
class:preset-filled-success={print_status === 'done'}
|
||||
class:preset-tonal-error={print_status === 'error'}
|
||||
onclick={handle_print_badge}
|
||||
disabled={print_status === 'loading' || print_status === 'done'}
|
||||
title={is_printed ? `Reprint badge (printed ${print_count}×)` : 'Print badge now'}
|
||||
data-testid="badge-print-btn"
|
||||
>
|
||||
{#if print_status === 'loading'}
|
||||
<LoaderCircle size="14" class="animate-spin" /> Printing…
|
||||
{:else if print_status === 'done'}
|
||||
<Check size="14" /> Printed!
|
||||
{:else if print_status === 'error'}
|
||||
Error — try again
|
||||
{:else}
|
||||
<Printer size="14" />
|
||||
{is_printed ? `Reprint (${print_count}×)` : 'Print Badge'}
|
||||
{/if}
|
||||
</button>
|
||||
<div class="mb-2 p-2 rounded-lg bg-primary-50/40 dark:bg-primary-950/20">
|
||||
<div class="flex gap-1.5">
|
||||
<!-- Canonical Print Badge: 70% — tracks count, returns to search -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn flex-7 flex items-center justify-center gap-2 border border-primary-200/50 dark:border-primary-800/30"
|
||||
class:preset-filled-primary={print_status === 'idle'}
|
||||
class:preset-tonal-surface={print_status === 'loading'}
|
||||
class:preset-filled-success={print_status === 'done'}
|
||||
class:preset-tonal-error={print_status === 'error'}
|
||||
onclick={handle_print_badge}
|
||||
disabled={print_status === 'loading' || print_status === 'done'}
|
||||
title={is_printed ? `Reprint badge (printed ${print_count}×)` : 'Print badge now'}
|
||||
data-testid="badge-print-btn"
|
||||
>
|
||||
{#if print_status === 'loading'}
|
||||
<LoaderCircle size="14" class="animate-spin" /> Printing…
|
||||
{:else if print_status === 'done'}
|
||||
<Check size="14" /> Printed!
|
||||
{:else if print_status === 'error'}
|
||||
Error — try again
|
||||
{:else}
|
||||
<Printer size="14" />
|
||||
{is_printed ? `Reprint (${print_count}×)` : 'Print Badge'}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Quick print: 30% — window.print() only, no count, no navigation.
|
||||
Use when you need to reprint mid-workflow without resetting search state
|
||||
or when the count should not change (e.g. a test print). -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn flex-3 preset-tonal-surface flex items-center justify-center gap-1 border border-primary-200/50 dark:border-primary-800/30 text-xs"
|
||||
onclick={() => window.print()}
|
||||
title="Print without tracking — no count increment, stays on this page"
|
||||
>
|
||||
<Printer size="12" />
|
||||
Print
|
||||
</button>
|
||||
</div>
|
||||
{#if is_printed && print_status === 'idle'}
|
||||
<p class="text-[10px] text-amber-600 dark:text-amber-400 text-center mt-1">
|
||||
Already printed {print_count}×
|
||||
@@ -541,15 +575,31 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1.5 text-sm">
|
||||
<div class="space-y-1 text-sm">
|
||||
|
||||
<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>
|
||||
<!-- Attendee section header — clickable, collapses staff when opened -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
onclick={() => toggle_section('attendee')}
|
||||
aria-expanded={attendee_open}
|
||||
>
|
||||
<span class="text-[9px] uppercase tracking-widest text-gray-400 dark:text-gray-500 font-semibold">Attendee info</span>
|
||||
{#if attendee_open}
|
||||
<ChevronDown size="10" class="opacity-40" />
|
||||
{:else}
|
||||
<ChevronRight size="10" class="opacity-40" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="ctrl-accordion" class:open={attendee_open}>
|
||||
<div class="ctrl-accordion-inner space-y-1">
|
||||
|
||||
<!-- === NAME ===
|
||||
Font controls: always visible (all can adjust for readability at kiosk).
|
||||
Edit form: Trusted+ only — accordion opens only when is_trusted. -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'name'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Name</p>
|
||||
{#if get_display('full_name_override', 'full_name')}
|
||||
@@ -608,7 +658,7 @@
|
||||
|
||||
<!-- === PROFESSIONAL TITLE === -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'title'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Title</p>
|
||||
{#if get_display('professional_title_override', 'professional_title')}
|
||||
@@ -661,7 +711,7 @@
|
||||
|
||||
<!-- === AFFILIATIONS === -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'affiliations'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Affiliations</p>
|
||||
{#if get_display('affiliations_override', 'affiliations')}
|
||||
@@ -714,7 +764,7 @@
|
||||
|
||||
<!-- === LOCATION === -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'location'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Location</p>
|
||||
{#if get_display('location_override', 'location')}
|
||||
@@ -767,7 +817,7 @@
|
||||
|
||||
<!-- === ALLOW TRACKING (Lead Scanning) === -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'allow_tracking'}>
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Lead Scanning</p>
|
||||
<p class="leading-snug text-xs">
|
||||
@@ -851,7 +901,7 @@
|
||||
|
||||
<!-- === PRONOUNS === -->
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'pronouns'}>
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Pronouns</p>
|
||||
{#if get_display('pronouns_override', 'pronouns')}
|
||||
@@ -898,19 +948,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- ctrl-accordion-inner attendee -->
|
||||
</div><!-- ctrl-accordion attendee -->
|
||||
|
||||
<!-- === STAFF-ONLY FIELDS === -->
|
||||
{#if is_trusted}
|
||||
|
||||
<!-- Divider between attendee and staff fields -->
|
||||
<div class="pt-1.5 pb-0.5 flex items-center gap-1.5">
|
||||
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
|
||||
<span class="text-[9px] uppercase tracking-widest text-gray-300 dark:text-gray-600 font-semibold whitespace-nowrap">Staff adjustments</span>
|
||||
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
|
||||
</div>
|
||||
<!-- Staff section header — clickable, collapses attendee when opened -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
onclick={() => toggle_section('staff')}
|
||||
aria-expanded={staff_open}
|
||||
>
|
||||
<span class="text-[9px] uppercase tracking-widest text-gray-400 dark:text-gray-500 font-semibold">Staff adjustments</span>
|
||||
{#if staff_open}
|
||||
<ChevronDown size="10" class="opacity-40" />
|
||||
{:else}
|
||||
<ChevronRight size="10" class="opacity-40" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="ctrl-accordion" class:open={staff_open}>
|
||||
<div class="ctrl-accordion-inner space-y-1 pt-0.5">
|
||||
|
||||
<!-- Chrome visibility toggle: hides page header + sys bar for a clean workspace.
|
||||
Keyboard shortcut [H] does the same thing from anywhere on the page. -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm w-full justify-between"
|
||||
@@ -933,7 +997,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Name layout: two-line (preferred) vs single-line -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -945,7 +1009,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Banner width toggle: 100% (production) vs natural size (dev/calibration) -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -957,7 +1021,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Debug Outlines Toggle (for print testing) -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -973,8 +1037,8 @@
|
||||
Stored in localStorage by the parent page — each workstation keeps
|
||||
its own values across sessions without any server config change.
|
||||
Positive X = shift right; positive Y = shift down. 0.5mm steps. -->
|
||||
<div class="px-2 py-1.5">
|
||||
<p class="field-label mb-1.5">
|
||||
<div class="px-2 py-1">
|
||||
<p class="field-label mb-1">
|
||||
Print position
|
||||
<span class="text-[9px] font-normal normal-case tracking-normal text-gray-400 ml-1">saved to browser</span>
|
||||
</p>
|
||||
@@ -1043,7 +1107,7 @@
|
||||
<!-- === BADGE TYPE === (only when template defines badge_type_list) -->
|
||||
{#if badge_type_code_li.length > 0}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'badge_type'}>
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="field-label">Badge Type</p>
|
||||
{#if badge_type_display}
|
||||
@@ -1101,6 +1165,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div><!-- ctrl-accordion-inner staff -->
|
||||
</div><!-- ctrl-accordion staff -->
|
||||
|
||||
{/if}<!-- end is_trusted -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
Fixed right-0 means width growth expands leftward into the badge area.
|
||||
top-20 clears the page header (~80px). Independently scrollable.
|
||||
bottom-24: clears the sys bar (fixed bottom-12 h-9 = 84px from bottom). -->
|
||||
<div class="print:hidden fixed right-0 top-20 bottom-24 overflow-y-auto z-30
|
||||
<div class="print:hidden fixed right-0 top-20 bottom-24 overflow-y-auto overflow-x-hidden z-30
|
||||
border-l border-gray-200 dark:border-gray-700
|
||||
bg-white dark:bg-zinc-900 p-2
|
||||
transition-all duration-200"
|
||||
|
||||
Reference in New Issue
Block a user