From b5eafc52bd1518416867426a1ab07dac7a216757 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Fri, 3 Jul 2026 08:10:25 +0200 Subject: [PATCH] refactor(frontend): decompose EventDetailsPage and ThemeCustomizerEnhanced Move-code split, entry paths/exports unchanged: - EventDetailsPage.tsx (2,697 -> 679) + pages/admin/event-details/* (16 files) - ThemeCustomizerEnhanced.tsx (1,541 -> 349) + admin/theme-customizer/* (11) Known ephemeral-UI delta: widget-local state (copied-link flags, unsaved PIN input, modal selection) now resets when a tab unmounts. --- .../admin/ThemeCustomizerEnhanced.tsx | 1398 +---------- .../ColorCustomizationCard.tsx | 305 +++ .../admin/theme-customizer/ColorPickerRow.tsx | 46 + .../theme-customizer/ControlsStyleCard.tsx | 85 + .../theme-customizer/CssTemplateCard.tsx | 76 + .../admin/theme-customizer/CustomCssCard.tsx | 125 + .../theme-customizer/GalleryLayoutCard.tsx | 373 +++ .../theme-customizer/HeaderStyleCard.tsx | 95 + .../theme-customizer/ThemePresetsCard.tsx | 119 + .../theme-customizer/TypographyStyleCard.tsx | 154 ++ .../admin/theme-customizer/fontUtils.ts | 34 + .../admin/theme-customizer/icons.tsx | 50 + frontend/src/pages/admin/EventDetailsPage.tsx | 2196 +---------------- .../admin/event-details/ArchiveStatusCard.tsx | 54 + .../admin/event-details/CategoriesTab.tsx | 35 + .../admin/event-details/ClientAccessCard.tsx | 149 ++ .../admin/event-details/EventActionsCard.tsx | 82 + .../event-details/EventDetailsHeader.tsx | 271 ++ .../event-details/EventInformationCard.tsx | 900 +++++++ .../pages/admin/event-details/EventTabs.tsx | 77 + .../admin/event-details/EventThemeSection.tsx | 118 + .../event-details/ExternalFolderPicker.tsx | 60 + .../admin/event-details/FolderTreeNode.tsx | 112 + .../pages/admin/event-details/OverviewTab.tsx | 199 ++ .../event-details/PhotoStatisticsCard.tsx | 78 + .../pages/admin/event-details/PhotosTab.tsx | 210 ++ .../admin/event-details/ShareLinkCard.tsx | 117 + .../src/pages/admin/event-details/types.ts | 94 + .../src/pages/admin/event-details/utils.ts | 19 + 29 files changed, 4229 insertions(+), 3402 deletions(-) create mode 100644 frontend/src/components/admin/theme-customizer/ColorCustomizationCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/ColorPickerRow.tsx create mode 100644 frontend/src/components/admin/theme-customizer/ControlsStyleCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/CssTemplateCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/CustomCssCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/GalleryLayoutCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/HeaderStyleCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/ThemePresetsCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/TypographyStyleCard.tsx create mode 100644 frontend/src/components/admin/theme-customizer/fontUtils.ts create mode 100644 frontend/src/components/admin/theme-customizer/icons.tsx create mode 100644 frontend/src/pages/admin/event-details/ArchiveStatusCard.tsx create mode 100644 frontend/src/pages/admin/event-details/CategoriesTab.tsx create mode 100644 frontend/src/pages/admin/event-details/ClientAccessCard.tsx create mode 100644 frontend/src/pages/admin/event-details/EventActionsCard.tsx create mode 100644 frontend/src/pages/admin/event-details/EventDetailsHeader.tsx create mode 100644 frontend/src/pages/admin/event-details/EventInformationCard.tsx create mode 100644 frontend/src/pages/admin/event-details/EventTabs.tsx create mode 100644 frontend/src/pages/admin/event-details/EventThemeSection.tsx create mode 100644 frontend/src/pages/admin/event-details/ExternalFolderPicker.tsx create mode 100644 frontend/src/pages/admin/event-details/FolderTreeNode.tsx create mode 100644 frontend/src/pages/admin/event-details/OverviewTab.tsx create mode 100644 frontend/src/pages/admin/event-details/PhotoStatisticsCard.tsx create mode 100644 frontend/src/pages/admin/event-details/PhotosTab.tsx create mode 100644 frontend/src/pages/admin/event-details/ShareLinkCard.tsx create mode 100644 frontend/src/pages/admin/event-details/types.ts create mode 100644 frontend/src/pages/admin/event-details/utils.ts diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index b0901fdd..7ac528d1 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -1,45 +1,20 @@ import React, { useState, useEffect } from 'react'; -import { Palette, RotateCcw, Check, Layout, LayoutTemplate, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info, FileCode, ImageIcon, Minimize2, EyeOff, Menu, SlidersHorizontal, Columns, Film, AlertTriangle } from 'lucide-react'; -import { Button, Card, Input } from '../common'; -import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType, HeaderStyleType, HeroDividerStyle } from '../../types/theme.types'; +import { Palette, RotateCcw } from 'lucide-react'; +import { Button } from '../common'; +import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType } from '../../types/theme.types'; import type { EnabledTemplate } from '../../services/cssTemplates.service'; import { settingsService } from '../../services/settings.service'; -import { fontsService, extractFamilyName, type FontDefinition } from '../../services/fonts.service'; +import { fontsService, type FontDefinition } from '../../services/fonts.service'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; - -/** - * Build the CSS font-family value for a scanned font, using the generic - * fallback the backend supplied (from each family's optional meta.json). - * Defaults to 'sans-serif' when the backend doesn't report one — keeps - * compatibility with backends that predate the generic field. - */ -function buildFontFamilyValue(font: FontDefinition): string { - const generic = font.generic ?? 'sans-serif'; - // Always quote the family name (covers multi-word like 'Playfair Display'). - return `'${font.family}', ${generic}`; -} - -/** - * Match a saved CSS font-family string against the available scanned families - * and return the canonical option value the dropdown renders. Handles both - * legacy unquoted strings ("Inter, sans-serif") and the new quoted format - * ("'Inter', sans-serif"), so events saved before this change still show the - * right option as selected. - */ -function resolveFontDropdownValue( - saved: string | undefined, - available: FontDefinition[] | undefined, - fallback: string -): string { - if (!saved) return fallback; - const family = extractFamilyName(saved); - if (!family) return saved; // generic family like "system-ui, sans-serif" - const match = (available || []).find( - (f) => f.family.toLowerCase() === family.toLowerCase() - ); - return match ? buildFontFamilyValue(match) : saved; -} +import { ThemePresetsCard } from './theme-customizer/ThemePresetsCard'; +import { GalleryLayoutCard } from './theme-customizer/GalleryLayoutCard'; +import { HeaderStyleCard } from './theme-customizer/HeaderStyleCard'; +import { ControlsStyleCard } from './theme-customizer/ControlsStyleCard'; +import { ColorCustomizationCard } from './theme-customizer/ColorCustomizationCard'; +import { TypographyStyleCard } from './theme-customizer/TypographyStyleCard'; +import { CssTemplateCard } from './theme-customizer/CssTemplateCard'; +import { CustomCssCard } from './theme-customizer/CustomCssCard'; interface ThemeCustomizerEnhancedProps { value: ThemeConfig; @@ -74,96 +49,6 @@ interface ThemeCustomizerEnhancedProps { slotBeforeCustomCss?: React.ReactNode; } -/** - * Compact color-picker row used by the 8-token palette. - * Renders [Label + Info icon (tooltip)] / [color swatch + hex input]. - * Help text is hidden in the static layout (lives on the Info icon's title - * attribute) so all rows are the same height — keeps the four Surfaces - * pickers and the two Accent pickers grid-aligned without forcing the user - * to read every help string up front. - */ -const ColorPickerRow: React.FC<{ - label: string; - help: string; - value: string; - fallback: string; - onChange: (value: string) => void; -}> = ({ label, help, value, fallback, onChange }) => ( -
- -
- onChange(e.target.value)} - className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer" - /> - onChange(e.target.value)} - placeholder={fallback} - className="flex-1" - /> -
-
-); - -const layoutIcons: Record = { - grid: , - masonry: , - carousel: , - timeline: , - mosaic: , - 'gallery-premium': , - 'gallery-story': -}; - -const headerStyleIcons: Record = { - hero: , - standard: , - banner: , - minimal: , - none: -}; - -const dividerStylePreviews: Record = { - wave: ( - - - - ), - straight: ( - - - - ), - angle: ( - - - - ), - curve: ( - - - - ), - none: ( - - - No divider - - ) -}; - // Layout descriptions will use translation keys export const ThemeCustomizerEnhanced: React.FC = ({ @@ -196,7 +81,6 @@ export const ThemeCustomizerEnhanced: React.FC = ( const [localTheme, setLocalTheme] = useState(value); const [selectedPreset, setSelectedPreset] = useState(presetName); const [customCss, setCustomCss] = useState(value.customCss || ''); - const [showCssInstructions, setShowCssInstructions] = useState(false); const BETA_LAYOUTS: GalleryLayoutType[] = ['gallery-premium', 'gallery-story']; const MIN_RECOMMENDED_THUMBNAIL_SIZE = 500; @@ -318,1083 +202,114 @@ export const ThemeCustomizerEnhanced: React.FC = ( handleChange('gallerySettings', updatedSettings); }; + const handleColorModeSelect = (mode: 'light' | 'dark' | 'auto') => { + handleChange('colorMode', mode); + // When switching to dark, auto-populate dark defaults if colors are still light + if (mode === 'dark' && (!localTheme.backgroundColor || localTheme.backgroundColor === '#fafafa' || localTheme.backgroundColor === '#ffffff')) { + const updated: ThemeConfig = { + ...localTheme, + colorMode: mode, + backgroundColor: '#0f0f0f', + surfaceColor: '#1a1a1a', + elevatedColor: '#242424', + surfaceBorderColor: '#2e2e2e', + textColor: '#e5e5e5', + mutedTextColor: '#a3a3a3', + }; + setLocalTheme(updated); + onChange({ ...updated, customCss }); + } else if (mode === 'light' && localTheme.colorMode === 'dark') { + const updated: ThemeConfig = { + ...localTheme, + colorMode: mode, + backgroundColor: '#fafafa', + surfaceColor: '#ffffff', + elevatedColor: '#f5f5f5', + surfaceBorderColor: '#e5e5e5', + textColor: '#171717', + mutedTextColor: '#737373', + }; + setLocalTheme(updated); + onChange({ ...updated, customCss }); + } + }; + + const handleCustomCssChange = (newCss: string) => { + setCustomCss(newCss); + // Mark as custom when CSS is added + if (newCss && selectedPreset !== 'custom' && onPresetChange) { + setSelectedPreset('custom'); + onPresetChange('custom'); + } + // Propagate to parent so Save sees the latest CSS (#323). + onChange({ ...localTheme, customCss: newCss }); + }; + return (
{/* Preset Themes */} - -

