Files
OSIT-AE-App-Svelte/documentation/MODULE__AE_Events_Badge_Templates.md
Scott Idem 7bf76bf766 feat(badges): configurable punch-out hole markers for badge clip slots
Adds cfg_json.punch_holes.{left,right,center} to mark pre-perforated badge
clip slots with X overlays. Slots are 5/8in x 1/8in, 1/4in from top,
3/8in from left/right edges. Markers print on the badge so attendees know
where to push out the perforations. Template form exposes checkboxes in
Header & Branding. Documented in MODULE__AE_Events_Badge_Templates.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:52:59 -04:00

21 KiB
Raw Blame History

MODULE: Aether Events — Badge Templates

Module Path: src/routes/events/[event_id]/(badges)/templates/ API Module: src/lib/ae_events/ae_events__event_badge_template.ts Database Table: event_badge_template Last Updated: 2026-03-02


Overview

Badge templates define the visual and structural configuration for printing event badges. Each template applies to one category of badge (e.g., general attendees, workshops, exhibitors). An event typically has 13 templates.

Key principle: One template per badge stock type/audience. Do not use flags on a single template to drive multiple layouts — create a separate template instead.

Common template sets:

  • General Attendees — main conference badge (most attendees)
  • Workshops / Pre-conference — alternate header, possibly different badge type list
  • Exhibitors — distinct footer stripe colors, exhibitor-specific badge types

Each template uses the same physical badge stock and printer configuration.


DB Field Reference

Core Identity

Field Type Notes
id int Internal PK
id_random str External-facing ID (AE Triple ID pattern)
event_id str Parent event
name str Template display name
description str Optional description

Image Assets

Field Type Notes
logo_path str (URL) Org logo — fallback when no header image
logo_filename str Deprecated — redundant with logo_path; do not use
header_path str (URL) Front-of-badge header image — primary branding
secondary_header_path str (URL) Back-of-badge header image (falls back to header_path)
footer_path str (URL) Optional footer image — rarely used
header_row_1 str/HTML Text fallback line 1 when no header image
header_row_2 str/HTML Text fallback line 2
footer_title, footer_left, footer_right str Legacy Flask-era fields — not used
header_background, footer_background str Legacy — not used; do not add to new templates

Network / WiFi

Field Type Notes
wireless_ssid str WiFi network name — displayed on badge back
wireless_password str WiFi password — displayed on badge back

QR Code Behavior

Field Type Notes
show_qr_front bool (0/1) Show attendee QR code on front of badge
show_qr_back bool (0/1) Show attendee QR code (+ ID text) on back of badge

Badge Type List

Field Type Notes
badge_type_list JSON string List of {code, name} objects for this template

Format:

[
  {"code": "current_member", "name": "Member"},
  {"code": "guest", "name": "Guest"},
  {"code": "staff", "name": "Staff"},
  {"code": "test", "name": "Test"}
]

The badge type footer stripe color is driven by CSS rules targeting the code value as a class on the footer element. Each event/template defines its own list — there is no global default. The component derives this list from the template at render time.

Ticket Definitions

Field Type Notes
ticket_list JSON string List of {num, code, name} for this template's tickets
ticket_1_text ticket_8_text HTML Ticket block HTML printed on badge back

ticket_list format:

[
  {"num": 1, "code": "foundation_reception", "name": "Foundation Reception"},
  {"num": 2, "code": "volunteer_reception", "name": "Volunteer Reception"}
]

The ticket_N_code field on the badge object references a ticket by its code. The corresponding ticket_N_text on the template provides the HTML rendered on the badge.

Print Layout / Styling

Field Type Notes
layout str Layout code — see Layout Codes below
style_filename str CSS filename for locally-served stylesheets
style_href str (URL) Preferred — external URL for custom CSS
script_src str (URL) Do not use — Flask-era arbitrary script injection

Access Control

Field Type Notes
passcode str Shared passcode for template management access
enable bool Standard AE enable flag
hide bool Standard AE hide flag
priority, sort, group int/str Standard AE sort fields
notes str Internal notes

Duplex / Single-Sided

Field Type Notes
duplex bool When false, back section is hidden from print (@media print)

The duplex field controls whether the back-of-badge section renders during printing. When false (single-sided), badge_back gets print:hidden applied so only the front prints. The back section still displays on screen for configuration reference.

duplex is in properties_to_save and show_badge_back is derived from it in ae_comp__badge_obj_view.svelte. (Verified 2026-03-18)

Axonius events use duplex = false — single-sided printing only.


External CSS Approach

Why External

