Neue Branding-Page und Gäste-PWA reagiert nun auf Branding-Einstellungen vom event-admin. Implemented local Google Fonts pipeline and admin UI selects for branding and invites.

- Added fonts:sync-google command (uses GOOGLE_FONTS_API_KEY, generates /public/fonts/google files, manifest, CSS, cache flush) and
    exposed manifest via new GET /api/v1/tenant/fonts endpoint with fallbacks for existing local fonts.
  - Imported generated fonts CSS, added API client + font loader hook, and wired branding page font fields to searchable selects (with
    custom override) that auto-load selected fonts.
  - Invites layout editor now offers font selection per element with runtime font loading for previews/export alignment.
  - New tests cover font sync command and font manifest API.

  Tests run: php artisan test --filter=Fonts --testsuite=Feature.
  Note: repository already has other modified files (e.g., EventPublicController, SettingsStoreRequest, guest components, etc.); left
  untouched. Run php artisan fonts:sync-google after setting the API key to populate /public/fonts/google.
This commit is contained in:
Codex Agent
2025-11-25 19:31:52 +01:00
parent 4d31eb4d42
commit 9bde8f3f32
38 changed files with 2420 additions and 104 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Http\Requests\Tenant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SettingsStoreRequest extends FormRequest
{
@@ -28,6 +29,29 @@ class SettingsStoreRequest extends FormRequest
'settings.branding.primary_color' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.secondary_color' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.font_family' => ['nullable', 'string', 'max:100'],
'settings.branding.surface_color' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.mode' => ['nullable', Rule::in(['light', 'dark', 'auto'])],
'settings.branding.use_default_branding' => ['nullable', 'boolean'],
'settings.branding.palette' => ['nullable', 'array'],
'settings.branding.palette.primary' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.palette.secondary' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.palette.background' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.palette.surface' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.typography' => ['nullable', 'array'],
'settings.branding.typography.heading' => ['nullable', 'string', 'max:150'],
'settings.branding.typography.body' => ['nullable', 'string', 'max:150'],
'settings.branding.typography.size' => ['nullable', Rule::in(['s', 'm', 'l'])],
'settings.branding.logo' => ['nullable', 'array'],
'settings.branding.logo.mode' => ['nullable', Rule::in(['emoticon', 'upload'])],
'settings.branding.logo.value' => ['nullable', 'string', 'max:500'],
'settings.branding.logo.position' => ['nullable', Rule::in(['left', 'right', 'center'])],
'settings.branding.logo.size' => ['nullable', Rule::in(['s', 'm', 'l'])],
'settings.branding.buttons' => ['nullable', 'array'],
'settings.branding.buttons.style' => ['nullable', Rule::in(['filled', 'outline'])],
'settings.branding.buttons.radius' => ['nullable', 'integer', 'min:0', 'max:64'],
'settings.branding.buttons.primary' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.buttons.secondary' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.branding.buttons.link_color' => ['nullable', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'settings.features' => ['sometimes', 'array'],
'settings.features.photo_likes_enabled' => ['nullable', 'boolean'],
'settings.features.event_checklist' => ['nullable', 'boolean'],