- - {t('branding.themePresets')} -

-
- {Object.entries(GALLERY_THEME_PRESETS).map(([key, theme]) => ( - - ))} -
- - {/* Warning: Beta preset with low thumbnail resolution */} - {isBetaLayout && isThumbnailTooSmall && !showGalleryLayouts && ( -
-
- -
-

- {t('branding.betaThumbnailWarningTitle')} -

-

- {t('branding.betaThumbnailWarningText', { width: thumbnailWidth, height: thumbnailHeight, recommended: MIN_RECOMMENDED_THUMBNAIL_SIZE })} -

- { - e.preventDefault(); - window.location.href = '/admin/settings'; - }} - > - {t('branding.betaThumbnailWarningLink')} → - -
-
-
- )} -
+ {/* Gallery Layout */} {showGalleryLayouts && ( - -

- - {t('branding.galleryLayout')} -

-
- {(Object.keys(layoutIcons) as GalleryLayoutType[]).map((layout) => ( - - ))} -
- - {/* Warning: Beta theme with low thumbnail resolution */} - {isBetaLayout && isThumbnailTooSmall && ( -
-
- -
-

- {t('branding.betaThumbnailWarningTitle')} -

-

- {t('branding.betaThumbnailWarningText', { width: thumbnailWidth, height: thumbnailHeight, recommended: MIN_RECOMMENDED_THUMBNAIL_SIZE })} -

- { - e.preventDefault(); - window.location.href = '/admin/settings'; - }} - > - {t('branding.betaThumbnailWarningLink')} → - -
-
-
- )} - - {/* Layout-specific settings */} - {localTheme.galleryLayout && ( -
-

{t('branding.layoutSettings')}

- - {/* Common settings */} -
-
- - -
- -
- - -
-
- - {/* Grid specific */} - {localTheme.galleryLayout === 'grid' && ( - <> -
- -
-
- - updateGallerySettings('gridColumns', { - ...localTheme.gallerySettings?.gridColumns, - mobile: parseInt(e.target.value) - })} - /> -
-
- - updateGallerySettings('gridColumns', { - ...localTheme.gallerySettings?.gridColumns, - tablet: parseInt(e.target.value) - })} - /> -
-
- - updateGallerySettings('gridColumns', { - ...localTheme.gallerySettings?.gridColumns, - desktop: parseInt(e.target.value) - })} - /> -
-
-
-
- - -

- {t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')} -

-
- - )} - - {/* Carousel specific */} - {localTheme.galleryLayout === 'carousel' && ( - <> -
- -
- {localTheme.gallerySettings?.carouselAutoplay && ( -
- - updateGallerySettings('carouselInterval', parseInt(e.target.value) * 1000)} - /> -
- )} - - )} - - {/* Timeline specific */} - {localTheme.galleryLayout === 'timeline' && ( -
- - -
- )} - - {/* Masonry specific */} - {localTheme.galleryLayout === 'masonry' && ( - <> -
- - -

- {localTheme.gallerySettings?.masonryMode === 'columns' - ? t('branding.masonryModeHint.columns', 'Pinterest-style vertical columns with varied heights') - : localTheme.gallerySettings?.masonryMode === 'flickr' - ? t('branding.masonryModeHint.flickr', 'Flickr\'s open-source justified layout algorithm') - : localTheme.gallerySettings?.masonryMode === 'justified' - ? t('branding.masonryModeHint.justified', 'Google Photos-style rows using Knuth-Plass algorithm for optimal breaks') - : t('branding.masonryModeHint.rows', 'Custom row-based justified layout')} -

-
- - {/* Thumbnail scale - only for columns mode */} - {(!localTheme.gallerySettings?.masonryMode || localTheme.gallerySettings?.masonryMode === 'columns') && ( -
- - -

- {t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')} -

-
- )} - - {/* Row-specific settings - show for all row-based modes */} - {['rows', 'flickr', 'justified'].includes(localTheme.gallerySettings?.masonryMode || '') && ( - <> -
- - updateGallerySettings('masonryRowHeight', parseInt(e.target.value))} - /> -

- {t('branding.targetRowHeightHint', 'Height in pixels (150-400). Photos will scale to fit rows.')} -

-
- {/* Last row behavior - only for rows and flickr modes */} - {['rows', 'flickr'].includes(localTheme.gallerySettings?.masonryMode || '') && ( -
- - -
- )} - - )} - - )} - - {/* Mosaic specific */} - {localTheme.galleryLayout === 'mosaic' && ( -
- - -

- {t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')} -

-
- )} -
- )} -
+ )} {/* Header Style - Decoupled from Layout */} {showGalleryLayouts && ( - -

- - {t('branding.headerStyle', 'Header Style')} -

-

- {t('branding.headerStyleDescription', 'Choose how the gallery header appears. The header style is independent of the photo layout.')} -

-
- {(Object.keys(headerStyleIcons) as HeaderStyleType[]).map((style) => ( - - ))} -
- - {/* Divider Style - Only show when hero header is selected */} - {localTheme.headerStyle === 'hero' && ( -
-

- {t('branding.heroDividerStyle', 'Divider Style')} -

-

- {t('branding.heroDividerDescription', 'Choose how the transition between the hero image and gallery content looks.')} -

-
- {(Object.keys(dividerStylePreviews) as HeroDividerStyle[]).map((divider) => ( - - ))} -
-
- )} -
+ )} {/* Controls Style */} {showGalleryLayouts && ( - -

- - {t('branding.controlsStyle', 'Controls Style')} -

-

- {t('branding.controlsStyleDescription', 'Choose how gallery filters and controls are displayed.')} -

-
- - -
- {/* Info about hero header */} - {localTheme.headerStyle === 'hero' && localTheme.controlsStyle !== 'sidebar' && ( -
-

- - {t('branding.controlsStyleHeroWarning', 'Sidebar is recommended for hero headers to prevent controls appearing above the hero image.')} -

-
- )} -
+ )} {/* Color Customization */} - -
-

