feat: badge print UX improvements — chrome toggle, banner width, overlap fix, header centering
- Replace ae_comp__badge_obj_view_v2 with ae_comp__badge_obj_view (consolidated component) - Add hide-chrome toggle ([H] shortcut + button) to hide site nav/footer/sys bar for clean print workspace — syncs $ae_loc.sys_menu.hide + $ae_sess.disable_sys_nav/footer with restore-on-unmount - Add banner_full_width toggle (default true=100% width, false=natural pixel size for calibration) - Center badge header image (display:block; margin:0 auto) — was left-aligned when narrower than badge - Fix controls panel overlap: move from bottom-0 to bottom-24 to clear sys bar (84px tall) - Add [H] keyboard shortcut for chrome toggle (guards against focus in inputs) - Persist hide_chrome and banner_full_width in ae_badge_print_tweaks localStorage key - Add sample header image assets (calibration SVG/PNG, hex blue SVG/PNG, demo PNG) - Update badge PVC CSS layout and module docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -358,7 +358,7 @@ Firefox users can use "Save to PDF" directly — it just works.
|
||||
|
||||
- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
|
||||
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
|
||||
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view_v2.svelte`; `show_badge_back` derived from `duplex` field. Note: v1 (`ae_comp__badge_obj_view.svelte`) was archived to `~/tmp/gemini_trash/`; v2 is canonical. (2026-03-18 verified)
|
||||
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
|
||||
- [ ] Make `layout` field drive actual card dimensions in the badge component — currently the Zebra ZC10L layout CSS (`badge_layout_zebra_zc10l_pvc.css`) sets dimensions correctly via `[data-layout="..."]` scoping, but fanfold layouts still use Tailwind defaults. Needs proper CSS for each layout code.
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view_v2.svelte` (if they were carried over from v1)
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
|
||||
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
- Once satisfied, staff prints the badge.
|
||||
- The key differentiator vs the review form: **the live badge render** shows exactly how
|
||||
the badge will print. Attendees and staff can see changes immediately.
|
||||
- Component: `ae_comp__badge_obj_view.svelte` / `ae_comp__badge_obj_view_v2.svelte`
|
||||
- Component: `ae_comp__badge_obj_view.svelte`
|
||||
- Route: `/events/[event_id]/badges/[badge_id]/print/`
|
||||
|
||||
### Permission Model — Same Logic, Both Flows
|
||||
@@ -69,8 +69,8 @@ Work needed:
|
||||
or whether it should share/reuse the review form component alongside the badge render.
|
||||
- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`.
|
||||
|
||||
### 1. Auto-Scaling Badge Text (v2) — In Progress
|
||||
`ae_comp__badge_obj_view_v2.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
### 1. Auto-Scaling Badge Text — In Progress
|
||||
`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button.
|
||||
Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges.
|
||||
|
||||
@@ -118,7 +118,7 @@ the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badg
|
||||
**Files created/updated:**
|
||||
- `src/lib/elements/action_fit_text.ts` — Svelte action
|
||||
- `src/lib/elements/element_fit_text.svelte` — Component wrapper
|
||||
- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render (canonical)
|
||||
- `src/routes/events/.../ae_comp__badge_obj_view.svelte` — V2 badge render (canonical)
|
||||
Debug blocks gated behind `$ae_loc.edit_mode` (hidden in production).
|
||||
- `print/+page.svelte` — Always uses v2 now. v1/v2 toggle removed. Header redesigned for kiosk UX.
|
||||
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
width: fit-content;
|
||||
max-width: fit-content;
|
||||
width: 3.5in;
|
||||
max-width: 3.5in;
|
||||
}
|
||||
|
||||
@media print {
|
||||
[data-layout="badge_3.5x5.5_pvc"].event_badge_wrapper {
|
||||
width: 3.5in !important;
|
||||
height: 5.5in !important;
|
||||
max-width: 3.5in !important;
|
||||
max-height: 5.5in !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
* values immediately without waiting for Save + liveQuery round-trip.
|
||||
*/
|
||||
preview_overrides?: Record<string, any> | null;
|
||||
/**
|
||||
* When true (default), header images stretch to 100% badge width (production view).
|
||||
* Set false via the controls panel to see the image at its natural size (dev/testing).
|
||||
*/
|
||||
banner_full_width?: boolean;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
@@ -52,6 +57,7 @@
|
||||
font_size_affiliations,
|
||||
font_size_location,
|
||||
preview_overrides = null,
|
||||
banner_full_width = true,
|
||||
}: Props = $props();
|
||||
|
||||
// Badge layout CSS — compiled in, hot-reloads in dev.
|
||||
@@ -60,6 +66,7 @@
|
||||
import '$lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css';
|
||||
import '$lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css';
|
||||
|
||||
import { untrack } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { core_func } from '$lib/ae_core/ae_core_functions';
|
||||
import { ae_loc, slct } from '$lib/stores/ae_stores';
|
||||
@@ -133,9 +140,6 @@
|
||||
$lq__event_badge_template_obj?.duplex == null || !!$lq__event_badge_template_obj?.duplex
|
||||
);
|
||||
|
||||
// Receipt and ticket sections — disabled pending redesign.
|
||||
let show_receipt = $derived(false);
|
||||
let show_tickets = $derived(false);
|
||||
|
||||
/**
|
||||
* Layout-aware section heights for Element_fit_text.
|
||||
@@ -224,9 +228,17 @@
|
||||
let qr_error_message = $state('');
|
||||
|
||||
// --- Demo background pattern (edit mode only, for preview/demo purposes) ---
|
||||
// 0 = off, 1–4 = pattern variants. Cycles on each button click.
|
||||
// 0 = off, 1–7 = pattern variants. Cycles on each button click.
|
||||
// Automatically resets to 0 when global edit mode is turned off so the pattern
|
||||
// never persists into a live print session.
|
||||
let demo_bg_state = $state(0);
|
||||
const DEMO_BG_PATTERNS = 5;
|
||||
const DEMO_BG_PATTERNS = 7;
|
||||
|
||||
$effect(() => {
|
||||
if (!$ae_loc.edit_mode) {
|
||||
untrack(() => { demo_bg_state = 0; });
|
||||
}
|
||||
});
|
||||
|
||||
function cycle_demo_bg() {
|
||||
demo_bg_state = (demo_bg_state + 1) % (DEMO_BG_PATTERNS + 1);
|
||||
@@ -255,8 +267,11 @@
|
||||
// 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.
|
||||
// Paths use corner-to-corner cubic beziers so opposite tile edges match perfectly.
|
||||
[`<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).
|
||||
[`<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'],
|
||||
];
|
||||
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};`;
|
||||
@@ -279,22 +294,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
/* *** BEGIN *** Legacy ticket/option state — move to template config in future */
|
||||
let option_ticket_1_override = $state('');
|
||||
let option_ticket_2_override = $state('');
|
||||
let option_ticket_3_override = $state('');
|
||||
let option_other_1_override = $state('');
|
||||
let option_other_2_override = $state('');
|
||||
|
||||
let option_ticket_1_display_opt = 'front_bool';
|
||||
let option_ticket_2_display_opt = 'front_bool';
|
||||
let option_ticket_3_display_opt = 'front_bool';
|
||||
|
||||
let option_other_1_display_opt = 'back_html';
|
||||
let option_other_2_display_opt = 'back_html';
|
||||
|
||||
// Maps badge option field values → Lucide icon components.
|
||||
// option_1 = dietary/allergy flag; option_2 = special designation (first-time, etc.)
|
||||
// Maps other_1_code (dietary/allergy) and other_2_code (special designation e.g.
|
||||
// First Time Attendee) values → Lucide icon components for badge display.
|
||||
// WHY: Replaces the old code_to_html FA string map — no FontAwesome dependency.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const code_to_icon: { option_1: Record<string, any>; option_2: Record<string, any> } = {
|
||||
@@ -335,7 +336,7 @@
|
||||
onclick={cycle_demo_bg}
|
||||
title="Cycle demo background pattern — edit mode only"
|
||||
>
|
||||
🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls'][demo_bg_state - 1] : 'demo bg'}
|
||||
🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls','in-grid','mm-grid'][demo_bg_state - 1] : 'demo bg'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -396,6 +397,7 @@
|
||||
>
|
||||
<img
|
||||
class="header_image"
|
||||
class:header_full_width={banner_full_width}
|
||||
src={$lq__event_badge_template_obj.header_path}
|
||||
alt="check header path"
|
||||
/>
|
||||
@@ -532,24 +534,17 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if ['front_bool', 'front_back_bool'].includes(option_ticket_1_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_2_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_3_display_opt) || $lq__event_badge_template_obj?.show_qr_front}
|
||||
<!-- flex-col so ticket icons row and QR stack vertically; QR is centered -->
|
||||
<!-- Ticket indicators (front) + front QR if template enables it.
|
||||
ticket_N_code fields drive the Star icons — empty = no indicator. -->
|
||||
{#if eff_badge?.ticket_1_code || eff_badge?.ticket_2_code || eff_badge?.ticket_3_code || $lq__event_badge_template_obj?.show_qr_front}
|
||||
<div class="special flex flex-col items-center w-full">
|
||||
<div class="flex flex-row justify-between w-full">
|
||||
<span class="badge_body_special_left">
|
||||
{#if option_ticket_1_override}<span
|
||||
class="ticket_1_code fg_green"
|
||||
><Star size="1em" /></span>{/if}
|
||||
{#if eff_badge?.ticket_1_code}<span class="ticket_1_code fg_green"><Star size="1em" /></span>{/if}
|
||||
</span>
|
||||
<span class="badge_body_special_right">
|
||||
{#if option_ticket_2_override}
|
||||
<span class="ticket_2_code fg_red"
|
||||
><Star size="1em" /></span>
|
||||
{/if}
|
||||
{#if option_ticket_3_override}
|
||||
<span class="ticket_3_code fg_blue"
|
||||
><Star size="1em" /></span>
|
||||
{/if}
|
||||
{#if eff_badge?.ticket_2_code}<span class="ticket_2_code fg_red"><Star size="1em" /></span>{/if}
|
||||
{#if eff_badge?.ticket_3_code}<span class="ticket_3_code fg_blue"><Star size="1em" /></span>{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if $lq__event_badge_template_obj?.show_qr_front}
|
||||
@@ -582,14 +577,12 @@
|
||||
"
|
||||
title={effective_badge_type_code}
|
||||
>
|
||||
{#if option_other_1_override && ['front_bool', 'front_back_bool'].includes(option_other_1_display_opt)}
|
||||
<span class="badge_footer_special_left"
|
||||
><Biohazard size="1em" /></span
|
||||
>
|
||||
{:else if option_other_1_override && ['front_html', 'front_back_html'].includes(option_other_1_display_opt)}
|
||||
{@const Icon1 = code_to_icon.option_1[option_other_1_override]}
|
||||
<!-- other_1_code = dietary/allergy flag (left slot).
|
||||
Looks up icon from code_to_icon map; falls back to generic Biohazard. -->
|
||||
{#if eff_badge?.other_1_code}
|
||||
{@const Icon1 = code_to_icon.option_1[eff_badge.other_1_code]}
|
||||
<span class="badge_footer_special_left">
|
||||
{#if Icon1}<Icon1 size="1em" />{/if}
|
||||
{#if Icon1}<Icon1 size="1em" />{:else}<Biohazard size="1em" />{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -600,14 +593,11 @@
|
||||
>{badge_type_name}</span
|
||||
>
|
||||
|
||||
{#if option_other_2_override && ['front_bool', 'front_back_bool'].includes(option_other_2_display_opt)}
|
||||
<span class="badge_footer_special_right"
|
||||
><Asterisk size="1em" /></span
|
||||
>
|
||||
{:else if option_other_2_override && ['front_html', 'front_back_html'].includes(option_other_2_display_opt)}
|
||||
{@const Icon2 = code_to_icon.option_2[option_other_2_override]}
|
||||
<!-- other_2_code = special designation (right slot), e.g. First Time Attendee. -->
|
||||
{#if eff_badge?.other_2_code}
|
||||
{@const Icon2 = code_to_icon.option_2[eff_badge.other_2_code]}
|
||||
<span class="badge_footer_special_right">
|
||||
{#if Icon2}<Icon2 size="1em" />{/if}
|
||||
{#if Icon2}<Icon2 size="1em" />{:else}<Asterisk size="1em" />{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -656,6 +646,7 @@
|
||||
>
|
||||
<img
|
||||
class="header_image"
|
||||
class:header_full_width={banner_full_width}
|
||||
src={$lq__event_badge_template_obj.secondary_header_path}
|
||||
alt="check secondary header path"
|
||||
/>
|
||||
@@ -664,6 +655,7 @@
|
||||
<div class="badge_back_header image max-w-xl">
|
||||
<img
|
||||
class="header_image"
|
||||
class:header_full_width={banner_full_width}
|
||||
src={$lq__event_badge_template_obj.header_path}
|
||||
alt="check primary header path"
|
||||
/>
|
||||
@@ -768,7 +760,7 @@
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{#if $lq__event_badge_obj.other_1 || $lq__event_badge_obj.other_2 || $lq__event_badge_obj.ticket_1_code || $lq__event_badge_obj.ticket_2_code || $lq__event_badge_obj.ticket_3_code || $lq__event_badge_obj.ticket_4_code || $lq__event_badge_obj.ticket_5_code}
|
||||
{#if $lq__event_badge_obj?.other_1_code || $lq__event_badge_obj?.other_2_code || $lq__event_badge_obj?.ticket_1_code || $lq__event_badge_obj?.ticket_2_code || $lq__event_badge_obj?.ticket_3_code || $lq__event_badge_obj?.ticket_4_code || $lq__event_badge_obj?.ticket_5_code}
|
||||
<div class="attendee_information">
|
||||
<ul>
|
||||
{#if $lq__event_badge_obj.ticket_1_code}
|
||||
@@ -799,26 +791,18 @@
|
||||
</ul>
|
||||
|
||||
{#if $ae_loc.administrator_access}
|
||||
{#if $lq__event_badge_obj.other_1}
|
||||
{#if $lq__event_badge_obj?.other_1_code}
|
||||
{@const BIcon1 = code_to_icon.option_1[$lq__event_badge_obj.other_1_code]}
|
||||
<div class="other_1">
|
||||
Other 1: {$lq__event_badge_obj.other_1}
|
||||
{#if option_other_1_override && ['back_bool', 'front_back_bool'].includes(option_other_1_display_opt)}
|
||||
- {option_other_1_override}
|
||||
{:else if option_other_1_override && ['back_html', 'front_back_html'].includes(option_other_1_display_opt)}
|
||||
{@const BIcon1 = code_to_icon.option_1[option_other_1_override]}
|
||||
- {option_other_1_override}{#if BIcon1} <BIcon1 size="0.85em" class="inline" />{/if}
|
||||
{/if}
|
||||
Other 1: {$lq__event_badge_obj.other_1_code}
|
||||
{#if BIcon1} <BIcon1 size="0.85em" class="inline" />{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $lq__event_badge_obj.other_2}
|
||||
{#if $lq__event_badge_obj?.other_2_code}
|
||||
{@const BIcon2 = code_to_icon.option_2[$lq__event_badge_obj.other_2_code]}
|
||||
<div class="other_2">
|
||||
Other 2: {$lq__event_badge_obj.other_2}
|
||||
{#if option_other_2_override && ['back_bool', 'front_back_bool'].includes(option_other_2_display_opt)}
|
||||
- {option_other_2_override}
|
||||
{:else if option_other_2_override && ['back_html', 'front_back_html'].includes(option_other_2_display_opt)}
|
||||
{@const BIcon2 = code_to_icon.option_2[option_other_2_override]}
|
||||
- {option_other_2_override}{#if BIcon2} <BIcon2 size="0.85em" class="inline" />{/if}
|
||||
{/if}
|
||||
Other 2: {$lq__event_badge_obj.other_2_code}
|
||||
{#if BIcon2} <BIcon2 size="0.85em" class="inline" />{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -919,91 +903,8 @@
|
||||
{/if}
|
||||
<!-- *** fold under section end *** -->
|
||||
|
||||
<!-- *** receipt section start *** -->
|
||||
{#if show_receipt}
|
||||
<section class="receipt hidden">
|
||||
<div class="receipt_header">
|
||||
<img
|
||||
class="badge_logo max-w-sm"
|
||||
src={$lq__event_badge_template_obj.logo_path}
|
||||
alt="check badge logo"
|
||||
/>
|
||||
<div class="banner_text">
|
||||
<div class="row_one">
|
||||
{$lq__event_badge_template_obj.header_row_1}
|
||||
</div>
|
||||
<div class="row_two">
|
||||
{$lq__event_badge_template_obj.header_row_2}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="receipt_content">
|
||||
<div>
|
||||
Account Number: <span class="BT_ID">BT_ID</span><br />
|
||||
Order Number:
|
||||
<span class="Order_Number">Order_Number</span><br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="receipt_footer">
|
||||
<p class="printed">Receipt Printed</p>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<!-- *** receipt section end *** -->
|
||||
|
||||
<!-- *** ticket section start *** -->
|
||||
{#if show_tickets}
|
||||
<section class="tickets_left_container hidden">
|
||||
<div class="tickets">
|
||||
<div class="ticket_container ticket_1">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_1_code}{@html $lq__event_badge_template_obj.ticket_1_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_2">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_2_code}{@html $lq__event_badge_template_obj.ticket_2_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_3">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_3_code}{@html $lq__event_badge_template_obj.ticket_3_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_4">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_4_code}{@html $lq__event_badge_template_obj.ticket_4_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tickets_right_container hidden">
|
||||
<div class="tickets">
|
||||
<div class="ticket_container ticket_5">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_5_code}{@html $lq__event_badge_template_obj.ticket_5_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_6">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_6_code}{@html $lq__event_badge_template_obj.ticket_6_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_7">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_7_code}{@html $lq__event_badge_template_obj.ticket_7_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket_container ticket_8">
|
||||
<div class="ticket_body">
|
||||
{#if $lq__event_badge_obj.ticket_8_code}{@html $lq__event_badge_template_obj.ticket_8_text}{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<!-- *** ticket section end *** -->
|
||||
<!-- Receipt and standalone ticket tear-off sections reserved for future fanfold layouts.
|
||||
Not rendered until show_receipt / show_tickets is wired to template config. -->
|
||||
{/if}
|
||||
<!-- End if for lq__event_badge_obj && lq__event_badge_template_obj -->
|
||||
</section>
|
||||
@@ -1081,4 +982,28 @@
|
||||
background-color: white;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/*
|
||||
* Header image: center horizontally within the badge.
|
||||
* <img> defaults to display:inline, which left-aligns any image narrower than
|
||||
* its container. display:block + margin:auto centers it regardless of image width.
|
||||
* max-width:100% prevents overflow; height:auto preserves aspect ratio.
|
||||
* Applied to all three header_image uses: front primary, back secondary, back fallback.
|
||||
*/
|
||||
.header_image {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* banner_full_width mode (default on): stretches the header image to the full badge
|
||||
* width so it fills the banner slot edge-to-edge.
|
||||
* Toggle off in the controls panel staff section to see the image at its natural
|
||||
* pixel size — useful for calibration and checking source image dimensions.
|
||||
*/
|
||||
.header_image.header_full_width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import { browser } from '$app/environment';
|
||||
import { Check, ChevronDown, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte';
|
||||
import { Check, ChevronDown, Eye, EyeOff, Info, LoaderCircle, Pencil, Printer, RotateCcw, X } from '@lucide/svelte';
|
||||
interface Props {
|
||||
event_id: string;
|
||||
event_badge_id: string;
|
||||
@@ -42,6 +42,24 @@
|
||||
* as the user types — no save required.
|
||||
*/
|
||||
preview_overrides?: Record<string, any> | null;
|
||||
/**
|
||||
* Per-browser print position calibration (mm). Stored in localStorage by the
|
||||
* parent page so each workstation/printer can be tuned independently.
|
||||
* Positive X = shift right; positive Y = shift down.
|
||||
*/
|
||||
print_offset_x?: number;
|
||||
print_offset_y?: number;
|
||||
/**
|
||||
* Hides the page header and sys bar for a clean badge-preview workspace.
|
||||
* Toggle with the [H] keyboard shortcut or this button. Stored in localStorage.
|
||||
*/
|
||||
hide_chrome?: boolean;
|
||||
/**
|
||||
* When true (default), header images stretch to 100% badge width.
|
||||
* Toggle off to see the image at its natural pixel size for calibration.
|
||||
* Stored in localStorage.
|
||||
*/
|
||||
banner_full_width?: boolean;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
@@ -55,6 +73,10 @@
|
||||
font_size_affiliations = $bindable(null),
|
||||
font_size_location = $bindable(null),
|
||||
preview_overrides = $bindable(null),
|
||||
print_offset_x = $bindable(0),
|
||||
print_offset_y = $bindable(0),
|
||||
hide_chrome = $bindable(false),
|
||||
banner_full_width = $bindable(true),
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
|
||||
@@ -125,6 +147,10 @@
|
||||
try { return JSON.parse(raw); } catch { return []; }
|
||||
});
|
||||
|
||||
// --- Print position offset configuration ---
|
||||
const OFFSET_STEP = 0.5; // mm per click
|
||||
const OFFSET_MAX = 15; // mm in either direction
|
||||
|
||||
// --- Font size configuration ---
|
||||
const FONT_SIZE_STEP = 2;
|
||||
const FONT_SIZE_DEFAULTS = { name: 58, title: 34, affiliations: 38, location: 34 } as const;
|
||||
@@ -879,6 +905,42 @@
|
||||
<span class="flex-1 border-t border-gray-200 dark:border-gray-700"></span>
|
||||
</div>
|
||||
|
||||
<!-- Chrome visibility toggle: hides page header + sys bar for a clean workspace.
|
||||
Keyboard shortcut [H] does the same thing from anywhere on the page. -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm w-full justify-between"
|
||||
class:preset-tonal-warning={hide_chrome}
|
||||
class:preset-tonal-surface={!hide_chrome}
|
||||
onclick={() => hide_chrome = !hide_chrome}
|
||||
title={hide_chrome ? 'Show site nav, footer, and menu bar' : 'Hide site nav, footer, and menu bar for a clean workspace'}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
{#if hide_chrome}
|
||||
<Eye size="13" class="shrink-0" />
|
||||
Show nav/footer/menu
|
||||
{:else}
|
||||
<EyeOff size="13" class="shrink-0" />
|
||||
Hide nav/footer/menu
|
||||
{/if}
|
||||
</span>
|
||||
<span class="text-[10px] opacity-40">[H]</span>
|
||||
</button>
|
||||
</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">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={banner_full_width}
|
||||
/>
|
||||
<span class="text-xs">Banner full width</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Debug Outlines Toggle (for print testing) -->
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
@@ -891,6 +953,78 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Print Position Offset
|
||||
Per-browser calibration for physical printer alignment.
|
||||
Stored in localStorage by the parent page — each workstation keeps
|
||||
its own values across sessions without any server config change.
|
||||
Positive X = shift right; positive Y = shift down. 0.5mm steps. -->
|
||||
<div class="px-2 py-1.5">
|
||||
<p class="field-label mb-1.5">
|
||||
Print position
|
||||
<span class="text-[9px] font-normal normal-case tracking-normal text-gray-400 ml-1">saved to browser</span>
|
||||
</p>
|
||||
<div class="space-y-1">
|
||||
<!-- X axis: left ← / right → -->
|
||||
<div class="flex items-center gap-1" role="group" aria-label="Horizontal print offset">
|
||||
<span class="text-[10px] text-gray-400 w-8 shrink-0">L/R</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
|
||||
onclick={() => print_offset_x = Math.max(-OFFSET_MAX, print_offset_x - OFFSET_STEP)}
|
||||
disabled={print_offset_x <= -OFFSET_MAX}
|
||||
title="Shift badge left"
|
||||
aria-label="Shift left"
|
||||
>−</button>
|
||||
<span class="font-mono text-[11px] w-14 text-center tabular-nums"
|
||||
class:text-gray-400={print_offset_x === 0}
|
||||
class:text-amber-600={print_offset_x !== 0}
|
||||
aria-live="polite"
|
||||
>{print_offset_x > 0 ? '+' : ''}{print_offset_x.toFixed(1)} mm</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
|
||||
onclick={() => print_offset_x = Math.min(OFFSET_MAX, print_offset_x + OFFSET_STEP)}
|
||||
disabled={print_offset_x >= OFFSET_MAX}
|
||||
title="Shift badge right"
|
||||
aria-label="Shift right"
|
||||
>+</button>
|
||||
</div>
|
||||
<!-- Y axis: up ↑ / down ↓ -->
|
||||
<div class="flex items-center gap-1" role="group" aria-label="Vertical print offset">
|
||||
<span class="text-[10px] text-gray-400 w-8 shrink-0">U/D</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
|
||||
onclick={() => print_offset_y = Math.max(-OFFSET_MAX, print_offset_y - OFFSET_STEP)}
|
||||
disabled={print_offset_y <= -OFFSET_MAX}
|
||||
title="Shift badge up"
|
||||
aria-label="Shift up"
|
||||
>−</button>
|
||||
<span class="font-mono text-[11px] w-14 text-center tabular-nums"
|
||||
class:text-gray-400={print_offset_y === 0}
|
||||
class:text-amber-600={print_offset_y !== 0}
|
||||
aria-live="polite"
|
||||
>{print_offset_y > 0 ? '+' : ''}{print_offset_y.toFixed(1)} mm</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-surface px-2 min-h-0 h-6 text-base leading-none"
|
||||
onclick={() => print_offset_y = Math.min(OFFSET_MAX, print_offset_y + OFFSET_STEP)}
|
||||
disabled={print_offset_y >= OFFSET_MAX}
|
||||
title="Shift badge down"
|
||||
aria-label="Shift down"
|
||||
>+</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if print_offset_x !== 0 || print_offset_y !== 0}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-warning mt-1.5 w-full"
|
||||
onclick={() => { print_offset_x = 0; print_offset_y = 0; }}
|
||||
title="Reset print position to center"
|
||||
>↺ Reset position</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- === BADGE TYPE === (only when template defines badge_type_list) -->
|
||||
{#if badge_type_code_li.length > 0}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'badge_type'}>
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
|
||||
import { untrack } from 'svelte';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import { ae_loc, ae_sess } from '$lib/stores/ae_stores';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import { events_loc } from '$lib/stores/ae_events_stores';
|
||||
import { page } from '$app/state';
|
||||
import { ArrowLeft, Eye, LoaderCircle, Mail, Printer } from '@lucide/svelte';
|
||||
import Comp_badge_obj_view_v2 from '../ae_comp__badge_obj_view_v2.svelte';
|
||||
import Comp_badge_obj_view from '../ae_comp__badge_obj_view.svelte';
|
||||
import Comp_badge_print_controls from '../ae_comp__badge_print_controls.svelte';
|
||||
|
||||
let event_badge_id = $derived(page.params.badge_id);
|
||||
@@ -102,6 +103,64 @@
|
||||
let font_size_affiliations: number | null = $state(null);
|
||||
let font_size_location: number | null = $state(null);
|
||||
|
||||
// Per-browser print workstation tweaks.
|
||||
// Each workstation/printer is calibrated independently — values are stored in
|
||||
// localStorage so they survive page reloads and browser restarts without any
|
||||
// server config change.
|
||||
// WHY localStorage not a store: this is intentionally per-device, not per-user or
|
||||
// per-event. Different printers at the same venue may need different offsets.
|
||||
const _PRINT_TWEAKS_KEY = 'ae_badge_print_tweaks';
|
||||
const _saved_tweaks = (() => {
|
||||
if (!browser) return null;
|
||||
try { return JSON.parse(localStorage.getItem(_PRINT_TWEAKS_KEY) ?? 'null'); } catch { return null; }
|
||||
})();
|
||||
|
||||
// Print position offset (mm). Positive X = shift right; positive Y = shift down.
|
||||
let print_offset_x: number = $state(_saved_tweaks?.x ?? 0);
|
||||
let print_offset_y: number = $state(_saved_tweaks?.y ?? 0);
|
||||
|
||||
// Banner width: true = 100% (production), false = natural pixel size (dev/calibration).
|
||||
let banner_full_width: boolean = $state(_saved_tweaks?.banner_full_width ?? 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);
|
||||
|
||||
// Sync chrome visibility with hide_chrome. Restores all original states on page leave.
|
||||
// Hides three things:
|
||||
// 1. $ae_loc.sys_menu.hide — the fixed bottom-right AE sys bar
|
||||
// 2. $ae_sess.disable_sys_nav — the Events layout <nav class="submenu"> (site header)
|
||||
// 3. $ae_sess.disable_sys_footer — the Events layout <footer> (site footer)
|
||||
const _restore_sys_bar = $ae_loc.sys_menu.hide;
|
||||
const _restore_sys_nav = $ae_sess.disable_sys_nav;
|
||||
const _restore_sys_footer = $ae_sess.disable_sys_footer;
|
||||
$effect(() => {
|
||||
$ae_loc.sys_menu.hide = hide_chrome;
|
||||
$ae_sess.disable_sys_nav = hide_chrome;
|
||||
$ae_sess.disable_sys_footer = hide_chrome;
|
||||
return () => {
|
||||
$ae_loc.sys_menu.hide = _restore_sys_bar;
|
||||
$ae_sess.disable_sys_nav = _restore_sys_nav;
|
||||
$ae_sess.disable_sys_footer = _restore_sys_footer;
|
||||
};
|
||||
});
|
||||
|
||||
// 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 }));
|
||||
});
|
||||
|
||||
// Inject print offset as @media print CSS. Uses translate() so the centering
|
||||
// logic in ae-print-badge.css stays intact — this just shifts the final position.
|
||||
// The effect re-runs whenever either offset changes, removing the old <style> tag
|
||||
// and inserting a fresh one (always last in <head>, so it wins the cascade).
|
||||
$effect(() => {
|
||||
const el = document.createElement('style');
|
||||
el.textContent = `@media print { .event_badge_wrapper { transform: translate(calc(-50% + ${print_offset_x}mm), calc(-50% + ${print_offset_y}mm)) !important; } }`;
|
||||
document.head.appendChild(el);
|
||||
return () => el.remove();
|
||||
});
|
||||
|
||||
// Real-time preview: the controls panel sets this to the current edit field
|
||||
// value on every keystroke. The badge render merges it over the saved badge
|
||||
// object so the user can see changes as they type, before saving.
|
||||
@@ -132,14 +191,32 @@
|
||||
$lq__event_badge_template_obj?.layout === 'badge_4x5_fanfold' ? '4in 10in' :
|
||||
'4in 12in' // Default: badge_4x6_fanfold or layout not yet set
|
||||
);
|
||||
// @page size + optional per-template margin override from cfg_json.print_margin.
|
||||
// print_margin_cfg is parsed from the template's cfg_json field — allows per-event
|
||||
// margin tuning without a code deploy (edit cfg_json in the DB, refresh page).
|
||||
// For fine-tuning per-printer/workstation, use the print_offset_x/y controls instead.
|
||||
$effect(() => {
|
||||
const m = print_margin_cfg;
|
||||
const margin_val = m
|
||||
? `${m.top ?? '0'} ${m.right ?? '0'} ${m.bottom ?? '0'} ${m.left ?? '0'}`
|
||||
: '0';
|
||||
const el = document.createElement('style');
|
||||
el.textContent = `@page { size: ${page_size_css}; margin: 0; }`;
|
||||
el.textContent = `@page { size: ${page_size_css}; margin: ${margin_val}; }`;
|
||||
document.head.appendChild(el);
|
||||
return () => el.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(e) => {
|
||||
// [H] key: toggle header/sys-bar chrome for a clean print workspace.
|
||||
// Guard: don't fire when typing in form fields.
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
||||
if (e.key === 'h' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
hide_chrome = !hide_chrome;
|
||||
}
|
||||
}} />
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
Æ: Print Badge —
|
||||
@@ -188,8 +265,10 @@
|
||||
|
||||
{#if $lq__event_badge_obj && $lq__event_badge_obj.event_id && event_badge_id}
|
||||
|
||||
<!-- Kiosk header: screen-only chrome. Minimal — the badge render is the focus. -->
|
||||
<header class="print:hidden w-full flex flex-row flex-wrap gap-2 items-center justify-between border-b border-gray-300 mb-3 pb-2">
|
||||
<!-- Kiosk header: screen-only chrome. Minimal — the badge render is the focus.
|
||||
Hidden when hide_chrome is on (toggle with [H] or the controls panel button). -->
|
||||
<header class="print:hidden w-full flex flex-row flex-wrap gap-2 items-center justify-between border-b border-gray-300 mb-3 pb-2"
|
||||
class:hidden={hide_chrome}>
|
||||
|
||||
<!-- Left: Back + attendee name + status indicator -->
|
||||
<div class="flex flex-row gap-2 items-center min-w-0">
|
||||
@@ -283,7 +362,7 @@
|
||||
<div id="badge_render_area" class="print:pr-0 transition-all duration-200" class:pr-80={is_editing} class:pr-64={!is_editing}>
|
||||
<!-- null → Element_fit_text auto-scales; a number → manual size override from controls. -->
|
||||
<!-- preview_overrides: non-null while a field card is open in controls — gives live preview. -->
|
||||
<Comp_badge_obj_view_v2
|
||||
<Comp_badge_obj_view
|
||||
event_id={$lq__event_badge_obj.event_id as string}
|
||||
event_badge_id={event_badge_id as string}
|
||||
{lq__event_badge_obj}
|
||||
@@ -294,6 +373,7 @@
|
||||
font_size_affiliations={font_size_affiliations}
|
||||
font_size_location={font_size_location}
|
||||
{preview_overrides}
|
||||
{banner_full_width}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -301,8 +381,9 @@
|
||||
Normally w-64 (256px); expands to w-80 (320px) while a field is being edited
|
||||
so the scaled value text and form inputs have room without clipping.
|
||||
Fixed right-0 means width growth expands leftward into the badge area.
|
||||
top-20 clears the page header (~80px). Independently scrollable. -->
|
||||
<div class="print:hidden fixed right-0 top-20 bottom-0 overflow-y-auto z-30
|
||||
top-20 clears the page header (~80px). Independently scrollable.
|
||||
bottom-24: clears the sys bar (fixed bottom-12 h-9 = 84px from bottom). -->
|
||||
<div class="print:hidden fixed right-0 top-20 bottom-24 overflow-y-auto z-30
|
||||
border-l border-gray-200 dark:border-gray-700
|
||||
bg-white dark:bg-zinc-900 p-2
|
||||
transition-all duration-200"
|
||||
@@ -318,6 +399,10 @@
|
||||
bind:font_size_affiliations
|
||||
bind:font_size_location
|
||||
bind:preview_overrides
|
||||
bind:print_offset_x
|
||||
bind:print_offset_y
|
||||
bind:hide_chrome
|
||||
bind:banner_full_width
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import { ae_api } from '$lib/stores/ae_stores';
|
||||
import { events_slct } from '$lib/stores/ae_events_stores';
|
||||
import Comp_badge_obj_view from '../[badge_id]/ae_comp__badge_obj_view_v2.svelte';
|
||||
import Comp_badge_obj_view from '../[badge_id]/ae_comp__badge_obj_view.svelte';
|
||||
import type { ae_EventBadge } from '$lib/types/ae_types';
|
||||
import { ArrowLeft, Printer } from '@lucide/svelte';
|
||||
interface Props {
|
||||
|
||||
@@ -80,7 +80,10 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
TEMPORARY DEBUG OUTLINES — remove before going live
|
||||
Debug outlines — toggled via "Show debug outlines" checkbox
|
||||
in the Staff section of the print controls panel. Gated on
|
||||
the html.debug_outlines class set by ae_comp__badge_print_controls.
|
||||
These are intentionally permanent — not temporary.
|
||||
============================================================ */
|
||||
html.debug_outlines {
|
||||
outline: 3px dashed lime !important;
|
||||
|
||||
BIN
static/badge_header_calibration.png
Normal file
BIN
static/badge_header_calibration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
6
static/badge_header_calibration.svg
Normal file
6
static/badge_header_calibration.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="332" height="72" viewBox="0 0 332 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="332" height="72" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1" />
|
||||
<rect x="53" y="24" width="48" height="12" rx="4" fill="#ef4444" fill-opacity="0.4" stroke="#b91c1c" />
|
||||
<rect x="231" y="24" width="48" height="12" rx="4" fill="#ef4444" fill-opacity="0.4" stroke="#b91c1c" />
|
||||
<text x="166" y="42" font-family="monospace" font-size="10" fill="#ef4444" text-anchor="middle">CLIP SLOT DANGER ZONE</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
BIN
static/badge_header_demo.png
Normal file
BIN
static/badge_header_demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
static/badge_header_hex_blue.png
Normal file
BIN
static/badge_header_hex_blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
20
static/badge_header_hex_blue.svg
Normal file
20
static/badge_header_hex_blue.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="332" height="72" viewBox="0 0 332 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1e3a8a;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<pattern id="hexagons" width="20" height="17.32" patternUnits="userSpaceOnUse" patternTransform="scale(0.5)">
|
||||
<path d="M10 0 L20 5.77 L20 17.32 L10 23.09 L0 17.32 L0 5.77 Z" fill="none" stroke="white" stroke-width="1" stroke-opacity="0.1" />
|
||||
</pattern>
|
||||
<filter id="softShadow" x="0" y="0" width="200%" height="200%">
|
||||
<feOffset result="offOut" in="SourceAlpha" dx="1" dy="1" />
|
||||
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" />
|
||||
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="332" height="72" fill="url(#grad)" />
|
||||
<rect width="332" height="72" fill="url(#hexagons)" />
|
||||
<text x="166" y="32" font-family="sans-serif" font-weight="bold" font-size="18" fill="white" text-anchor="middle" filter="url(#softShadow)">ONE SKY IT</text>
|
||||
<text x="166" y="50" font-family="sans-serif" font-size="11" font-style="italic" fill="#93c5fd" text-anchor="middle">AETHER PLATFORM</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user