From b04202ecec87799702b4b739ede2eda4545374a2 Mon Sep 17 00:00:00 2001
From: Scott Idem
Date: Thu, 4 Jun 2026 19:40:18 -0400
Subject: [PATCH] feat(badges): configurable header bottom border and padding
per template
Replaces hardcoded border-bottom/padding-bottom on badge_header div with
cfg_json fields: header_border_color, header_border_width, header_padding_bottom.
Empty color = no border. Template form exposes all three in Header & Branding.
Co-Authored-By: Claude Sonnet 4.6
---
.../ae_events/types/ae_badge_template_cfg.ts | 11 +++-
.../[badge_id]/ae_comp__badge_obj_view.svelte | 31 ++++++++++-
.../ae_comp__badge_template_form.svelte | 52 +++++++++++++++++++
3 files changed, 91 insertions(+), 3 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 14905488..0aca69e5 100644
--- a/src/lib/ae_events/types/ae_badge_template_cfg.ts
+++ b/src/lib/ae_events/types/ae_badge_template_cfg.ts
@@ -39,10 +39,19 @@ export interface BadgeTemplateCfg {
// Header image vertical offset. CSS length applied as margin-top on the badge_header div.
// Default (unset) = "2rem" (matches the prior hardcoded mt-8).
// Negative values shift the image toward the top edge; larger values push it down.
- // Useful when a background image's designed zone doesn't align with the default position.
// Any CSS length works: "-0.5in", "1rem", "8px".
header_margin_top?: string;
+ // Border drawn below the badge header image. Set header_border_color to enable.
+ // Unset = no border (default). Any valid CSS hex color.
+ header_border_color?: string;
+ // Thickness of the header bottom border. Any CSS length. Default "2px" when color is set.
+ header_border_width?: string;
+ // Padding below the header image (inside the badge_header div, above the border).
+ // Useful for creating visual space between the image and the body.
+ // Any CSS length: "0.5in", "1rem". Unset = no extra padding.
+ header_padding_bottom?: string;
+
// Allow arbitrary extra keys to preserve forward-compatibility.
[key: string]: any;
}
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 ae1aec53..81a5d268 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
@@ -326,6 +326,21 @@ let header_margin_top = $derived.by(() => {
return v && typeof v === 'string' && v.trim() ? v.trim() : '2rem';
});
+// 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.
+let header_div_style = $derived.by(() => {
+ const parts: string[] = [`margin-top: ${header_margin_top}`];
+ const color = (template_cfg?.header_border_color ?? '').trim();
+ if (color) {
+ const width = (template_cfg?.header_border_width ?? '').trim() || '2px';
+ parts.push(`border-bottom: ${width} solid ${color}`);
+ }
+ const pb = (template_cfg?.header_padding_bottom ?? '').trim();
+ if (pb) parts.push(`padding-bottom: ${pb}`);
+ return parts.join('; ');
+});
+
/**
* Layout-aware section heights for Element_fit_text.
*
@@ -676,7 +691,7 @@ const code_to_icon: {
p-2
hover:outline-2 hover:outline-gray-500/75 hover:outline-dashed
"
- style="margin-top: {header_margin_top};">
+ style={header_div_style}>
+
{@html $lq__event_badge_template_obj.header_row_1}
+
{@html $lq__event_badge_template_obj.header_row_2}
@@ -732,7 +749,7 @@ const code_to_icon: {
"
style="height: {fit_heights.grp_name_title}; justify-content: {flex_justify(
fit_heights.grp_name_title_flex
- )}">
+ )};">
{@html display_title}
{/if}
@@ -822,6 +840,7 @@ const code_to_icon: {
flex items-center justify-start
"
>
+
{@html display_affiliations}
{/if}
@@ -837,6 +856,7 @@ const code_to_icon: {
manual_size={font_size_location ?? null}
height={fit_heights.location}
class="location leading-none hover:bg-pink-100/50">
+
{@html display_location}
{/if}
@@ -987,9 +1007,11 @@ const code_to_icon: {
alt="check badge logo" />
+
{@html $lq__event_badge_template_obj.header_row_1}
+
{@html $lq__event_badge_template_obj.header_row_2}
@@ -1079,26 +1101,31 @@ const code_to_icon: {
{#if $lq__event_badge_obj.ticket_1_code}
-
+
{@html $lq__event_badge_template_obj.ticket_1_text}
{/if}
{#if $lq__event_badge_obj.ticket_2_code}
-
+
{@html $lq__event_badge_template_obj.ticket_2_text}
{/if}
{#if $lq__event_badge_obj.ticket_3_code}
-
+
{@html $lq__event_badge_template_obj.ticket_3_text}
{/if}
{#if $lq__event_badge_obj.ticket_4_code}
-
+
{@html $lq__event_badge_template_obj.ticket_4_text}
{/if}
{#if $lq__event_badge_obj.ticket_5_code}
-
+
{@html $lq__event_badge_template_obj.ticket_5_text}
{/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 c003c29f..d2c48e0f 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
@@ -67,6 +67,11 @@ 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('');
+// Header bottom border. Empty color = no border.
+let cfg_header_border_color = $state('');
+let cfg_header_border_width = $state('');
+// Padding below the header image (above the border line). Empty = no extra padding.
+let cfg_header_padding_bottom = $state('');
// Alignment overrides: 'left' | 'center' | 'right' | 'justify'
let cfg_align_name = $state('center');
let cfg_align_title = $state('center');
@@ -163,6 +168,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_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 ?? '';
// Alignment overrides (nested under cfg_json.align and cfg_json.qr_alignment)
cfg_align_name = parsed_cfg?.align?.name ?? parsed_cfg.align_name ?? 'center';
@@ -248,6 +256,26 @@ async function handle_submit() {
delete cfg_obj.header_margin_top;
}
+ // Header bottom border: save color + width if color is set, remove both if cleared
+ if (cfg_header_border_color.trim()) {
+ cfg_obj.header_border_color = cfg_header_border_color.trim();
+ if (cfg_header_border_width.trim()) {
+ cfg_obj.header_border_width = cfg_header_border_width.trim();
+ } else {
+ delete cfg_obj.header_border_width;
+ }
+ } else {
+ delete cfg_obj.header_border_color;
+ delete cfg_obj.header_border_width;
+ }
+
+ // Header padding below image
+ if (cfg_header_padding_bottom.trim()) {
+ cfg_obj.header_padding_bottom = cfg_header_padding_bottom.trim();
+ } else {
+ delete cfg_obj.header_padding_bottom;
+ }
+
const data_to_save: key_val = {
name,
background_image_path,
@@ -361,6 +389,30 @@ function toggle_cfg_controls_auth_editable(key: string) {
Any CSS length works. Leave blank to use the default (2rem).
+
+
+