- - {t('branding.colors')} -

- {/* "Sync from Branding" — caller-supplied so the customizer - doesn't have to know how to resolve the Branding theme. - Used in event create/edit to reset palette to site colours. */} - {onSyncFromBranding && ( - - )} -
- - {/* Color Mode Selector */} -
- {forcedColorActive && ( -
- {isBrandingContext - ? t('branding.forcedModeBrandingHint', 'Light/dark is locked site-wide by the Force control below — the per-theme mode picker is hidden because it would have no effect.') - : t('branding.forcedModeGalleryNote', 'A site-wide color lock is active, so this gallery follows the locked light/dark mode. Color and light/dark options are hidden here and can’t be overridden per gallery.')} -
- )} - {!forcedColorActive && (<> - -
- {(['light', 'dark', 'auto'] as const).map((mode) => ( - - ))} -
-

- {t('branding.colorModeHelp', 'Auto follows the visitor\'s system preference.')} -

- )} - - {/* - * Force color mode (instance-wide). Lives next to the per-theme - * Color Mode picker so the admin can find both controls in one - * place. The data flows through props from BrandingPage which - * persists it to branding settings; only renders when the - * onForceColorModeChange handler is provided (i.e. only on the - * Branding admin page, not in event-level theme editors). - */} - {onForceColorModeChange && ( -
-

- {t('branding.forceColorMode', 'Force color mode')} -

-

- {t( - 'branding.forceColorModeHelp', - 'Lock the entire admin and public site to dark or light. The user-facing dark/light toggle is hidden whenever a lock is active. Per-event themes that try to override the colour mode are also forced to follow.' - )} -

-
- {([ - { value: null, label: t('branding.forceColorModeNone', 'No force (user choice)') }, - { value: 'dark', label: t('branding.forceColorModeDark', 'Force dark') }, - { value: 'light', label: t('branding.forceColorModeLight', 'Force light') }, - ] as const).map(({ value, label }) => { - const active = (forceColorMode ?? null) === value; - return ( - - ); - })} -
-
- )} -
- - {/* - * 8-token CI palette pickers, grouped by role. - * Each token writes directly to the same field name on ThemeConfig - * (kebab → camel mapping happens via handleChange's first arg). - * Translation keys fall back to inline strings — German/English - * coverage only (per user language profile); other locales will - * show the fallback until reviewed by a native speaker. - */} - {/* - * 8-token CI palette pickers, grouped by role. Each picker label - * carries an Info icon whose `title` attribute renders the - * descriptive help text on hover (or long-press on touch). Keeping - * the help out of the static layout means every picker row is the - * same height so the four Surfaces and the two Accent rows align - * cleanly side-by-side. - */} - {!hideGalleryColors && ( -
- {/* Surfaces */} -
-

- {t('branding.colorGroupSurfaces', 'Surfaces')} - - - -

-
- {[ - { - key: 'backgroundColor', - label: t('branding.backgroundColor', 'Background'), - help: t('branding.backgroundColorHelp', 'The page itself — body background of every gallery, admin page and CMS page.'), - fallback: '#fafafa', - }, - { - key: 'surfaceColor', - label: t('branding.surfaceColor', 'Surface'), - help: t('branding.surfaceColorHelp', 'Cards, sidebar, header bar and navigation. The first layer above Background.'), - fallback: '#ffffff', - }, - { - key: 'elevatedColor', - label: t('branding.elevatedColor', 'Elevated'), - help: t('branding.elevatedColorHelp', 'Panels that float above cards: image placeholders, hover/active rows, modal headers, code blocks.'), - fallback: '#f5f5f5', - }, - { - key: 'surfaceBorderColor', - label: t('branding.borderColor', 'Border'), - help: t('branding.borderColorHelp', 'Dividers, table grid lines, card outlines, input borders.'), - fallback: '#e5e5e5', - }, - ].map(({ key, label, help, fallback }) => ( - )[key] || fallback} - fallback={fallback} - onChange={(v) => handleChange(key as keyof ThemeConfig, v)} - /> - ))} -
-
- - {/* Text */} -
-

