fix(branding): unify hero logo SIZE the same way as visibility (#756)

Follow-on to the visibility fix in this PR — hero_logo_size had the same
split-brain: GalleryLayout read the global branding_logo_size live while
the hero-header path used the per-event snapshot, so a hero logo could
render at different sizes on different layouts and the global size didn't
reach hero-header galleries.

Now mirrored on the visibility model: NULL per-event hero_logo_size =
inherit branding_logo_size; explicit = override.

- Migration 153: hero_logo_size nullable + backfill NULL so existing
  galleries inherit the global size (restores GalleryLayout's prior
  live-global behaviour and fixes the hero-header staleness).
- Creation stores NULL unless explicit; gallery.js resolves
  per-event ?? global and sends the effective size.
- GalleryLayout now consumes that resolved size for the hero logo (new
  heroLogoSize prop) instead of the global — both render paths match.
- Admin size control gains a 'Use branding default' (inherit) option.

Verified: migration on SQLite + PG; live resolution (inherit follows
global both ways, override wins); creation stores NULL on PG; tsc clean,
106 adminEvents+gallery tests pass, build green.
This commit is contained in:
Paul Nothaft
2026-07-06 11:44:33 +02:00
parent 96fe478bf8
commit 60b03b1728
9 changed files with 85 additions and 16 deletions
@@ -309,7 +309,8 @@ export const EventDetailsPage: React.FC = () => {
// Load hero logo settings from event. Preserve null = "inherit global"
// (#756) — don't collapse it to true, or saving would snapshot an override.
hero_logo_visible: event.hero_logo_visible ?? null,
hero_logo_size: event.hero_logo_size || 'medium',
// Preserve null = "inherit global size" (#756) — don't collapse to medium.
hero_logo_size: event.hero_logo_size ?? null,
hero_logo_position: event.hero_logo_position || 'top',
// Hero image anchor position (#162)
hero_image_anchor: event.hero_image_anchor || 'center',
@@ -620,10 +620,12 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
{t('events.heroLogoSize', 'Logo Size')}
</label>
<select
value={editForm.hero_logo_size}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
// '' = inherit the global branding logo size (#756).
value={editForm.hero_logo_size ?? ''}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value === '' ? null : e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md shadow-sm focus:ring-primary-500 focus:border-accent-dark text-sm"
>
<option value="">{t('events.heroLogoInherit', 'Use branding default')}</option>
<option value="small">{t('events.heroLogoSizeSmall', 'Small')}</option>
<option value="medium">{t('events.heroLogoSizeMedium', 'Medium')}</option>
<option value="large">{t('events.heroLogoSizeLarge', 'Large')}</option>
@@ -26,7 +26,7 @@ export type EditFormState = {
use_canvas_rendering: boolean;
// Hero logo settings. null = inherit the global branding toggle (#756).
hero_logo_visible: boolean | null;
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge';
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge' | null;
hero_logo_position: 'top' | 'center' | 'bottom';
// Hero image anchor position (#162) keyword or "X% Y%" focal point
hero_image_anchor: string;
@@ -74,7 +74,7 @@ export const INITIAL_EDIT_FORM: EditFormState = {
use_canvas_rendering: false,
// Hero logo settings — null = inherit global branding toggle (#756)
hero_logo_visible: null,
hero_logo_size: 'medium',
hero_logo_size: null,
hero_logo_position: 'top',
// Hero image anchor position (#162)
hero_image_anchor: 'center',