key => [type, label, default or options, help]. Colours use [color, label, default]; choices use [select, label, [value => label]]. */ function design_fields(): array { return [ 'Colours' => [ 'accent' => ['color', 'Main colour', brand_color(), 'Banner tint, card bars and the ticket panel.'], 'accent2' => ['color', 'Second colour', '#1f4b5e', 'Pairs with the main colour in gradients.'], 'hero_text' => ['color', 'Banner text', '#ffffff', 'Title and description on the banner.'], 'hero_label' => ['color', 'Banner small labels', '#c9c9c9', 'DATE AND TIME, VENUE and similar.'], 'hero_panel' => ['color', 'Banner cards and text panel', '#0b0b0c', 'Behind the date, venue and (with no overlay) the text.'], 'cta_from' => ['color', 'Get tickets button, start', '#d40000', ''], 'cta_to' => ['color', 'Get tickets button, end', '#d40000', 'Same as the start for a flat colour.'], 'cta_text' => ['color', 'Get tickets button text', '#ffffff', ''], 'tile_from' => ['color', 'Date tile and venue pin, start', '#d40000', ''], 'tile_to' => ['color', 'Date tile and venue pin, end', '#d40000', ''], 'panel_head' => ['color', 'Ticket panel header', brand_color(), ''], 'sponsors_head' => ['color', 'Sponsors panel header', '#0b0b0c', ''], 'card_bg' => ['color', 'Detail card background', '#ffffff', 'About, getting there and terms.'], 'card_text' => ['color', 'Detail card text', '#0b0b0c', ''], 'page_bg' => ['color', 'Page background behind the event', '#f6f6f5', 'Leave off to keep your theme background.'], ], 'Font and style' => [ 'font' => ['select', 'Font', ['inter' => 'Inter', 'system' => 'System font of each device', 'serif' => 'Serif'], 'Applies to the whole event page.'], 'hero_ratio' => ['select', 'Banner height', ['11' => 'Square (1:1), tallest', '43' => '4:3 (default)', '32' => '3:2', '169' => '16:9', '21' => '2:1', '219' => '21:9 cinema', '31' => '3:1 slim', '41' => '4:1 strip, shortest', 'auto' => 'Just fit the text'], 'The cover image always fills the banner. If the text needs more room than the shape allows, the banner grows to fit it.'], 'hero_align' => ['select', 'Banner text position', ['left' => 'Left', 'center' => 'Centre'], ''], 'title_size' => ['select', 'Title size', ['m' => 'Medium', 'l' => 'Large (default)', 'xl' => 'Extra large'], ''], 'facts_style' => ['select', 'Date and venue cards', ['glass' => 'Dark glass (default)', 'solid' => 'Solid white', 'outline' => 'Outline only'], ''], 'cta_style' => ['select', 'Get tickets button', ['filled' => 'Filled (default)', 'outline' => 'Outline'], ''], 'show_countdown' => ['select', '"In 19 days" label', ['yes' => 'Show', 'no' => 'Hide'], ''], 'radius' => ['select', 'Corners', ['square' => 'Square', 'soft' => 'Soft (default)', 'round' => 'Very round'], ''], 'width' => ['select', 'Page width', ['theme' => 'Same as your theme', 'wide' => 'Wide', 'full' => 'Full width'], ''], ], ]; } function design_flat_fields(): array { $o = []; foreach (design_fields() as $fields) foreach ($fields as $k => $f) $o[$k] = $f; return $o; } /** Keep only valid, switched-on values from a submitted form. */ function design_clean(array $values, array $on): array { $out = []; foreach (design_flat_fields() as $k => $f) { $v = isset($values[$k]) && is_string($values[$k]) ? trim($values[$k]) : ''; if ($f[0] === 'color') { if (!empty($on[$k]) && preg_match('/^#[0-9a-fA-F]{6}$/', $v)) $out[$k] = strtolower($v); } elseif ($v !== '' && isset($f[2][$v])) { $out[$k] = $v; } } return $out; } function design_decode(?string $json): array { $a = $json ? json_decode($json, true) : null; if (!is_array($a)) return []; $out = design_clean($a, array_fill_keys(array_keys($a), 1)); if (!empty($a['layout']) && is_array($a['layout'])) $out['layout'] = layout_clean($a['layout']); return $out; } /** Site-wide choices, then this event's own on top. */ function event_design(array $ev): array { return design_decode($ev['design'] ?? null) + design_decode(setting('event_design')); } /** The CSS variables for the colours that are set, scoped to one page wrapper. */ function design_css(array $d, string $scope): string { $map = ['accent' => '--ev-accent', 'accent2' => '--ev-accent2', 'hero_text' => '--ev-hero-text', 'hero_label' => '--ev-hero-label', 'hero_panel' => '--ev-hero-panel', 'cta_from' => '--ev-cta-from', 'cta_to' => '--ev-cta-to', 'cta_text' => '--ev-cta-text', 'tile_from' => '--ev-tile-from', 'tile_to' => '--ev-tile-to', 'panel_head' => '--ev-panel-head', 'sponsors_head' => '--ev-sponsors-head', 'card_bg' => '--ev-card-bg', 'card_text' => '--ev-card-text', 'page_bg' => '--ev-page-bg']; $vars = []; foreach ($map as $k => $var) { $hex = css_hex($d[$k] ?? '', ''); if ($hex !== '') $vars[$var] = $hex; } if (isset($vars['--ev-accent'])) $vars['--ev-accent-dark'] = 'color-mix(in srgb,' . $vars['--ev-accent'] . ' 78%,#000)'; if (isset($vars['--ev-panel-head'])) $vars['--ev-panel-head-dark'] = 'color-mix(in srgb,' . $vars['--ev-panel-head'] . ' 76%,#000)'; return css_vars($scope, $vars); } /** Layout choices as classes on the page wrapper. */ function design_classes(array $d): string { $c = []; foreach (['hero_ratio' => 'ratio', 'hero_align' => 'align', 'title_size' => 'title', 'facts_style' => 'facts', 'cta_style' => 'cta', 'radius' => 'r', 'width' => 'w', 'font' => 'font'] as $k => $p) { if (!empty($d[$k])) $c[] = 'ev-' . $p . '-' . $d[$k]; } if (!empty($d['page_bg'])) $c[] = 'ev-has-bg'; return implode(' ', $c); } /* ---------- Visual layout designer ---------- */ /** The blocks of an event page: id => [label, help, can be hidden]. */ function layout_blocks(): array { return [ 'hero' => ['Banner', 'Image, title, date, venue and the Get tickets button.', true], 'about' => ['About this event', 'The full description.', true], 'details' => ['Getting there and terms', 'Transport, accessibility and event terms.', true], 'gate' => ['Living gate', 'The Gatecraft gate, filling in from the ground up as tickets sell. Shown when tickets are on sale.', true], 'tickets' => ['Tickets and booking', 'Ticket categories and the booking form. Always shown.', false], 'sponsors' => ['Sponsors', 'Logos and links of the sponsors added to the event.', true], ]; } function layout_zones(): array { return ['top' => 'Top, full width', 'main' => 'Main column', 'side' => 'Side column', 'bottom' => 'Bottom, full width']; } /** Choices for the options on a block: key => [label, [value => label]]. Colours are separate. */ function layout_style_choices(): array { return [ 'bw' => ['Border width', ['1' => 'Thin', '2' => 'Medium', '4' => 'Thick']], 'radius' => ['Corners', ['square' => 'Square', 'soft' => 'Soft', 'round' => 'Very round']], 'shadow' => ['Shadow', ['none' => 'None', 'soft' => 'Soft', 'strong' => 'Strong']], 'gap' => ['Space above', ['none' => 'None', 's' => 'Small', 'l' => 'Large']], ]; } /** The layout a page gets before anyone designs it, following the older ticket layout and sponsors options. */ function layout_default(array $d = []): array { $below = ($d['ticket_layout'] ?? setting('ticket_layout')) === 'below'; $sponsorsMain = ($d['sponsors_pos'] ?? 'side') === 'main'; $z = ['top' => ['hero'], 'main' => ['about', 'details'], 'side' => ['gate', 'tickets', 'sponsors'], 'bottom' => []]; if ($sponsorsMain) { $z['side'] = array_values(array_diff($z['side'], ['sponsors'])); $z['main'][] = 'sponsors'; } if ($below) { $z['main'] = array_merge($z['main'], $z['side']); $z['side'] = []; } return ['zones' => $z, 'hidden' => [], 'cols' => 'normal', 'flip' => 0, 'style' => []]; } /** Validate a layout from the designer. Every block ends up in exactly one zone. */ function layout_clean($raw): array { if (!is_array($raw)) return []; $blocks = layout_blocks(); $zones = ['top' => [], 'main' => [], 'side' => [], 'bottom' => []]; $seen = []; foreach ($zones as $z => $_) { foreach ((array)($raw['zones'][$z] ?? []) as $id) { if (is_string($id) && isset($blocks[$id]) && !isset($seen[$id])) { $zones[$z][] = $id; $seen[$id] = 1; } } } $def = layout_default()['zones']; foreach ($def as $z => $ids) foreach ($ids as $id) if (!isset($seen[$id])) { $zones[$z][] = $id; $seen[$id] = 1; } foreach ($blocks as $id => $_) if (!isset($seen[$id])) $zones['main'][] = $id; $hidden = []; foreach ((array)($raw['hidden'] ?? []) as $id) if (is_string($id) && !empty($blocks[$id][2])) $hidden[] = $id; $style = []; $choices = layout_style_choices(); foreach ((array)($raw['style'] ?? []) as $id => $st) { if (!isset($blocks[$id]) || !is_array($st)) continue; $o = []; foreach (['bg', 'fg', 'bd'] as $c) if (isset($st[$c]) && is_string($st[$c]) && preg_match('/^#[0-9a-fA-F]{6}$/', $st[$c])) $o[$c] = strtolower($st[$c]); foreach ($choices as $k => $ch) if (isset($st[$k]) && is_string($st[$k]) && isset($ch[1][$st[$k]])) $o[$k] = $st[$k]; if ($id === 'hero') unset($o['bg'], $o['fg']); if ($o) $style[$id] = $o; } $cols = isset($raw['cols']) && in_array($raw['cols'], ['normal', 'main', 'side', 'equal'], true) ? $raw['cols'] : 'normal'; return ['zones' => $zones, 'hidden' => array_values(array_unique($hidden)), 'cols' => $cols, 'flip' => !empty($raw['flip']) ? 1 : 0, 'style' => $style]; } /** The layout in force for an event: its own, else the site's, else the default. */ function event_layout(array $design): array { return !empty($design['layout']) ? $design['layout'] : layout_default($design); } /** Classes on the wrapper of one block. */ function layout_block_classes(string $id, array $layout): string { $st = $layout['style'][$id] ?? []; $c = ['evb', 'evb-' . $id]; foreach (['bg', 'fg', 'bd'] as $k) if (!empty($st[$k])) $c[] = 'evb-has-' . $k; foreach (['bw' => 'bw', 'radius' => 'r', 'shadow' => 'sh', 'gap' => 'gap'] as $k => $p) if (!empty($st[$k])) $c[] = 'evb-' . $p . '-' . $st[$k]; return implode(' ', $c); } /** Colour variables of each block, scoped to one page wrapper. Only validated hex values reach this. */ function layout_css(array $layout, string $scope): string { $css = ''; foreach ($layout['style'] ?? [] as $id => $st) { if (!is_string($id) || !preg_match('/^[a-z0-9_-]{1,40}$/', $id) || !is_array($st)) continue; $vars = []; foreach (['bg' => '--evb-bg', 'fg' => '--evb-fg', 'bd' => '--evb-bd'] as $k => $var) { $hex = css_hex($st[$k] ?? '', ''); if ($hex !== '') $vars[$var] = $hex; } $css .= css_vars($scope . ' .evb-' . $id, $vars); } return $css; } /** Colour and style choices were just saved from a form: carry over the layout stored before, which the form does not hold. */ function design_keep_layout(array $new, ?string $oldJson): array { $old = design_decode($oldJson); if (!empty($old['layout'])) $new['layout'] = $old['layout']; return $new; }