- {t('branding.colorGroupText', 'Text')} - - - -

-
- {[ - { - key: 'textColor', - label: t('branding.textColor', 'Primary text'), - help: t('branding.textColorHelp', 'Headlines, body copy, table cells, form input values, navigation labels — the main text colour.'), - fallback: '#171717', - }, - { - key: 'mutedTextColor', - label: t('branding.mutedTextColor', 'Secondary text'), - help: t('branding.mutedTextColorHelp', 'Captions, helper text under inputs, table column headers, footer links, dates and metadata.'), - fallback: '#737373', - }, - ].map(({ key, label, help, fallback }) => ( - )[key] || fallback} - fallback={fallback} - onChange={(v) => handleChange(key as keyof ThemeConfig, v)} - /> - ))} -
-
- - {/* Accent */} -
-

- {t('branding.colorGroupAccent', 'Accent')} - - - -

-
- {[ - { - key: 'accentColor', - label: t('branding.accentColor', 'Accent'), - help: t( - 'branding.accentColorHelp', - 'Links, icons, focus rings, hover states on primary buttons, active sidebar item underline. Should read clearly on both Background and Surface.' - ), - fallback: '#22c55e', - }, - { - key: 'accentDarkColor', - label: t('branding.accentDarkColor', 'Accent (filled)'), - help: t( - 'branding.accentDarkColorHelp', - 'Filled CTA buttons, active sidebar item background, badges and tags. Needs enough contrast for white text to be readable on top.' - ), - fallback: '#5C8762', - }, - ].map(({ key, label, help, fallback }) => ( - )[key] || fallback} - fallback={fallback} - onChange={(v) => handleChange(key as keyof ThemeConfig, v)} - /> - ))} -
- {/* primaryColor is kept in sync with accentDarkColor inside - handleChange() — no dedicated picker. */} -
-
- )} -
+ {/* Typography & Style */} - -

