feat: badge print — two-line name toggle + leading-none tightening + calibration SVG
- Add name_two_lines toggle (default: true) — uses CSS horizontal padding scaled by character count to coax short names (e.g. "Scott Idem") into a natural two-line wrap without a hard <br>; three tiers: ≤12 chars (18%), ≤20 (8%), ≤28 (2%), >28 no pad - Inner <div> (block element) used inside Element_fit_text for class: directives — Svelte scoped CSS requires static class names in the template; dynamic strings and class: on component elements both fail to match scoped CSS rules - Add leading-none to all four Element_fit_text fields (name, title, affiliations, location) — line-height must be set at the wrapper div level where fit_text measures scrollHeight, otherwise the binary-search scaler returns inflated sizes - name_two_lines state persisted to localStorage (ae_badge_print_tweaks key) alongside existing print_offset, hide_chrome, and banner_full_width tweaks - Rewrite badge_header_calibration.svg as a precise SVG ruler with labeled tick marks (major at 1in intervals, minor at 0.25in) for accurate physical print calibration - Gate debug outline CSS on html.debug_outlines class (set by controls panel) so outlines never appear in normal print mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,13 @@
|
||||
* Set false via the controls panel to see the image at its natural size (dev/testing).
|
||||
*/
|
||||
banner_full_width?: boolean;
|
||||
/**
|
||||
* When true (default), the person's name is split at the last space and rendered
|
||||
* on two lines. Gives short names like "Scott Idem" the same two-line visual weight
|
||||
* as long names. Set false for names that genuinely read better on one line.
|
||||
* Toggle lives in the controls panel; persisted in localStorage per workstation.
|
||||
*/
|
||||
name_two_lines?: boolean;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
@@ -58,6 +65,7 @@
|
||||
font_size_location,
|
||||
preview_overrides = null,
|
||||
banner_full_width = true,
|
||||
name_two_lines = true,
|
||||
}: Props = $props();
|
||||
|
||||
// Badge layout CSS — compiled in, hot-reloads in dev.
|
||||
@@ -102,6 +110,19 @@
|
||||
let display_name = $derived(
|
||||
eff_badge?.full_name_override ?? eff_badge?.full_name ?? ''
|
||||
);
|
||||
|
||||
// Two-line name padding: when name_two_lines is on, apply horizontal padding scaled
|
||||
// to name length so CSS word-wrap does the break naturally — no hard <br> needed.
|
||||
// Short names (≤12 chars) get heavy padding to force a break at the space.
|
||||
// Progressively less padding as names get longer; very long names wrap on their own.
|
||||
// Breakpoints are character-count heuristics — adjust if specific names look off.
|
||||
// name_pad_* — three tiers of horizontal padding to coax short names into two lines.
|
||||
// Svelte scopes component style rules via compile-time class name hashing; dynamic class
|
||||
// strings are invisible to the compiler so the hash never matches. class:name={cond} is the
|
||||
// correct pattern — the literal class names appear in the template for the compiler to find.
|
||||
let name_pad_short = $derived(name_two_lines && display_name.trim().length <= 12);
|
||||
let name_pad_mid = $derived(name_two_lines && display_name.trim().length > 12 && display_name.trim().length <= 20);
|
||||
let name_pad_long = $derived(name_two_lines && display_name.trim().length > 20 && display_name.trim().length <= 28);
|
||||
let display_title = $derived(
|
||||
eff_badge?.professional_title_override ?? eff_badge?.professional_title ?? ''
|
||||
);
|
||||
@@ -258,20 +279,20 @@
|
||||
const [bg, fg] = palettes[effective_badge_type_code?.toLowerCase() ?? ''] ?? ['#f1f5f9', '#64748b'];
|
||||
// Each entry: [svg string, tile size]. Swirls use a larger tile for smoother repeats.
|
||||
const patterns: [string, string][] = [
|
||||
// 1: diagonal stripes
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='0' y1='24' x2='24' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='-12' y1='24' x2='12' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='12' y1='24' x2='36' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
|
||||
// 2: polka dots
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><circle cx='12' cy='12' r='5' fill='${fg}' fill-opacity='0.35'/></svg>`, '24px 24px'],
|
||||
// 3: diamonds
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><polygon points='12,3 21,12 12,21 3,12' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
|
||||
// 4: grid
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='12' y1='0' x2='12' y2='24' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/><line x1='0' y1='12' x2='24' y2='12' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/></svg>`, '24px 24px'],
|
||||
// 5: swirls — two crossing S-curves (yin-yang tiling) + echo curves for depth.
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'><rect width='64' height='64' fill='${bg}'/><path d='M0,0 C0,32 64,32 64,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M64,0 C64,32 0,32 0,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M0,16 C16,32 48,32 64,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><path d='M64,16 C48,32 16,32 0,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><circle cx='32' cy='32' r='4' fill='${fg}' fill-opacity='0.18'/></svg>`, '64px 64px'],
|
||||
// 6: Inch Calibration Grid (1 inch squares, 0.25 inch subdivisions).
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='96' height='96'><rect width='96' height='96' fill='white' fill-opacity='0.9'/><path d='M24 0 v96 M48 0 v96 M72 0 v96 M0 24 h96 M0 48 h96 M0 72 h96' stroke='${fg}' stroke-width='0.5' stroke-opacity='0.2'/><rect width='96' height='96' fill='none' stroke='${fg}' stroke-width='1' stroke-opacity='0.5'/><text x='2' y='10' font-family='monospace' font-size='8' fill='${fg}' opacity='0.5'>1in</text></svg>`, '96px 96px'],
|
||||
// 7: Metric Calibration Grid (10mm squares, 1mm subdivisions).
|
||||
// 1: Metric Calibration Grid (10mm squares, 1mm subdivisions).
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='37.795' height='37.795'><rect width='37.795' height='37.795' fill='white' fill-opacity='0.9'/><path d='M3.78 0 v37.8 M7.56 0 v37.8 M11.34 0 v37.8 M15.12 0 v37.8 M18.9 0 v37.8 M22.68 0 v37.8 M26.46 0 v37.8 M30.24 0 v37.8 M34.02 0 v37.8 M0 3.78 h37.8 M0 7.56 h37.8 M0 11.34 h37.8 M0 15.12 h37.8 M0 18.9 h37.8 M0 22.68 h37.8 M0 26.46 h37.8 M0 30.24 h37.8 M0 34.02 h37.8' stroke='${fg}' stroke-width='0.25' stroke-opacity='0.2'/><rect width='37.795' height='37.795' fill='none' stroke='${fg}' stroke-width='0.8' stroke-opacity='0.5'/><text x='2' y='8' font-family='monospace' font-size='6' fill='${fg}' opacity='0.5'>1cm</text></svg>`, '37.795px 37.795px'],
|
||||
// 2: Inch Calibration Grid (1 inch squares, 0.25 inch subdivisions).
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='96' height='96'><rect width='96' height='96' fill='white' fill-opacity='0.9'/><path d='M24 0 v96 M48 0 v96 M72 0 v96 M0 24 h96 M0 48 h96 M0 72 h96' stroke='${fg}' stroke-width='0.5' stroke-opacity='0.2'/><rect width='96' height='96' fill='none' stroke='${fg}' stroke-width='1' stroke-opacity='0.5'/><text x='2' y='10' font-family='monospace' font-size='8' fill='${fg}' opacity='0.5'>1in</text></svg>`, '96px 96px'],
|
||||
// 3: swirls — two crossing S-curves (yin-yang tiling) + echo curves for depth.
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'><rect width='64' height='64' fill='${bg}'/><path d='M0,0 C0,32 64,32 64,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M64,0 C64,32 0,32 0,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M0,16 C16,32 48,32 64,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><path d='M64,16 C48,32 16,32 0,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><circle cx='32' cy='32' r='4' fill='${fg}' fill-opacity='0.18'/></svg>`, '64px 64px'],
|
||||
// 4: diagonal stripes
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='0' y1='24' x2='24' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='-12' y1='24' x2='12' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='12' y1='24' x2='36' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
|
||||
// 5: polka dots
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><circle cx='12' cy='12' r='5' fill='${fg}' fill-opacity='0.35'/></svg>`, '24px 24px'],
|
||||
// 6: diamonds
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><polygon points='12,3 21,12 12,21 3,12' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
|
||||
// 7: grid
|
||||
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='12' y1='0' x2='12' y2='24' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/><line x1='0' y1='12' x2='24' y2='12' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/></svg>`, '24px 24px'],
|
||||
];
|
||||
const [svg, size] = patterns[(demo_bg_state - 1) % DEMO_BG_PATTERNS];
|
||||
return `background-image: url("data:image/svg+xml,${encodeURIComponent(svg)}"); background-repeat: repeat; background-size: ${size};`;
|
||||
@@ -336,7 +357,7 @@
|
||||
onclick={cycle_demo_bg}
|
||||
title="Cycle demo background pattern — edit mode only"
|
||||
>
|
||||
🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls','in-grid','mm-grid'][demo_bg_state - 1] : 'demo bg'}
|
||||
🎨 {demo_bg_state > 0 ? ['mm-grid','in-grid','swirls','stripes','dots','diamonds','grid'][demo_bg_state - 1] : 'demo bg'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -348,7 +369,6 @@
|
||||
flex flex-row flex-wrap gap-4
|
||||
items-stretch justify-center
|
||||
p-2 mx-auto
|
||||
outline-2 outline-dashed outline-blue-500
|
||||
min-h-[6.0in]
|
||||
max-w-[8.5in] overflow-visible
|
||||
"
|
||||
@@ -367,7 +387,7 @@
|
||||
p-0 m-0 overflow-visible
|
||||
text-center
|
||||
relative
|
||||
outline-4 outline-red-500/50 hover:outline-red-700/75
|
||||
hover:outline-2 hover:outline-dashed hover:outline-red-500/75
|
||||
group
|
||||
"
|
||||
style={demo_bg_style}
|
||||
@@ -455,15 +475,20 @@
|
||||
max={80}
|
||||
manual_size={font_size_name ?? null}
|
||||
height={fit_heights.name}
|
||||
class="full_name_override_all hover:bg-pink-100/50"
|
||||
class="full_name_override_all hover:bg-pink-100/50 leading-none"
|
||||
>
|
||||
<span class="full_name_override">
|
||||
<div
|
||||
class="full_name_override"
|
||||
class:name_pad_short={name_pad_short}
|
||||
class:name_pad_mid={name_pad_mid}
|
||||
class:name_pad_long={name_pad_long}
|
||||
>
|
||||
{#if display_name}
|
||||
{@html display_name.trim()}
|
||||
{display_name.trim()}
|
||||
{:else}
|
||||
-- no name --
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</Element_fit_text>
|
||||
|
||||
{#if display_title}
|
||||
@@ -476,7 +501,7 @@
|
||||
max={38}
|
||||
manual_size={font_size_title ?? null}
|
||||
height={fit_heights.title}
|
||||
class="professional_title italic hover:bg-pink-100/50"
|
||||
class="professional_title italic hover:bg-pink-100/50 leading-none"
|
||||
>
|
||||
{@html display_title}
|
||||
</Element_fit_text>
|
||||
@@ -508,7 +533,7 @@
|
||||
max={40}
|
||||
manual_size={font_size_affiliations ?? null}
|
||||
height={fit_heights.affiliations}
|
||||
class="affiliations hover:bg-pink-100/50"
|
||||
class="affiliations hover:bg-pink-100/50 leading-none"
|
||||
>
|
||||
{@html display_affiliations}
|
||||
</Element_fit_text>
|
||||
@@ -524,7 +549,7 @@
|
||||
max={34}
|
||||
manual_size={font_size_location ?? null}
|
||||
height={fit_heights.location}
|
||||
class="location hover:bg-pink-100/50"
|
||||
class="location hover:bg-pink-100/50 leading-none"
|
||||
>
|
||||
<span class="city state_province country"
|
||||
>{@html display_location}</span
|
||||
@@ -619,7 +644,7 @@
|
||||
p-0 m-0 overflow-visible
|
||||
text-left text-sm
|
||||
relative
|
||||
outline-4 outline-green-500/50 hover:outline-green-700/75
|
||||
hover:outline-2 hover:outline-dashed hover:outline-green-500/75
|
||||
group
|
||||
"
|
||||
>
|
||||
@@ -974,6 +999,7 @@
|
||||
.badge_front,
|
||||
.badge_back {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,8 +1007,37 @@
|
||||
.badge_back {
|
||||
background-color: white;
|
||||
color: #1a1a1a;
|
||||
/*
|
||||
* Standard badge/card corner radius: 3.18mm = 1/8 inch = ~12px at 96dpi.
|
||||
* Matches the physical die-cut corners of the printed card stock.
|
||||
*/
|
||||
border-radius: 12px;
|
||||
/*
|
||||
* Layered card shadow: gives each badge face a strong physical-card feel on screen.
|
||||
* Three layers: crisp base edge → medium spread → wide ambient — creates
|
||||
* convincing depth without looking like a UI box-shadow.
|
||||
* Suppressed on print — no decoration on the physical output.
|
||||
*/
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.20),
|
||||
0 6px 20px rgba(0, 0, 0, 0.14),
|
||||
0 16px 48px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/*
|
||||
* Two-line name padding: applied to the Element_fit_text wrapper div (not the inner span).
|
||||
* Squeezes the available text width so CSS word-wrap breaks the name at its natural space
|
||||
* — no hard <br> needed. Padding on the block div also ensures text-align:center works
|
||||
* correctly for all wrapped lines (inline-element padding breaks centering on each line).
|
||||
* Short names (≤12 chars, e.g. "Scott Idem") need heavy padding to force the break.
|
||||
* Progressively less padding as names get longer; very long names wrap on their own.
|
||||
* Percentages are relative to the Element_fit_text div width, so the effect scales
|
||||
* correctly regardless of badge template or font size.
|
||||
*/
|
||||
.name_pad_short { padding-left: 18%; padding-right: 18%; }
|
||||
.name_pad_mid { padding-left: 8%; padding-right: 8%; }
|
||||
.name_pad_long { padding-left: 2%; padding-right: 2%; }
|
||||
|
||||
/*
|
||||
* Header image: center horizontally within the badge.
|
||||
* <img> defaults to display:inline, which left-aligns any image narrower than
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
* Stored in localStorage.
|
||||
*/
|
||||
banner_full_width?: boolean;
|
||||
/** When true (default), name uses padding to prefer two-line layout. Stored in localStorage. */
|
||||
name_two_lines?: boolean;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
@@ -77,6 +79,7 @@
|
||||
print_offset_y = $bindable(0),
|
||||
hide_chrome = $bindable(false),
|
||||
banner_full_width = $bindable(true),
|
||||
name_two_lines = $bindable(true),
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
|
||||
@@ -929,6 +932,18 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Name layout: two-line (preferred) vs single-line -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={name_two_lines}
|
||||
/>
|
||||
<span class="text-xs">Name: prefer two lines</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Banner width toggle: 100% (production) vs natural size (dev/calibration) -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
|
||||
@@ -122,6 +122,11 @@
|
||||
// Banner width: true = 100% (production), false = natural pixel size (dev/calibration).
|
||||
let banner_full_width: boolean = $state(_saved_tweaks?.banner_full_width ?? true);
|
||||
|
||||
// Name layout: true = prefer two lines (first / last split), false = one line.
|
||||
// Default true — most badge templates look best with the name split across two lines.
|
||||
// Stored in localStorage so the preference persists per workstation across reloads.
|
||||
let name_two_lines: boolean = $state(_saved_tweaks?.name_two_lines ?? true);
|
||||
|
||||
// hide_chrome: hides the page header and sys bar for a clean badge-preview workspace.
|
||||
// Toggled via the [H] keyboard shortcut or the button in the controls panel staff section.
|
||||
let hide_chrome: boolean = $state(_saved_tweaks?.hide_chrome ?? false);
|
||||
@@ -147,7 +152,7 @@
|
||||
|
||||
// Persist all tweaks to localStorage
|
||||
$effect(() => {
|
||||
if (browser) localStorage.setItem(_PRINT_TWEAKS_KEY, JSON.stringify({ x: print_offset_x, y: print_offset_y, hide_chrome, banner_full_width }));
|
||||
if (browser) localStorage.setItem(_PRINT_TWEAKS_KEY, JSON.stringify({ x: print_offset_x, y: print_offset_y, hide_chrome, banner_full_width, name_two_lines }));
|
||||
});
|
||||
|
||||
// Inject print offset as @media print CSS. Uses translate() so the centering
|
||||
@@ -374,6 +379,7 @@
|
||||
font_size_location={font_size_location}
|
||||
{preview_overrides}
|
||||
{banner_full_width}
|
||||
{name_two_lines}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -403,6 +409,7 @@
|
||||
bind:print_offset_y
|
||||
bind:hide_chrome
|
||||
bind:banner_full_width
|
||||
bind:name_two_lines
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user