Expand branding controls and logo upload

This commit is contained in:
Codex Agent
2026-01-15 08:42:20 +01:00
parent a1f37bb491
commit b0ba29fcb6
11 changed files with 906 additions and 139 deletions

View File

@@ -38,6 +38,18 @@ const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: st
camera: Camera,
};
type LogoSize = 's' | 'm' | 'l';
const LOGO_SIZE_CLASSES: Record<LogoSize, { container: string; image: string; emoji: string; icon: string }> = {
s: { container: 'h-8 w-8', image: 'h-7 w-7', emoji: 'text-lg', icon: 'h-4 w-4' },
m: { container: 'h-10 w-10', image: 'h-9 w-9', emoji: 'text-xl', icon: 'h-5 w-5' },
l: { container: 'h-12 w-12', image: 'h-11 w-11', emoji: 'text-2xl', icon: 'h-6 w-6' },
};
function getLogoClasses(size?: LogoSize) {
return LOGO_SIZE_CLASSES[size ?? 'm'];
}
const NOTIFICATION_ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
broadcast: MessageSquare,
feedback_request: MessageSquare,
@@ -69,18 +81,25 @@ function getInitials(name: string): string {
return name.substring(0, 2).toUpperCase();
}
function renderEventAvatar(name: string, icon: unknown, accentColor: string, textColor: string, logo?: { mode: 'emoticon' | 'upload'; value: string | null }) {
function renderEventAvatar(
name: string,
icon: unknown,
accentColor: string,
textColor: string,
logo?: { mode: 'emoticon' | 'upload'; value: string | null; size?: LogoSize }
) {
const sizes = getLogoClasses(logo?.size);
if (logo?.mode === 'upload' && logo.value) {
return (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-sm">
<img src={logo.value} alt={name} className="h-9 w-9 rounded-full object-contain" />
<div className={`flex items-center justify-center rounded-full bg-white shadow-sm ${sizes.container}`}>
<img src={logo.value} alt={name} className={`rounded-full object-contain ${sizes.image}`} />
</div>
);
}
if (logo?.mode === 'emoticon' && logo.value && isLikelyEmoji(logo.value)) {
return (
<div
className="flex h-10 w-10 items-center justify-center rounded-full text-xl shadow-sm"
className={`flex items-center justify-center rounded-full shadow-sm ${sizes.container} ${sizes.emoji}`}
style={{ backgroundColor: accentColor, color: textColor }}
>
<span aria-hidden>{logo.value}</span>
@@ -94,21 +113,21 @@ function renderEventAvatar(name: string, icon: unknown, accentColor: string, tex
if (trimmed) {
const normalized = trimmed.toLowerCase();
const IconComponent = EVENT_ICON_COMPONENTS[normalized];
if (IconComponent) {
return (
<div
className="flex h-10 w-10 items-center justify-center rounded-full shadow-sm"
style={{ backgroundColor: accentColor, color: textColor }}
>
<IconComponent className="h-5 w-5" aria-hidden />
</div>
);
}
if (IconComponent) {
return (
<div
className={`flex items-center justify-center rounded-full shadow-sm ${sizes.container}`}
style={{ backgroundColor: accentColor, color: textColor }}
>
<IconComponent className={sizes.icon} aria-hidden />
</div>
);
}
if (isLikelyEmoji(trimmed)) {
return (
<div
className="flex h-10 w-10 items-center justify-center rounded-full text-xl shadow-sm"
className={`flex items-center justify-center rounded-full shadow-sm ${sizes.container} ${sizes.emoji}`}
style={{ backgroundColor: accentColor, color: textColor }}
>
<span aria-hidden>{trimmed}</span>
@@ -188,6 +207,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
const headerFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
const logoPosition = branding.logo?.position ?? 'left';
const headerStyle: React.CSSProperties = {
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
color: headerTextColor,
@@ -219,9 +239,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
className="guest-header z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
style={headerStyle}
>
<div className="flex items-center gap-3">
<div
className={
logoPosition === 'center'
? 'flex flex-col items-center gap-2 text-center'
: logoPosition === 'right'
? 'flex flex-row-reverse items-center gap-3'
: 'flex items-center gap-3'
}
>
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
<div className="flex flex-col" style={headerFont ? { fontFamily: headerFont } : undefined}>
<div
className={`flex flex-col${logoPosition === 'center' ? ' items-center text-center' : ''}`}
style={headerFont ? { fontFamily: headerFont } : undefined}
>
<div className="font-semibold text-lg">{event.name}</div>
<div className="flex items-center gap-2 text-xs text-white/70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
{stats && tasksEnabled && (

View File

@@ -38,6 +38,11 @@ export const DEFAULT_EVENT_BRANDING: EventBranding = {
const DEFAULT_PRIMARY = DEFAULT_EVENT_BRANDING.primaryColor.toLowerCase();
const DEFAULT_SECONDARY = DEFAULT_EVENT_BRANDING.secondaryColor.toLowerCase();
const DEFAULT_BACKGROUND = DEFAULT_EVENT_BRANDING.backgroundColor.toLowerCase();
const FONT_SCALE_MAP: Record<'s' | 'm' | 'l', number> = {
s: 0.94,
m: 1,
l: 1.08,
};
const EventBrandingContext = createContext<EventBrandingContextValue | undefined>(undefined);
@@ -62,7 +67,8 @@ function resolveBranding(input?: EventBranding | null): EventBranding {
const headingFont = input.typography?.heading ?? input.fontFamily ?? null;
const bodyFont = input.typography?.body ?? input.fontFamily ?? null;
const sizePreset = input.typography?.sizePreset ?? 'm';
const rawSize = input.typography?.sizePreset ?? 'm';
const sizePreset = rawSize === 's' || rawSize === 'm' || rawSize === 'l' ? rawSize : 'm';
const logoMode = input.logo?.mode ?? (input.logoUrl ? 'upload' : 'emoticon');
const logoValue = input.logo?.value ?? input.logoUrl ?? null;
@@ -116,6 +122,7 @@ function applyCssVariables(branding: EventBranding) {
root.style.setProperty('--guest-radius', `${branding.buttons?.radius ?? 12}px`);
root.style.setProperty('--guest-link', branding.buttons?.linkColor ?? branding.secondaryColor);
root.style.setProperty('--guest-button-style', branding.buttons?.style ?? 'filled');
root.style.setProperty('--guest-font-scale', String(FONT_SCALE_MAP[branding.typography?.sizePreset ?? 'm'] ?? 1));
const headingFont = branding.typography?.heading ?? branding.fontFamily;
const bodyFont = branding.typography?.body ?? branding.fontFamily;
@@ -149,6 +156,7 @@ function resetCssVariables() {
root.style.removeProperty('--guest-radius');
root.style.removeProperty('--guest-link');
root.style.removeProperty('--guest-button-style');
root.style.removeProperty('--guest-font-scale');
root.style.removeProperty('--guest-font-family');
root.style.removeProperty('--guest-body-font');
root.style.removeProperty('--guest-heading-font');

View File

@@ -9,6 +9,11 @@ const sampleBranding: EventBranding = {
backgroundColor: '#fef2f2',
fontFamily: 'Montserrat, sans-serif',
logoUrl: null,
typography: {
heading: null,
body: null,
sizePreset: 'l',
},
mode: 'dark',
};
@@ -17,6 +22,7 @@ describe('EventBrandingProvider', () => {
document.documentElement.classList.remove('guest-theme', 'dark');
document.documentElement.style.removeProperty('color-scheme');
document.documentElement.style.removeProperty('--guest-background');
document.documentElement.style.removeProperty('--guest-font-scale');
});
it('applies guest theme classes and variables', async () => {
@@ -31,6 +37,7 @@ describe('EventBrandingProvider', () => {
expect(document.documentElement.classList.contains('dark')).toBe(true);
expect(document.documentElement.style.colorScheme).toBe('dark');
expect(document.documentElement.style.getPropertyValue('--guest-background')).toBe(sampleBranding.backgroundColor);
expect(document.documentElement.style.getPropertyValue('--guest-font-scale')).toBe('1.08');
});
unmount();