- - {t('branding.typographyAndStyle')} -

-
-
-
- - -
- -
- - -
-
- - {/* Row 1: Font Size & Border Radius */} -
-
- - -
- -
- - -
-
- - {/* Row 2: Shadow Style & Background Pattern */} -
-
- - -
- -
- - -
-
-
-
+ {/* CSS Template Selector - only show if templates are provided */} {cssTemplates && cssTemplates.length > 0 && onCssTemplateChange && ( - -

- - {t('branding.cssTemplate', 'CSS Template')} -

-

- {t('branding.cssTemplateDescription', 'Select a pre-built CSS template to apply application-wide styling to this gallery. Templates can be managed in Settings > CSS Templates.')} -

-
- {/* No template option */} - - {/* Template options */} - {cssTemplates.map((template) => ( - - ))} -
-
+ )} {/* Caller-provided slot — used by BrandingPage to keep the @@ -1403,118 +318,11 @@ export const ThemeCustomizerEnhanced: React.FC = ( {slotBeforeCustomCss} {/* Event-specific Custom CSS */} - -

- - {t('branding.eventCustomCSS', 'Event-specific Custom CSS')} -

- - {/* Collapsible Instructions Panel */} -
- - - {showCssInstructions && ( -
- {/* Available CSS Variables */} -
-

- {t('branding.cssInstructions.variables', 'Theme CSS Variables')} -

-

- {t('branding.cssInstructions.variablesDesc', 'Use these CSS variables to match your theme presets:')} -

- -{`--color-background: ${localTheme.backgroundColor || '#fafafa'}; ---color-surface: ${localTheme.surfaceColor || '#ffffff'}; ---color-elevated: ${localTheme.elevatedColor || '#f5f5f5'}; ---color-surface-border: ${localTheme.surfaceBorderColor || '#e5e5e5'}; ---color-text: ${localTheme.textColor || '#171717'}; ---color-muted-text: ${localTheme.mutedTextColor || '#737373'}; ---color-accent: ${localTheme.accentColor || '#22c55e'}; ---color-accent-dark: ${localTheme.accentDarkColor || localTheme.primaryColor || '#5C8762'}; ---font-family: ${localTheme.fontFamily || 'Inter, sans-serif'}; ---heading-font: ${localTheme.headingFontFamily || localTheme.fontFamily || 'Inter, sans-serif'};`} - -
- - {/* Custom Gallery Layouts */} -
-

- {t('branding.cssInstructions.layouts', 'Custom Gallery Layouts')} -

-

- {t('branding.cssInstructions.layoutsDesc', 'Target gallery elements with these selectors:')} -

- -{`.gallery-container { /* Main gallery wrapper */ } -.gallery-grid { /* Photo grid container */ } -.gallery-item { /* Individual photo card */ } -.gallery-header { /* Header section */ } -.gallery-hero { /* Hero image area */ } -.photo-overlay { /* Photo hover overlay */ } -.photo-actions { /* Like/favorite buttons */ }`} - -
- - {/* Glassmorphism Example */} -
-

- {t('branding.cssInstructions.glassEffect', 'Glassmorphism Effect')} -

-

- {t('branding.cssInstructions.glassEffectDesc', 'Create modern glass effects:')} -

- -{`.glass-panel { - background: rgba(255, 255, 255, 0.25); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 16px; -}`} - -
- - {/* Tips */} -
- -
- {t('branding.cssInstructions.tip', 'Tip')}:{' '} - {t('branding.cssInstructions.tipText', 'Use CSS Templates from Settings > CSS Templates for pre-built designs like Apple Liquid Glass.')} -
-
-
- )} -
- -