- Replace ae_comp__badge_obj_view_v2 with ae_comp__badge_obj_view (consolidated component) - Add hide-chrome toggle ([H] shortcut + button) to hide site nav/footer/sys bar for clean print workspace — syncs $ae_loc.sys_menu.hide + $ae_sess.disable_sys_nav/footer with restore-on-unmount - Add banner_full_width toggle (default true=100% width, false=natural pixel size for calibration) - Center badge header image (display:block; margin:0 auto) — was left-aligned when narrower than badge - Fix controls panel overlap: move from bottom-0 to bottom-24 to clear sys bar (84px tall) - Add [H] keyboard shortcut for chrome toggle (guards against focus in inputs) - Persist hide_chrome and banner_full_width in ae_badge_print_tweaks localStorage key - Add sample header image assets (calibration SVG/PNG, hex blue SVG/PNG, demo PNG) - Update badge PVC CSS layout and module docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
365 lines
16 KiB
Markdown
365 lines
16 KiB
Markdown
# 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 1–3 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:**
|
||
```json
|
||
[
|
||
{"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:**
|
||
```json
|
||
[
|
||
{"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 |
|
||
|
||
### New Field (pending backend addition)
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `duplex` | bool | **Planned** — 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.
|
||
|
||
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
|
||
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
|
||
|
||
---
|
||
|
||
## 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
|
||
<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 not yet implemented — tracked as a pending Phase 1 item.
|
||
|
||
### 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) | *(none — Tailwind defaults)* | Generic fanfold fallback; dimensions match the hardcoded Tailwind values |
|
||
| `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).
|
||
|
||
---
|
||
|
||
## 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.svelte` — `badge_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:
|
||
|
||
- `style_href` — needed once external CSS is wired via `<svelte:head>`
|
||
- `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
|
||
- `duplex` — **add 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.
|
||
|
||
---
|
||
|
||
## Related Files
|
||
|
||
| 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
|
||
|
||
- [x] Wire `style_href` via `<svelte:head>` 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.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.svelte` (if they were carried over from v1)
|
||
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
|