Badge templates may need visual adjustments mid-event (e.g., a color correction, a footer fix) without deploying a new SvelteKit build. Hosting the CSS at an external URL allows changes to take effect on next page load without any deployment.

How It Works

The style_href field contains a full URL to a CSS file hosted on the static server (e.g., https://static.oneskyit.com/c/ISHLT/css/badges_custom_ishlt.css).

The print page (print/+page.svelte) or the badge view should conditionally add a <link> element via <svelte:head> when style_href is populated:

<svelte:head>
    {#if $lq__event_badge_template_obj?.style_href}
        <link
            rel="stylesheet"
            href={$lq__event_badge_template_obj.style_href}
        />
    {/if}
</svelte:head>

This is implemented — style_href loads via <svelte:head> in print/+page.svelte and is included in properties_to_save. (Verified 2026-03-18)

CSS Scope

External badge CSS should scope all rules under .badge_front, .badge_back, etc. to avoid bleeding into the rest of the app. The classes used in ae_comp__badge_obj_view.svelte are the canonical hook points:

  • .badge_front — entire front card
  • .badge_back — entire back card
  • .badge_header — front header area
  • .badge_body — front content area (name, title, affiliations, location)
  • .badge_footer — front footer stripe
  • .badge_back_header — back header area
  • .badge_back_content — back content area
  • .badge_footer_center.<code> — footer text per badge type code (for color stripes)

layout field

The layout field encodes physical badge stock dimensions. Standard codes to use:

Code Dimensions CSS file Description
badge_4x5_fanfold 4" × 5" (101.6 × 127mm) badge_layout_epson_4x5_fanfold.css Epson ColorWorks C3500 / ExpoBadge fanfold — preferred for general conference use (ISHLT, demos)
badge_3.5x5.5_pvc 3.5" × 5.5" (88.9 × 139.7mm) badge_layout_zebra_zc10l_pvc.css PVC card, Zebra ZC10L — single-sided, set duplex=0
badge_4x6_fanfold 4" × 6" (101.6 × 152.4mm) badge_layout_epson_4x6_fanfold.css Single-sided fanfold; Axonius Adapt 2026 (June 2026). Lanyard hole: 5/8in × 1/8in, centered, 1/4in from top.
badge_4x6_fanfold_tickets 4" × 6" + tear-offs (pending) Fanfold with ticket stubs

Layout CSS files live in src/lib/ae_events/badges/css/ and are imported by ae_comp__badge_obj_view.svelte. Rules are scoped under [data-layout="..."] on the wrapper so multiple layouts can coexist in the bundle without conflict.

@page paper size rules are injected per-layout from print/+page.svelte <svelte:head> (attribute selectors cannot scope @page rules, so they're handled dynamically).


cfg_json Reference

All keys are optional. Unknown keys are preserved on save (forward-compatible). Managed via the template form's Advanced and Header & Branding sections, or directly in phpMyAdmin.

Visibility

Key Type Default Notes
hide_badge_header bool false Hides the entire header section (image + logo/text fallback). Auto-true when background_image_path is set, unless explicitly overridden.
hide_badge_footer bool false Hides the badge type footer stripe.
hide_title bool false Suppresses the professional title field on the badge front.
hide_affiliations bool false Suppresses the affiliations field.
hide_location bool false Suppresses the location field.

QR Codes

These keys override the top-level DB fields (show_qr_front, show_qr_back) when present. Prefer setting them here rather than the top-level fields.

Key Type Default Notes
show_qr_front bool false Show attendee QR on badge front.
show_qr_back bool true Show attendee QR (+ ID text) on badge back.

Text Alignment

Stored under a nested align object.

"align": { "name": "left", "title": "left", "affiliations": "left", "location": "center" }
Key Values Default
align.name left | center | right | justify center
align.title left | center | right | justify center
align.affiliations left | center | right | justify center
align.location left | center | right | justify center

QR alignment stored under qr_alignment:

Key Values Default
qr_alignment.front left | center | right center
qr_alignment.back left | center | right | justify center

Header Image

Key Type Default Notes
header_margin_top CSS length 2rem Vertical offset of the header image. Negative = shift up. e.g. "-0.25in", "1rem".
header_border_color hex color none Bottom border drawn below the header div. Empty = no border. e.g. "#FE6111".
header_border_width CSS length 2px Thickness of the header bottom border. Only applied when header_border_color is set.
header_padding_bottom CSS length none Space between the header image and the bottom border line. e.g. "1.45in".

Appearance

Key Type Default Notes
body_text_color hex color #000000 Inline color applied to all badge body text.
bleed CSS length none Extends background image past card edges on all sides. Prevents white borders on printers that clip slightly inside the card. e.g. "0.125in", "3mm".

Text Zone Heights (fit_heights)

Per-layout height overrides for the auto-scaling text zones. Set any subset — unset keys fall back to the layout default. Useful when background_image_path is set and the designed zones don't align with code defaults.

"fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" }
Key Notes
grp_name_title Height of the name+title container
grp_name_title_flex Flex distribution: around | between | even | center | start | end
name Height of the name text zone
title Height of the title text zone
grp_aff_loc Height of the affiliations+location container
grp_aff_loc_flex Flex distribution (same values as above)
affiliations Height of the affiliations text zone
location Height of the location text zone

Punch-Out Hole Markers (punch_holes)

Enables X overlays at the physical badge clip slot positions. Slots are pre-perforated on the badge stock — the markers print on the badge so attendees know where to push them out.

Slot dimensions: 5/8″ wide × 1/8″ tall, 1/4″ from top edge, 3/8″ from left/right edges. Center slot is horizontally centered.

"punch_holes": { "left": true, "right": true, "center": false }
Key Default Notes
punch_holes.left false Left clip slot marker
punch_holes.right false Right clip slot marker
punch_holes.center false Center clip slot marker (less common)

Controls Panel (controls_cfg)

Controls which fields appear in the print controls panel for non-trusted users, and which fields authenticated users may edit. Trusted + Edit Mode always sees and can edit all fields regardless of this config.

"controls_cfg": {
  "shown": ["name", "title", "affiliations"],
  "auth_editable": ["title", "affiliations", "location"]
}
Key Type Default
controls_cfg.shown string[] ["name", "title", "affiliations", "location"]
controls_cfg.auth_editable string[] ["title", "affiliations", "location", "allow_tracking", "pronouns"]

Valid field keys: name, title, affiliations, location, pronouns, allow_tracking.


Template-Derived Features (component behavior)

badge_type_list → badge type select

The badge type dropdown shown when editing a badge is derived from the template's badge_type_list JSON, not a hardcoded list. This was a bug (fixed 2026-03-02). See ae_comp__badge_obj_view.sveltebadge_type_code_li is now $derived.by().

"Info section" flags (exhibitor_info, presenter_info, etc.)

These flags (exhibitor_info, presenter_info, staff_info, vip_info, vote_info) do not exist as DB columns. They appeared as placeholder {#if} blocks in the badge view component from Flask-era development and were never implemented.

The correct approach is one template per badge audience — an Exhibitor template will have exhibitor-specific badge_type_list, header images, and CSS. No flags needed.

The dead {#if $lq__event_badge_template_obj.exhibitor_info} blocks in ae_comp__badge_obj_view.svelte should be removed in a future cleanup pass.


Properties Saved to IDB (Dexie)

The properties_to_save array in ae_events__event_badge_template.ts controls what gets cached locally. Current state — fields NOT in properties_to_save that exist in DB and may be needed:

  • passcode — not needed client-side
  • footer_title, footer_left, footer_right — not needed (legacy)
  • header_background, footer_background — not needed (legacy)
  • script_src — do not add; this field should not be used
  • duplexadd when backend adds the field

Standard Template Setup (per event)

1. General Attendees template

  • header_path: event-specific conference header image
  • secondary_header_path: back-of-badge header (often same or related image)
  • wireless_ssid + wireless_password: venue WiFi
  • show_qr_back: 1 (back QR is standard for most events)
  • show_qr_front: 0 (usually off for front)
  • badge_type_list: full list of member/guest/staff/test types
  • ticket_list + ticket_N_text: event-specific tickets if applicable
  • style_href: client-specific CSS URL
  • layout: appropriate layout code
  • duplex: 1 (or 0 for single-sided events like Axonius 2026)

2. Workshop / Pre-conference template

  • Same as above but with workshop-specific header images
  • badge_type_list: reduced list (workshop-relevant types only)
  • ticket_list: may be empty []
  • duplex: match main template

3. Exhibitor template

  • Exhibitor-specific header images
  • badge_type_list: exhibitor-only types (ex_all, ex_booth, guest, staff, test)
  • ticket_list: [] (exhibitors typically don't have event tickets)
  • show_qr_back: may be 0 (exhibitors scan others, they don't need their own QR prominent)
  • wireless_ssid + wireless_password: same venue WiFi

Print Layout Architecture

How the print CSS works

The print page (print/+page.svelte) injects <style> blocks into <svelte:head> that take effect only in @media print. Multiple layers of the SvelteKit layout chain must be neutered to get a clean print surface.

#ae_main_content — cannot dissolve, must passthrough: #ae_main_content has overflow: auto (it is the events layout scroll container). CSS spec prohibits display: contents from overriding elements with overflow clipping — Firefox enforces this strictly, Chrome is lenient. Workaround: strip all its visual/layout effects with explicit display: block; overflow: visible; position: static; width: 100%.

Wrappers dissolved via display: contents (safe — no overflow constraints):

Selector Source Why dissolved
.main_content events/+layout.svelte pb-48, pt-20+, grow
#badge_render_area print/+page.svelte Screen-only right-padding offset for controls panel

App chrome hidden via print:hidden:

  • nav.submenu (events layout nav bar)
  • footer.footer (events layout footer)
  • Scroll-to-top / scroll-to-bottom button div
  • Kiosk header (<header> in print page)
  • Controls panel (<div> fixed right in print page)
  • Debug info section (edit mode only)
  • Root layout: offline banner, session expired banner, hydration overlay, sys/debug menus

Badge centering — position: fixed: .event_badge_wrapper uses position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%). In print, position: fixed anchors relative to the @page content area, bypassing the entire ancestor hierarchy (no containing-block height dependency, no overflow-clip interference).

Future per-template margins: print_margin_cfg is already parsed from cfg_json in print/+page.svelte. A dynamic @page { margin: ... } injection can be built from that value when a UI for it exists.


Cross-browser print behavior — IMPORTANT

Verified 2026-03-12 by comparing print-to-PDF output from both browsers across multiple print dialog settings.

@page { size } — paper size

Browser Save to PDF Physical printer
Firefox Paper size locked — cannot change in dialog; CSS @page { size } used Can select paper size in dialog
Chrome/Chromium Paper size locked — cannot change in dialog; uses system default (letter, A4, etc.) Can select paper size under "More settings"

Chrome intentionally does not honor @page { size } for Save as PDF. It uses the system default paper size. This is a Chrome design decision, not a bug in our code.

For actual printing to Epson/Zebra hardware: the printer driver controls paper size from the loaded badge stock. CSS @page { size } is advisory only. Real badge printing is unaffected by Chrome's behavior.

Use Firefox for accurate print-to-PDF proofing — it produces a correctly-sized PDF that matches the badge stock dimensions exactly.

Margins — Chrome "Default" causes layout problems

Chrome margin setting Result
Default Adds URL, date, and page-number headers/footers into the printable area. These eat into the space that position: fixed; top: 50% references, making the badge appear off-center or clipped against the footer.
None Correct — badge centered cleanly
Minimum Correct — small margins, badge still centered
Custom (reasonable values) Correct

The badge content itself is not distorted. Verified: Chrome "None" margins on an A4 page produces the badge perfectly horizontally centered (page center 297.5 pts, badge content center 297.5 pts). The CSS centering logic is correct.

Staff guidance for Chrome:

  • Set Margins → None (or Minimum) in Chrome's print dialog.
  • Optionally set paper size to match badge stock under "More settings" when printing to PDF.
  • For physical printer: select correct paper size under "More settings".

Firefox users can use "Save to PDF" directly — it just works.


File Role
ae_events__event_badge_template.ts API + IDB functions; properties_to_save
db_events.ts Dexie schema for badge_template table
templates/+page.svelte Template list + create/edit/delete UI
templates/ae_comp__badge_template_form.svelte Template create/edit form
[badge_id]/ae_comp__badge_obj_view.svelte Badge render — consumes template data
[badge_id]/print/+page.svelte Print page — loads template, hosts <svelte:head> CSS
documentation/MODULE__AE_Events_Badges.md Badge object reference

Pending / TODO

  • Wire style_href via <svelte:head> in print page — done in print/+page.svelte; also in properties_to_save. (2026-03-18 verified)
  • Add duplex to properties_to_save — done. (2026-03-18 verified)
  • Add duplex-driven suppression to badge_back section — done in ae_comp__badge_obj_view.svelte; show_badge_back derived from duplex field.
  • badge_4x6_fanfold layout CSS created (badge_layout_epson_4x6_fanfold.css), imported in badge component, @page 4in 6in wired in print page. (2026-05-15)
  • Template form expanded — layout, style_href, badge_type_list, duplex, and all cfg_json keys now editable via the form. (2026-06-04)
  • cfg_json.header_margin_top, header_border_color, header_border_width, header_padding_bottom added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
  • Wire badge_type_list from the template into the badge search filter — currently the search form uses a hardcoded list. See ae_comp__badge_search.svelte TODO comment.
  • badge_4x5_fanfold layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
  • 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)