From 04c204206053ccfe07990ed99adaeafd87531d71 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 4 Jun 2026 20:01:03 -0400 Subject: [PATCH] feat(badges): per-slot fg/bg colors for punch-out hole markers Adds left_fg/left_bg, right_fg/right_bg, center_fg/center_bg to punch_holes cfg_json, plus shared fg/bg fallback. Template form shows color pickers per slot (only when slot is enabled). Defaults: #777777 stroke, rgba white fill. Co-Authored-By: Claude Sonnet 4.6 --- .../ae_events/types/ae_badge_template_cfg.ts | 10 +++ .../[badge_id]/ae_comp__badge_obj_view.svelte | 32 ++++++--- .../ae_comp__badge_template_form.svelte | 68 ++++++++++++++----- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/lib/ae_events/types/ae_badge_template_cfg.ts b/src/lib/ae_events/types/ae_badge_template_cfg.ts index a63bf08a..e39e4d13 100644 --- a/src/lib/ae_events/types/ae_badge_template_cfg.ts +++ b/src/lib/ae_events/types/ae_badge_template_cfg.ts @@ -56,10 +56,20 @@ export interface BadgeTemplateCfg { // 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. + // Colors: per-slot _fg/_bg override the shared fg/bg fallback. Unset = component defaults. + // fg = stroke + line color (hex). bg = rectangle fill color (hex). punch_holes?: { left?: boolean; right?: boolean; center?: boolean; + fg?: string; // shared fallback stroke/line color + bg?: string; // shared fallback fill color + left_fg?: string; + left_bg?: string; + right_fg?: string; + right_bg?: string; + center_fg?: string; + center_bg?: string; }; // Allow arbitrary extra keys to preserve forward-compatibility. diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte index 3c4b2d9b..7c9a3456 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte @@ -332,6 +332,20 @@ 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); +// Per-slot colors: slot-specific override → shared fallback → component default. +const PH_DEFAULT_FG = '#777777'; +const PH_DEFAULT_BG = 'rgba(255,255,255,0.4)'; +let punch_holes_colors = $derived.by(() => { + const ph = template_cfg?.punch_holes; + const shared_fg = ph?.fg || PH_DEFAULT_FG; + const shared_bg = ph?.bg || PH_DEFAULT_BG; + return { + left: { fg: ph?.left_fg || shared_fg, bg: ph?.left_bg || shared_bg }, + right: { fg: ph?.right_fg || shared_fg, bg: ph?.right_bg || shared_bg }, + center: { fg: ph?.center_fg || shared_fg, bg: ph?.center_bg || shared_bg } + }; +}); + // 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. @@ -679,9 +693,9 @@ const code_to_icon: { aria-hidden="true" style="top: calc(0.25in + 1mm); left: calc(0.375in + 1mm); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;"> - - - + + + {/if} @@ -690,9 +704,9 @@ const code_to_icon: { aria-hidden="true" style="top: calc(0.25in + 1mm); right: calc(0.375in + 1mm); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;"> - - - + + + {/if} @@ -701,9 +715,9 @@ const code_to_icon: { aria-hidden="true" style="top: calc(0.25in + 1mm); left: 50%; transform: translateX(-50%); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;"> - - - + + + {/if} diff --git a/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte b/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte index 97c6a504..9dfc91d9 100644 --- a/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte +++ b/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte @@ -71,6 +71,13 @@ let cfg_header_margin_top = $state(''); let cfg_punch_holes_left = $state(false); let cfg_punch_holes_right = $state(false); let cfg_punch_holes_center = $state(false); +// Per-slot colors. Empty = use component default (#777777 fg, semi-transparent white bg). +let cfg_punch_holes_left_fg = $state(''); +let cfg_punch_holes_left_bg = $state(''); +let cfg_punch_holes_right_fg = $state(''); +let cfg_punch_holes_right_bg = $state(''); +let cfg_punch_holes_center_fg = $state(''); +let cfg_punch_holes_center_bg = $state(''); // Header bottom border. Empty color = no border. let cfg_header_border_color = $state(''); let cfg_header_border_width = $state(''); @@ -175,6 +182,12 @@ async function load_template(id: string) { 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_punch_holes_left_fg = parsed_cfg?.punch_holes?.left_fg ?? ''; + cfg_punch_holes_left_bg = parsed_cfg?.punch_holes?.left_bg ?? ''; + cfg_punch_holes_right_fg = parsed_cfg?.punch_holes?.right_fg ?? ''; + cfg_punch_holes_right_bg = parsed_cfg?.punch_holes?.right_bg ?? ''; + cfg_punch_holes_center_fg = parsed_cfg?.punch_holes?.center_fg ?? ''; + cfg_punch_holes_center_bg = parsed_cfg?.punch_holes?.center_bg ?? ''; 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 ?? ''; @@ -258,11 +271,18 @@ async function handle_submit() { // 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 = { + const ph: NonNullable = { left: cfg_punch_holes_left, right: cfg_punch_holes_right, center: cfg_punch_holes_center }; + if (cfg_punch_holes_left_fg.trim()) ph.left_fg = cfg_punch_holes_left_fg.trim(); + if (cfg_punch_holes_left_bg.trim()) ph.left_bg = cfg_punch_holes_left_bg.trim(); + if (cfg_punch_holes_right_fg.trim()) ph.right_fg = cfg_punch_holes_right_fg.trim(); + if (cfg_punch_holes_right_bg.trim()) ph.right_bg = cfg_punch_holes_right_bg.trim(); + if (cfg_punch_holes_center_fg.trim()) ph.center_fg = cfg_punch_holes_center_fg.trim(); + if (cfg_punch_holes_center_bg.trim()) ph.center_bg = cfg_punch_holes_center_bg.trim(); + cfg_obj.punch_holes = ph; } else { delete cfg_obj.punch_holes; } @@ -399,26 +419,42 @@ function toggle_cfg_controls_auth_editable(key: string) { Header Path (URL) — top banner image (used when no background image) -
+

Punch-Out Hole Markers

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. + Leave colors blank to use defaults (gray stroke, semi-transparent white fill).

-
- - - -
+ {#snippet hole_colors(label: string, enabled: boolean, fg: string, bg: string, on_fg: (v: string) => void, on_bg: (v: string) => void)} +
+ + {#if enabled} +
+ + +
+ {/if} +
+ {/snippet} + {@render hole_colors('Left', cfg_punch_holes_left, cfg_punch_holes_left_fg, cfg_punch_holes_left_bg, (v) => cfg_punch_holes_left_fg = v, (v) => cfg_punch_holes_left_bg = v)} + {@render hole_colors('Right', cfg_punch_holes_right, cfg_punch_holes_right_fg, cfg_punch_holes_right_bg, (v) => cfg_punch_holes_right_fg = v, (v) => cfg_punch_holes_right_bg = v)} + {@render hole_colors('Center', cfg_punch_holes_center, cfg_punch_holes_center_fg, cfg_punch_holes_center_bg, (v) => cfg_punch_holes_center_fg = v, (v) => cfg_punch_holes_center_bg = v)}