diff --git a/documentation/MODULE__AE_Events_Badge_Templates.md b/documentation/MODULE__AE_Events_Badge_Templates.md index 7ed48f96..c362003a 100644 --- a/documentation/MODULE__AE_Events_Badge_Templates.md +++ b/documentation/MODULE__AE_Events_Badge_Templates.md @@ -358,7 +358,7 @@ Firefox users can use "Save to PDF" directly — it just works. - [x] Wire `style_href` via `` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified) - [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified) -- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view_v2.svelte`; `show_badge_back` derived from `duplex` field. Note: v1 (`ae_comp__badge_obj_view.svelte`) was archived to `~/tmp/gemini_trash/`; v2 is canonical. (2026-03-18 verified) +- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field. - [ ] Make `layout` field drive actual card dimensions in the badge component — currently the Zebra ZC10L layout CSS (`badge_layout_zebra_zc10l_pvc.css`) sets dimensions correctly via `[data-layout="..."]` scoping, but fanfold layouts still use Tailwind defaults. Needs proper CSS for each layout code. -- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view_v2.svelte` (if they were carried over from v1) +- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1) - [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal) diff --git a/documentation/PROJECT__AE_Events_Badges_Review_Print.md b/documentation/PROJECT__AE_Events_Badges_Review_Print.md index 4f333399..e15486f4 100644 --- a/documentation/PROJECT__AE_Events_Badges_Review_Print.md +++ b/documentation/PROJECT__AE_Events_Badges_Review_Print.md @@ -31,7 +31,7 @@ - Once satisfied, staff prints the badge. - The key differentiator vs the review form: **the live badge render** shows exactly how the badge will print. Attendees and staff can see changes immediately. -- Component: `ae_comp__badge_obj_view.svelte` / `ae_comp__badge_obj_view_v2.svelte` +- Component: `ae_comp__badge_obj_view.svelte` - Route: `/events/[event_id]/badges/[badge_id]/print/` ### Permission Model — Same Logic, Both Flows @@ -69,8 +69,8 @@ Work needed: or whether it should share/reuse the review form component alongside the badge render. - **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`. -### 1. Auto-Scaling Badge Text (v2) — In Progress -`ae_comp__badge_obj_view_v2.svelte` using `element_fit_text.svelte` (binary search auto-scale). +### 1. Auto-Scaling Badge Text — In Progress +`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale). Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button. Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges. @@ -118,7 +118,7 @@ the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badg **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) +- `src/routes/events/.../ae_comp__badge_obj_view.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, diff --git a/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css b/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css index c16f14d5..8d16e836 100644 --- a/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css +++ b/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css @@ -32,6 +32,16 @@ padding: 0; gap: 0; min-height: 0; - width: fit-content; - max-width: fit-content; + width: 3.5in; + max-width: 3.5in; +} + +@media print { + [data-layout="badge_3.5x5.5_pvc"].event_badge_wrapper { + width: 3.5in !important; + height: 5.5in !important; + max-width: 3.5in !important; + max-height: 5.5in !important; + overflow: hidden !important; + } } diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte similarity index 83% rename from src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte rename to src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte index 4b9f0bfe..4c31af8b 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte @@ -38,6 +38,11 @@ * values immediately without waiting for Save + liveQuery round-trip. */ preview_overrides?: Record | null; + /** + * When true (default), header images stretch to 100% badge width (production view). + * Set false via the controls panel to see the image at its natural size (dev/testing). + */ + banner_full_width?: boolean; log_lvl?: number; } @@ -52,6 +57,7 @@ font_size_affiliations, font_size_location, preview_overrides = null, + banner_full_width = true, }: Props = $props(); // Badge layout CSS — compiled in, hot-reloads in dev. @@ -60,6 +66,7 @@ import '$lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css'; import '$lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css'; + import { untrack } from 'svelte'; import { browser } from '$app/environment'; import { core_func } from '$lib/ae_core/ae_core_functions'; import { ae_loc, slct } from '$lib/stores/ae_stores'; @@ -133,9 +140,6 @@ $lq__event_badge_template_obj?.duplex == null || !!$lq__event_badge_template_obj?.duplex ); - // Receipt and ticket sections — disabled pending redesign. - let show_receipt = $derived(false); - let show_tickets = $derived(false); /** * Layout-aware section heights for Element_fit_text. @@ -224,9 +228,17 @@ let qr_error_message = $state(''); // --- Demo background pattern (edit mode only, for preview/demo purposes) --- - // 0 = off, 1–4 = pattern variants. Cycles on each button click. + // 0 = off, 1–7 = pattern variants. Cycles on each button click. + // Automatically resets to 0 when global edit mode is turned off so the pattern + // never persists into a live print session. let demo_bg_state = $state(0); - const DEMO_BG_PATTERNS = 5; + const DEMO_BG_PATTERNS = 7; + + $effect(() => { + if (!$ae_loc.edit_mode) { + untrack(() => { demo_bg_state = 0; }); + } + }); function cycle_demo_bg() { demo_bg_state = (demo_bg_state + 1) % (DEMO_BG_PATTERNS + 1); @@ -255,8 +267,11 @@ // 4: grid [``, '24px 24px'], // 5: swirls — two crossing S-curves (yin-yang tiling) + echo curves for depth. - // Paths use corner-to-corner cubic beziers so opposite tile edges match perfectly. [``, '64px 64px'], + // 6: Inch Calibration Grid (1 inch squares, 0.25 inch subdivisions). + [`1in`, '96px 96px'], + // 7: Metric Calibration Grid (10mm squares, 1mm subdivisions). + [`1cm`, '37.795px 37.795px'], ]; const [svg, size] = patterns[(demo_bg_state - 1) % DEMO_BG_PATTERNS]; return `background-image: url("data:image/svg+xml,${encodeURIComponent(svg)}"); background-repeat: repeat; background-size: ${size};`; @@ -279,22 +294,8 @@ } }); - /* *** BEGIN *** Legacy ticket/option state — move to template config in future */ - let option_ticket_1_override = $state(''); - let option_ticket_2_override = $state(''); - let option_ticket_3_override = $state(''); - let option_other_1_override = $state(''); - let option_other_2_override = $state(''); - - let option_ticket_1_display_opt = 'front_bool'; - let option_ticket_2_display_opt = 'front_bool'; - let option_ticket_3_display_opt = 'front_bool'; - - let option_other_1_display_opt = 'back_html'; - let option_other_2_display_opt = 'back_html'; - - // Maps badge option field values → Lucide icon components. - // option_1 = dietary/allergy flag; option_2 = special designation (first-time, etc.) + // Maps other_1_code (dietary/allergy) and other_2_code (special designation e.g. + // First Time Attendee) values → Lucide icon components for badge display. // WHY: Replaces the old code_to_html FA string map — no FontAwesome dependency. // eslint-disable-next-line @typescript-eslint/no-explicit-any const code_to_icon: { option_1: Record; option_2: Record } = { @@ -335,7 +336,7 @@ onclick={cycle_demo_bg} title="Cycle demo background pattern — edit mode only" > - 🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls'][demo_bg_state - 1] : 'demo bg'} + 🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls','in-grid','mm-grid'][demo_bg_state - 1] : 'demo bg'} {/if} @@ -396,6 +397,7 @@ > check header path @@ -532,24 +534,17 @@ {/if} - {#if ['front_bool', 'front_back_bool'].includes(option_ticket_1_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_2_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_3_display_opt) || $lq__event_badge_template_obj?.show_qr_front} - + + {#if eff_badge?.ticket_1_code || eff_badge?.ticket_2_code || eff_badge?.ticket_3_code || $lq__event_badge_template_obj?.show_qr_front}
- {#if option_ticket_1_override}{/if} + {#if eff_badge?.ticket_1_code}{/if} - {#if option_ticket_2_override} - - {/if} - {#if option_ticket_3_override} - - {/if} + {#if eff_badge?.ticket_2_code}{/if} + {#if eff_badge?.ticket_3_code}{/if}
{#if $lq__event_badge_template_obj?.show_qr_front} @@ -582,14 +577,12 @@ " title={effective_badge_type_code} > - {#if option_other_1_override && ['front_bool', 'front_back_bool'].includes(option_other_1_display_opt)} - - {:else if option_other_1_override && ['front_html', 'front_back_html'].includes(option_other_1_display_opt)} - {@const Icon1 = code_to_icon.option_1[option_other_1_override]} + + {#if eff_badge?.other_1_code} + {@const Icon1 = code_to_icon.option_1[eff_badge.other_1_code]} - {#if Icon1}{/if} + {#if Icon1}{:else}{/if} {/if} @@ -600,14 +593,11 @@ >{badge_type_name} - {#if option_other_2_override && ['front_bool', 'front_back_bool'].includes(option_other_2_display_opt)} - - {:else if option_other_2_override && ['front_html', 'front_back_html'].includes(option_other_2_display_opt)} - {@const Icon2 = code_to_icon.option_2[option_other_2_override]} + + {#if eff_badge?.other_2_code} + {@const Icon2 = code_to_icon.option_2[eff_badge.other_2_code]} - {#if Icon2}{/if} + {#if Icon2}{:else}{/if} {/if}
@@ -656,6 +646,7 @@ > check secondary header path @@ -664,6 +655,7 @@
check primary header path @@ -768,7 +760,7 @@
- {#if $lq__event_badge_obj.other_1 || $lq__event_badge_obj.other_2 || $lq__event_badge_obj.ticket_1_code || $lq__event_badge_obj.ticket_2_code || $lq__event_badge_obj.ticket_3_code || $lq__event_badge_obj.ticket_4_code || $lq__event_badge_obj.ticket_5_code} + {#if $lq__event_badge_obj?.other_1_code || $lq__event_badge_obj?.other_2_code || $lq__event_badge_obj?.ticket_1_code || $lq__event_badge_obj?.ticket_2_code || $lq__event_badge_obj?.ticket_3_code || $lq__event_badge_obj?.ticket_4_code || $lq__event_badge_obj?.ticket_5_code}
    {#if $lq__event_badge_obj.ticket_1_code} @@ -799,26 +791,18 @@
{#if $ae_loc.administrator_access} - {#if $lq__event_badge_obj.other_1} + {#if $lq__event_badge_obj?.other_1_code} + {@const BIcon1 = code_to_icon.option_1[$lq__event_badge_obj.other_1_code]}
- Other 1: {$lq__event_badge_obj.other_1} - {#if option_other_1_override && ['back_bool', 'front_back_bool'].includes(option_other_1_display_opt)} - - {option_other_1_override} - {:else if option_other_1_override && ['back_html', 'front_back_html'].includes(option_other_1_display_opt)} - {@const BIcon1 = code_to_icon.option_1[option_other_1_override]} - - {option_other_1_override}{#if BIcon1} {/if} - {/if} + Other 1: {$lq__event_badge_obj.other_1_code} + {#if BIcon1} {/if}
{/if} - {#if $lq__event_badge_obj.other_2} + {#if $lq__event_badge_obj?.other_2_code} + {@const BIcon2 = code_to_icon.option_2[$lq__event_badge_obj.other_2_code]}
- Other 2: {$lq__event_badge_obj.other_2} - {#if option_other_2_override && ['back_bool', 'front_back_bool'].includes(option_other_2_display_opt)} - - {option_other_2_override} - {:else if option_other_2_override && ['back_html', 'front_back_html'].includes(option_other_2_display_opt)} - {@const BIcon2 = code_to_icon.option_2[option_other_2_override]} - - {option_other_2_override}{#if BIcon2} {/if} - {/if} + Other 2: {$lq__event_badge_obj.other_2_code} + {#if BIcon2} {/if}
{/if} {/if} @@ -919,91 +903,8 @@ {/if} - - {#if show_receipt} - - {/if} - - - - {#if show_tickets} - - - - {/if} - + {/if} @@ -1081,4 +982,28 @@ background-color: white; color: #1a1a1a; } + + /* + * Header image: center horizontally within the badge. + * defaults to display:inline, which left-aligns any image narrower than + * its container. display:block + margin:auto centers it regardless of image width. + * max-width:100% prevents overflow; height:auto preserves aspect ratio. + * Applied to all three header_image uses: front primary, back secondary, back fallback. + */ + .header_image { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; + } + + /* + * banner_full_width mode (default on): stretches the header image to the full badge + * width so it fills the banner slot edge-to-edge. + * Toggle off in the controls panel staff section to see the image at its natural + * pixel size — useful for calibration and checking source image dimensions. + */ + .header_image.header_full_width { + width: 100%; + } diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte index bd363f96..26d716fb 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte @@ -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, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte'; + import { Check, ChevronDown, Eye, EyeOff, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte'; interface Props { event_id: string; event_badge_id: string; @@ -42,6 +42,24 @@ * as the user types — no save required. */ preview_overrides?: Record | null; + /** + * Per-browser print position calibration (mm). Stored in localStorage by the + * parent page so each workstation/printer can be tuned independently. + * Positive X = shift right; positive Y = shift down. + */ + print_offset_x?: number; + print_offset_y?: number; + /** + * Hides the page header and sys bar for a clean badge-preview workspace. + * Toggle with the [H] keyboard shortcut or this button. Stored in localStorage. + */ + hide_chrome?: boolean; + /** + * When true (default), header images stretch to 100% badge width. + * Toggle off to see the image at its natural pixel size for calibration. + * Stored in localStorage. + */ + banner_full_width?: boolean; log_lvl?: number; } @@ -55,6 +73,10 @@ font_size_affiliations = $bindable(null), font_size_location = $bindable(null), preview_overrides = $bindable(null), + print_offset_x = $bindable(0), + print_offset_y = $bindable(0), + hide_chrome = $bindable(false), + banner_full_width = $bindable(true), log_lvl = 0 }: Props = $props(); @@ -125,6 +147,10 @@ try { return JSON.parse(raw); } catch { return []; } }); + // --- Print position offset configuration --- + const OFFSET_STEP = 0.5; // mm per click + const OFFSET_MAX = 15; // mm in either direction + // --- Font size configuration --- const FONT_SIZE_STEP = 2; const FONT_SIZE_DEFAULTS = { name: 58, title: 34, affiliations: 38, location: 34 } as const; @@ -879,6 +905,42 @@
+ +
+ +
+ + +
+ +
+
+ +
+

+ Print position + saved to browser +

+
+ +
+ L/R + + {print_offset_x > 0 ? '+' : ''}{print_offset_x.toFixed(1)} mm + +
+ +
+ U/D + + {print_offset_y > 0 ? '+' : ''}{print_offset_y.toFixed(1)} mm + +
+
+ {#if print_offset_x !== 0 || print_offset_y !== 0} + + {/if} +
+ {#if badge_type_code_li.length > 0}
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte index 9e1bedd3..4737848b 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte @@ -9,13 +9,14 @@ import { untrack } from 'svelte'; import { liveQuery } from 'dexie'; + import { browser } from '$app/environment'; - import { ae_loc } from '$lib/stores/ae_stores'; + import { ae_loc, ae_sess } from '$lib/stores/ae_stores'; import { db_events } from '$lib/ae_events/db_events'; import { events_loc } from '$lib/stores/ae_events_stores'; import { page } from '$app/state'; import { ArrowLeft, Eye, LoaderCircle, Mail, Printer } from '@lucide/svelte'; - import Comp_badge_obj_view_v2 from '../ae_comp__badge_obj_view_v2.svelte'; + import Comp_badge_obj_view from '../ae_comp__badge_obj_view.svelte'; import Comp_badge_print_controls from '../ae_comp__badge_print_controls.svelte'; let event_badge_id = $derived(page.params.badge_id); @@ -102,6 +103,64 @@ let font_size_affiliations: number | null = $state(null); let font_size_location: number | null = $state(null); + // Per-browser print workstation tweaks. + // Each workstation/printer is calibrated independently — values are stored in + // localStorage so they survive page reloads and browser restarts without any + // server config change. + // WHY localStorage not a store: this is intentionally per-device, not per-user or + // per-event. Different printers at the same venue may need different offsets. + const _PRINT_TWEAKS_KEY = 'ae_badge_print_tweaks'; + const _saved_tweaks = (() => { + if (!browser) return null; + try { return JSON.parse(localStorage.getItem(_PRINT_TWEAKS_KEY) ?? 'null'); } catch { return null; } + })(); + + // Print position offset (mm). Positive X = shift right; positive Y = shift down. + let print_offset_x: number = $state(_saved_tweaks?.x ?? 0); + let print_offset_y: number = $state(_saved_tweaks?.y ?? 0); + + // Banner width: true = 100% (production), false = natural pixel size (dev/calibration). + let banner_full_width: boolean = $state(_saved_tweaks?.banner_full_width ?? true); + + // hide_chrome: hides the page header and sys bar for a clean badge-preview workspace. + // Toggled via the [H] keyboard shortcut or the button in the controls panel staff section. + let hide_chrome: boolean = $state(_saved_tweaks?.hide_chrome ?? false); + + // Sync chrome visibility with hide_chrome. Restores all original states on page leave. + // Hides three things: + // 1. $ae_loc.sys_menu.hide — the fixed bottom-right AE sys bar + // 2. $ae_sess.disable_sys_nav — the Events layout