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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill={punch_holes_colors.left.bg} stroke={punch_holes_colors.left.fg} stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke={punch_holes_colors.left.fg} stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke={punch_holes_colors.left.fg} stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/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;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill={punch_holes_colors.right.bg} stroke={punch_holes_colors.right.fg} stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke={punch_holes_colors.right.fg} stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke={punch_holes_colors.right.fg} stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/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;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill="rgba(255,255,255,0.4)" stroke="#777" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke="#777" stroke-width="1"/>
|
||||
<rect x="0.5" y="0.5" width="49" height="7" fill={punch_holes_colors.center.bg} stroke={punch_holes_colors.center.fg} stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<line x1="0" y1="0" x2="50" y2="8" stroke={punch_holes_colors.center.fg} stroke-width="1"/>
|
||||
<line x1="50" y1="0" x2="0" y2="8" stroke={punch_holes_colors.center.fg} stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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<BadgeTemplateCfg['punch_holes']> = {
|
||||
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) {
|
||||
<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">
|
||||
<div class="space-y-2">
|
||||
<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.
|
||||
Leave colors blank to use defaults (gray stroke, semi-transparent white fill).
|
||||
</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>
|
||||
{#snippet hole_colors(label: string, enabled: boolean, fg: string, bg: string, on_fg: (v: string) => void, on_bg: (v: string) => void)}
|
||||
<div class="space-y-1 rounded border border-surface-200-800 p-2">
|
||||
<label class="label flex items-center gap-2 font-medium">
|
||||
<input type="checkbox" checked={enabled} onchange={(e) => { if (e.target instanceof HTMLInputElement) { if (label === 'Left') cfg_punch_holes_left = e.target.checked; else if (label === 'Right') cfg_punch_holes_right = e.target.checked; else cfg_punch_holes_center = e.target.checked; } }} class="checkbox" />
|
||||
<span>{label} slot</span>
|
||||
</label>
|
||||
{#if enabled}
|
||||
<div class="grid grid-cols-2 gap-2 pl-6">
|
||||
<label class="label">
|
||||
<span class="text-xs">Stroke / line (fg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={fg || '#777777'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={fg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="input input-sm w-24" placeholder="#777777" />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs">Fill (bg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={bg || '#ffffff'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={bg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="input input-sm w-24" placeholder="#ffffff" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/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)}
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
|
||||
Reference in New Issue
Block a user