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>
This commit is contained in:
Scott Idem
2026-06-04 19:52:59 -04:00
parent 3ae5b30c37
commit 7bf76bf766
4 changed files with 111 additions and 0 deletions

View File

@@ -274,6 +274,24 @@ Per-layout height overrides for the auto-scaling text zones. Set any subset —
| `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.
```json
"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.

View File

@@ -52,6 +52,16 @@ export interface BadgeTemplateCfg {
// Any CSS length: "0.5in", "1rem". Unset = no extra padding.
header_padding_bottom?: string;
// Punch-out hole markers: show X overlays at the physical badge clip slot positions.
// Slots are pre-perforated on the badge stock — markers guide attendees to push them out.
// Hole dimensions: 5/8in wide × 1/8in tall, 1/4in from top, 3/8in from left/right edges.
// Center hole: horizontally centered, same vertical position.
punch_holes?: {
left?: boolean;
right?: boolean;
center?: boolean;
};
// Allow arbitrary extra keys to preserve forward-compatibility.
[key: string]: any;
}

View File

@@ -326,6 +326,12 @@ let header_margin_top = $derived.by(() => {
return v && typeof v === 'string' && v.trim() ? v.trim() : '2rem';
});
// Punch-out hole markers: which of the three physical badge clip slots to mark.
// Holes are 5/8in × 1/8in, 1/4in from top, 3/8in from left/right edges (center: horizontally centered).
let punch_holes_left = $derived(!!template_cfg?.punch_holes?.left);
let punch_holes_right = $derived(!!template_cfg?.punch_holes?.right);
let punch_holes_center = $derived(!!template_cfg?.punch_holes?.center);
// Full inline style string for the badge_header div.
// Combines margin-top, optional bottom border (color + width), and optional bottom padding.
// Unset border_color = no border drawn.
@@ -662,6 +668,43 @@ const code_to_icon: {
{#if show_badge_back}Front of badge{:else}Badge preview{/if}
</span>
<!-- Punch-out hole markers: X overlays at physical badge clip slot positions.
Slots: 5/8in wide × 1/8in tall, 1/4in from top, 3/8in from left/right edges.
Center slot: horizontally centered. Markers print so attendees know to push them out. -->
{#if punch_holes_left}
<div class="punch_hole_marker pointer-events-none absolute"
aria-hidden="true"
style="top: 0.25in; left: 0.375in; width: 0.625in; height: 0.125in;">
<svg width="100%" height="100%" viewBox="0 0 50 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="9" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
<line x1="0" y1="0" x2="50" y2="10" stroke="#777" stroke-width="1"/>
<line x1="50" y1="0" x2="0" y2="10" stroke="#777" stroke-width="1"/>
</svg>
</div>
{/if}
{#if punch_holes_right}
<div class="punch_hole_marker pointer-events-none absolute"
aria-hidden="true"
style="top: 0.25in; right: 0.375in; width: 0.625in; height: 0.125in;">
<svg width="100%" height="100%" viewBox="0 0 50 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="9" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
<line x1="0" y1="0" x2="50" y2="10" stroke="#777" stroke-width="1"/>
<line x1="50" y1="0" x2="0" y2="10" stroke="#777" stroke-width="1"/>
</svg>
</div>
{/if}
{#if punch_holes_center}
<div class="punch_hole_marker pointer-events-none absolute"
aria-hidden="true"
style="top: 0.25in; left: 50%; transform: translateX(-50%); width: 0.625in; height: 0.125in;">
<svg width="100%" height="100%" viewBox="0 0 50 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="9" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
<line x1="0" y1="0" x2="50" y2="10" stroke="#777" stroke-width="1"/>
<line x1="50" y1="0" x2="0" y2="10" stroke="#777" stroke-width="1"/>
</svg>
</div>
{/if}
<!-- Background image bleed container.
Absolutely positioned so it can extend past badge_front's edges by bleed_offset
(e.g. "-0.125in" on all sides). z-index: -1 keeps it behind all badge content.

View File

@@ -67,6 +67,10 @@ let cfg_body_text_color = $state('#000000');
let cfg_bleed = $state('');
// Header image vertical offset (CSS length). Empty = default 2rem. Negative = shift up.
let cfg_header_margin_top = $state('');
// Punch-out hole markers (left/right/center badge clip slots).
let cfg_punch_holes_left = $state(false);
let cfg_punch_holes_right = $state(false);
let cfg_punch_holes_center = $state(false);
// Header bottom border. Empty color = no border.
let cfg_header_border_color = $state('');
let cfg_header_border_width = $state('');
@@ -168,6 +172,9 @@ async function load_template(id: string) {
// Background bleed (CSS length, e.g. "0.125in"). Empty = no bleed.
cfg_bleed = parsed_cfg.bleed ?? '';
cfg_header_margin_top = parsed_cfg.header_margin_top ?? '';
cfg_punch_holes_left = parsed_cfg?.punch_holes?.left ?? false;
cfg_punch_holes_right = parsed_cfg?.punch_holes?.right ?? false;
cfg_punch_holes_center = parsed_cfg?.punch_holes?.center ?? false;
cfg_header_border_color = parsed_cfg.header_border_color ?? '';
cfg_header_border_width = parsed_cfg.header_border_width ?? '';
cfg_header_padding_bottom = parsed_cfg.header_padding_bottom ?? '';
@@ -249,6 +256,17 @@ async function handle_submit() {
delete cfg_obj.bleed;
}
// Punch-out hole markers: save the nested object only when at least one is enabled
if (cfg_punch_holes_left || cfg_punch_holes_right || cfg_punch_holes_center) {
cfg_obj.punch_holes = {
left: cfg_punch_holes_left,
right: cfg_punch_holes_right,
center: cfg_punch_holes_center
};
} else {
delete cfg_obj.punch_holes;
}
// Header image vertical offset: save if set, remove key when cleared (falls back to 2rem default)
if (cfg_header_margin_top.trim()) {
cfg_obj.header_margin_top = cfg_header_margin_top.trim();
@@ -381,6 +399,28 @@ function toggle_cfg_controls_auth_editable(key: string) {
<span>Header Path (URL) — top banner image (used when no background image)</span>
<input type="text" bind:value={header_path} class="input" />
</label>
<div class="space-y-1">
<p class="text-sm font-medium">Punch-Out Hole Markers</p>
<p class="text-xs text-surface-400 italic">
Show X overlays at the physical badge clip slot positions (5/8″ × 1/8″ slots, 1/4″ from top).
Markers help attendees know where to push out the perforations.
</p>
<div class="flex flex-wrap gap-4">
<label class="label flex items-center gap-2">
<input type="checkbox" bind:checked={cfg_punch_holes_left} class="checkbox" />
<span>Left slot</span>
</label>
<label class="label flex items-center gap-2">
<input type="checkbox" bind:checked={cfg_punch_holes_right} class="checkbox" />
<span>Right slot</span>
</label>
<label class="label flex items-center gap-2">
<input type="checkbox" bind:checked={cfg_punch_holes_center} class="checkbox" />
<span>Center slot</span>
</label>
</div>
</div>
<label class="label">
<span>Header Image Vertical Offset</span>
<input type="text" bind:value={cfg_header_margin_top} class="input" placeholder="e.g. 0.5in, -4px, 1rem (default: 2rem)" />