diff --git a/app/Helpers/ThemeHelper.php b/app/Helpers/ThemeHelper.php index c1c446f..4d17ba6 100644 --- a/app/Helpers/ThemeHelper.php +++ b/app/Helpers/ThemeHelper.php @@ -10,6 +10,15 @@ class ThemeHelper public static function getLogoPath(string $type = 'positive'): string { $theme = config('app.theme', 'b2in'); + return self::getLogoPathForBrand($theme, $type); + } + + /** + * Get the logo path for a specific brand + */ + public static function getLogoPathForBrand(?string $brand = null, string $type = 'positive'): string + { + $brand = $brand ?? config('app.theme', 'b2in'); $logoMap = [ 'b2in' => [ @@ -30,7 +39,7 @@ class ThemeHelper ] ]; - return $logoMap[$theme][$type] ?? $logoMap['b2in'][$type]; + return $logoMap[$brand][$type] ?? $logoMap['b2in'][$type]; } /** @@ -110,4 +119,45 @@ class ThemeHelper $config = self::getDomainConfig(); return $config['url'] ?? config('app.url'); } + + /** + * Get brand colors for a specific brand + */ + public static function getBrandColors(?string $brand = null): array + { + $brand = $brand ?? config('app.theme', 'b2in'); + $config = config("domains.domains.{$brand}", []); + + return [ + 'primary' => $config['color_scheme']['primary'] ?? '#2b3f51', + 'secondary' => $config['color_scheme']['secondary'] ?? '#20a0da', + 'accent' => $config['color_scheme']['accent'] ?? null, + ]; + } + + /** + * Get brand name for a specific brand + */ + public static function getBrandName(?string $brand = null): string + { + $brand = $brand ?? config('app.theme', 'b2in'); + + $brandNames = [ + 'b2in' => 'B2IN', + 'b2a' => 'B2A', + 'stileigentum' => 'StilEigentum', + 'style2own' => 'Style2Own', + ]; + + return $brandNames[$brand] ?? strtoupper($brand); + } + + /** + * Get all brand configuration for a specific brand + */ + public static function getBrandConfig(?string $brand = null): array + { + $brand = $brand ?? config('app.theme', 'b2in'); + return config("domains.domains.{$brand}", []); + } } diff --git a/app/Http/Controllers/Api/DisplayConfigController.php b/app/Http/Controllers/Api/DisplayConfigController.php new file mode 100644 index 0000000..6f9bade --- /dev/null +++ b/app/Http/Controllers/Api/DisplayConfigController.php @@ -0,0 +1,43 @@ +get()->map(function ($video) { + return [ + 'src' => $video->full_path, + 'position' => $video->position, + ]; + }); + + $footerContent = DisplayFooterContent::active()->get()->map(function ($footer) { + $data = [ + 'headline' => $footer->headline, + 'subline' => $footer->subline, + ]; + + // URL nur hinzufügen wenn vorhanden + if ($footer->url) { + $data['url'] = $footer->short_url; + } + + return $data; + }); + + return response()->json([ + 'videoPlaylist' => $videos, + 'footerContent' => $footerContent, + ]); + } +} diff --git a/app/Http/Middleware/BasicAuthMiddleware.php b/app/Http/Middleware/BasicAuthMiddleware.php index 5767e29..6c7e383 100644 --- a/app/Http/Middleware/BasicAuthMiddleware.php +++ b/app/Http/Middleware/BasicAuthMiddleware.php @@ -28,6 +28,16 @@ class BasicAuthMiddleware return $next($request); } + // Skip Basic Auth für Flux UI Assets (flux.js, flux.min.js, editor.js, etc.) + if (str_starts_with($path, 'flux/')) { + return $next($request); + } + + // Skip Basic Auth für Display-API und Short-Links (öffentlicher Zugriff für Display-Seite) + if ($request->is('api/display/*') || $request->is('_cabinet/*')) { + return $next($request); + } + // Credentials from .env file $user = config('auth.basic.user'); $pass = config('auth.basic.password'); diff --git a/app/Http/Middleware/SetDomainUrl.php b/app/Http/Middleware/SetDomainUrl.php new file mode 100644 index 0000000..494438e --- /dev/null +++ b/app/Http/Middleware/SetDomainUrl.php @@ -0,0 +1,59 @@ +getHost(); + + // Suche nach der Domain-Konfiguration + $domainConfig = null; + $domains = config('domains.domains', []); + + foreach ($domains as $name => $config) { + if (is_array($config) && isset($config['domain_name']) && $config['domain_name'] === $host) { + $domainConfig = $config; + break; + } + } + + // Wenn eine Domain-Konfiguration gefunden wurde, setze die URL + if ($domainConfig && isset($domainConfig['url'])) { + $domainUrl = $domainConfig['url']; + + // URL-Generator konfigurieren + URL::forceRootUrl($domainUrl); + URL::forceScheme(parse_url($domainUrl, PHP_URL_SCHEME) ?: 'https'); + + // Asset-Root setzen + /** @var UrlGenerator $urlGenerator */ + $urlGenerator = app('url'); + $urlGenerator->useAssetOrigin($domainUrl); + + // Config aktualisieren + config([ + 'app.url' => $domainUrl, + 'app.asset_url' => $domainUrl, + ]); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/ThemeMiddleware.php b/app/Http/Middleware/ThemeMiddleware.php index 391260f..27a9166 100644 --- a/app/Http/Middleware/ThemeMiddleware.php +++ b/app/Http/Middleware/ThemeMiddleware.php @@ -19,13 +19,13 @@ class ThemeMiddleware $path = $request->path(); // Theme-Switching über Subdomains - if (str_contains($host, 'b2in.test')) { + if (str_contains($host, 'b2in')) { config(['app.theme' => 'b2in']); - } elseif (str_contains($host, 'b2a.test')) { + } elseif (str_contains($host, 'b2a') || str_contains($host, 'bridges2america')) { config(['app.theme' => 'b2a']); - } elseif (str_contains($host, 'stileigentum.test')) { + } elseif (str_contains($host, 'stileigentum')) { config(['app.theme' => 'stileigentum']); - } elseif (str_contains($host, 'style2own.test')) { + } elseif (str_contains($host, 'style2own')) { config(['app.theme' => 'style2own']); } @@ -41,7 +41,7 @@ class ThemeMiddleware if (str_starts_with($path, 'b2in/')) { config(['app.theme' => 'b2in']); $request->server->set('REQUEST_URI', '/' . substr($path, 5)); // Entferne 'b2in/' vom Pfad - } elseif (str_starts_with($path, 'b2a/')) { + } elseif (str_starts_with($path, 'b2a/') || str_starts_with($path, 'bridges2america/')) { config(['app.theme' => 'b2a']); $request->server->set('REQUEST_URI', '/' . substr($path, 4)); // Entferne 'b2a/' vom Pfad } elseif (str_starts_with($path, 'stileigentum/')) { diff --git a/app/Livewire/Admin/CMS/CabinetDisplay.php b/app/Livewire/Admin/CMS/CabinetDisplay.php new file mode 100644 index 0000000..78e86db --- /dev/null +++ b/app/Livewire/Admin/CMS/CabinetDisplay.php @@ -0,0 +1,292 @@ +loadAvailableVideos(); + } + + /** + * Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner + */ + public function loadAvailableVideos() + { + $assetsPath = public_path('_cabinet/assets'); + + if (File::exists($assetsPath)) { + $files = File::files($assetsPath); + $this->availableVideos = collect($files) + ->map(fn($file) => $file->getFilename()) + ->filter(fn($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov'])) + ->values() + ->toArray(); + } + } + + // ======================================== + // VIDEO-VERWALTUNG + // ======================================== + + public function openVideoModal($id = null) + { + if ($id) { + $video = DisplayVideo::findOrFail($id); + $this->videoId = $video->id; + $this->videoFilename = $video->filename; + $this->videoTitle = $video->title ?? ''; + $this->videoPosition = $video->position; + $this->videoIsActive = $video->is_active; + } else { + $this->resetVideoForm(); + } + $this->showVideoModal = true; + } + + public function saveVideo() + { + $this->validate([ + 'videoFilename' => 'required|string', + 'videoPosition' => 'required|integer|min:0|max:100', + ], [ + 'videoFilename.required' => 'Bitte wählen Sie ein Video aus.', + 'videoPosition.required' => 'Die Position ist erforderlich.', + 'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.', + 'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.', + ]); + + $data = [ + 'filename' => $this->videoFilename, + 'title' => $this->videoTitle, + 'position' => $this->videoPosition, + 'is_active' => $this->videoIsActive, + ]; + + if ($this->videoId) { + $video = DisplayVideo::findOrFail($this->videoId); + $video->update($data); + session()->flash('success', 'Video erfolgreich aktualisiert!'); + } else { + $maxSortOrder = DisplayVideo::max('sort_order') ?? 0; + $data['sort_order'] = $maxSortOrder + 1; + DisplayVideo::create($data); + session()->flash('success', 'Video erfolgreich hinzugefügt!'); + } + + $this->closeVideoModal(); + } + + public function deleteVideo($id) + { + DisplayVideo::findOrFail($id)->delete(); + session()->flash('success', 'Video erfolgreich gelöscht!'); + } + + public function toggleVideoStatus($id) + { + $video = DisplayVideo::findOrFail($id); + $video->update(['is_active' => !$video->is_active]); + } + + public function moveVideo($id, $direction) + { + $video = DisplayVideo::findOrFail($id); + $currentOrder = $video->sort_order; + + if ($direction === 'up' && $currentOrder > 0) { + $swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first(); + if ($swapVideo) { + $video->update(['sort_order' => $currentOrder - 1]); + $swapVideo->update(['sort_order' => $currentOrder]); + } + } elseif ($direction === 'down') { + $swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first(); + if ($swapVideo) { + $video->update(['sort_order' => $currentOrder + 1]); + $swapVideo->update(['sort_order' => $currentOrder]); + } + } + } + + public function resetVideoForm() + { + $this->videoId = null; + $this->videoFilename = ''; + $this->videoTitle = ''; + $this->videoPosition = 25; + $this->videoIsActive = true; + } + + public function closeVideoModal() + { + $this->showVideoModal = false; + $this->resetVideoForm(); + } + + // ======================================== + // FOOTER-CONTENT-VERWALTUNG + // ======================================== + + public function openFooterModal($id = null) + { + if ($id) { + $footer = DisplayFooterContent::findOrFail($id); + $this->footerId = $footer->id; + $this->footerHeadline = $footer->headline; + $this->footerSubline = $footer->subline; + $this->footerUrl = $footer->url; + $this->footerIsActive = $footer->is_active; + } else { + $this->resetFooterForm(); + } + $this->showFooterModal = true; + } + + public function saveFooter() + { + $this->validate([ + 'footerHeadline' => 'required|string|max:255', + 'footerSubline' => 'required|string|max:255', + 'footerUrl' => 'nullable|url', + ], [ + 'footerHeadline.required' => 'Die Überschrift ist erforderlich.', + 'footerSubline.required' => 'Die Unterzeile ist erforderlich.', + 'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.', + ]); + + $data = [ + 'headline' => $this->footerHeadline, + 'subline' => $this->footerSubline, + 'url' => $this->footerUrl ?: null, + 'is_active' => $this->footerIsActive, + ]; + + if ($this->footerId) { + $footer = DisplayFooterContent::findOrFail($this->footerId); + $footer->update($data); + + // Short-Code generieren falls URL vorhanden aber noch kein Short-Code + if ($footer->url && !$footer->short_code) { + $footer->short_code = DisplayFooterContent::generateUniqueShortCode(); + $footer->save(); + } + + session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!'); + } else { + $maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0; + $data['sort_order'] = $maxSortOrder + 1; + + $footer = DisplayFooterContent::create($data); + + // Short-Code nur generieren wenn URL vorhanden + if ($footer->url) { + $footer->short_code = DisplayFooterContent::generateUniqueShortCode(); + $footer->save(); + session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: ' . $footer->short_url); + } else { + session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)'); + } + } + + $this->closeFooterModal(); + } + + public function regenerateShortCode($id) + { + $footer = DisplayFooterContent::findOrFail($id); + $footer->short_code = DisplayFooterContent::generateUniqueShortCode(); + $footer->save(); + session()->flash('success', 'Short-Code wurde neu generiert!'); + } + + public function resetClicks($id) + { + $footer = DisplayFooterContent::findOrFail($id); + $footer->clicks = 0; + $footer->save(); + session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!'); + } + + public function deleteFooter($id) + { + DisplayFooterContent::findOrFail($id)->delete(); + session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!'); + } + + public function toggleFooterStatus($id) + { + $footer = DisplayFooterContent::findOrFail($id); + $footer->update(['is_active' => !$footer->is_active]); + } + + public function moveFooter($id, $direction) + { + $footer = DisplayFooterContent::findOrFail($id); + $currentOrder = $footer->sort_order; + + if ($direction === 'up' && $currentOrder > 0) { + $swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first(); + if ($swapFooter) { + $footer->update(['sort_order' => $currentOrder - 1]); + $swapFooter->update(['sort_order' => $currentOrder]); + } + } elseif ($direction === 'down') { + $swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first(); + if ($swapFooter) { + $footer->update(['sort_order' => $currentOrder + 1]); + $swapFooter->update(['sort_order' => $currentOrder]); + } + } + } + + public function resetFooterForm() + { + $this->footerId = null; + $this->footerHeadline = ''; + $this->footerSubline = ''; + $this->footerUrl = ''; + $this->footerIsActive = true; + } + + public function closeFooterModal() + { + $this->showFooterModal = false; + $this->resetFooterForm(); + } + + public function render() + { + $videos = DisplayVideo::orderBy('sort_order')->get(); + $footerContents = DisplayFooterContent::orderBy('sort_order')->get(); + + return view('livewire.admin.c-m-s.cabinet-display', [ + 'videos' => $videos, + 'footerContents' => $footerContents, + ]); + } +} diff --git a/app/Livewire/Web/Components/UI/Header.php b/app/Livewire/Web/Components/UI/Header.php index 2807b8c..c3916e9 100644 --- a/app/Livewire/Web/Components/UI/Header.php +++ b/app/Livewire/Web/Components/UI/Header.php @@ -16,7 +16,18 @@ class Header extends Component $this->domainName = \App\Helpers\ThemeHelper::getDomainName(); $this->domainUrl = \App\Helpers\ThemeHelper::getDomainUrl(); $theme = config('app.theme', 'b2in'); - $this->content = config("content.themes.{$theme}.header", []); + $this->content = config("content.themes.{$theme}.header", [ + 'portal_login' => 'Portal Login', + 'navigation' => [] + ]); + + // Ensure required keys exist + if (!isset($this->content['portal_login'])) { + $this->content['portal_login'] = 'Portal Login'; + } + if (!isset($this->content['navigation']) || !is_array($this->content['navigation'])) { + $this->content['navigation'] = []; + } } public function toggleMobileMenu() diff --git a/app/Models/DisplayFooterContent.php b/app/Models/DisplayFooterContent.php new file mode 100644 index 0000000..3ff0f7f --- /dev/null +++ b/app/Models/DisplayFooterContent.php @@ -0,0 +1,89 @@ + 'boolean', + 'sort_order' => 'integer', + 'clicks' => 'integer', + ]; + + /** + * Boot-Methode für automatische Short-Code-Generierung + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + // Short-Code nur generieren wenn URL vorhanden ist + if ($model->url && empty($model->short_code)) { + $model->short_code = self::generateUniqueShortCode(); + } + }); + } + + /** + * Generiert einen eindeutigen Short-Code + */ + public static function generateUniqueShortCode(): string + { + do { + // Generiere einen 6-stelligen alphanumerischen Code + $code = strtolower(substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 6)); + } while (self::where('short_code', $code)->exists()); + + return $code; + } + + /** + * Gibt die Short-URL zurück (dynamisch je nach Umgebung) + */ + public function getShortUrlAttribute(): string + { + $basePath = config('display.base_path', '_display-b2in-eu'); + $subdomain = config('display.subdomain'); + $domain = config('display.domain', 'b2in.eu'); + + // Wenn Subdomain konfiguriert ist, verwende diese (Live-Server) + if ($subdomain) { + // Nutze HTTPS für Produktion + $protocol = config('app.env') === 'production' ? 'https' : 'http'; + return "{$protocol}://{$subdomain}.{$domain}/go.php?z={$this->short_code}"; + } + + // Ansonsten verwende APP_URL + Base-Path (Testserver) + $baseUrl = rtrim(config('app.url'), '/'); + return "{$baseUrl}/{$basePath}/go.php?z={$this->short_code}"; + } + + /** + * Erhöht den Klick-Zähler + */ + public function incrementClicks(): void + { + $this->increment('clicks'); + } + + /** + * Scope für aktive Footer-Inhalte in sortierter Reihenfolge + */ + public function scopeActive($query) + { + return $query->where('is_active', true)->orderBy('sort_order'); + } +} diff --git a/app/Models/DisplayVideo.php b/app/Models/DisplayVideo.php new file mode 100644 index 0000000..0be1bed --- /dev/null +++ b/app/Models/DisplayVideo.php @@ -0,0 +1,38 @@ + 'boolean', + 'position' => 'integer', + 'sort_order' => 'integer', + ]; + + /** + * Scope für aktive Videos in sortierter Reihenfolge + */ + public function scopeActive($query) + { + return $query->where('is_active', true)->orderBy('sort_order'); + } + + /** + * Gibt den vollständigen Pfad zum Video zurück + */ + public function getFullPathAttribute(): string + { + return "assets/{$this->filename}"; + } +} diff --git a/app/Models/Partner.php b/app/Models/Partner.php index c3a0afa..36ecb9b 100644 --- a/app/Models/Partner.php +++ b/app/Models/Partner.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; class Partner extends Model { @@ -13,9 +14,22 @@ class Partner extends Model protected $fillable = [ 'company_name', + 'display_name', 'slug', 'type', + 'brand', 'hub_id', + 'parent_partner_id', + 'salutation', + 'first_name', + 'last_name', + 'street', + 'house_number', + 'zip', + 'city', + 'country', + 'phone', + 'website', 'description', 'logo_url', 'is_active', @@ -50,6 +64,46 @@ class Partner extends Model return $this->hasMany(User::class); } + /** + * Für Kunden: Zugeordneter Makler/Händler (Parent-Partner) + */ + public function parentPartner(): BelongsTo + { + return $this->belongsTo(Partner::class, 'parent_partner_id'); + } + + /** + * Für Makler/Händler: Zugeordnete Kunden (Child-Partners) + */ + public function childPartners(): HasMany + { + return $this->hasMany(Partner::class, 'parent_partner_id'); + } + + /** + * Alias für parentPartner (für bessere Lesbarkeit im Code) + */ + public function broker(): BelongsTo + { + return $this->parentPartner(); + } + + /** + * Alias für childPartners (für bessere Lesbarkeit im Code) + */ + public function customers(): HasMany + { + return $this->childPartners(); + } + + /** + * Ein Partner (Manufacturer) kann eine Marke haben + */ + public function brand(): HasOne + { + return $this->hasOne(Brand::class); + } + // TODO: Später die Beziehung zu Products hinzufügen // public function products(): HasMany // { diff --git a/app/Models/RegistrationCode.php b/app/Models/RegistrationCode.php new file mode 100644 index 0000000..2b9a0a5 --- /dev/null +++ b/app/Models/RegistrationCode.php @@ -0,0 +1,87 @@ + 'array', + 'expires_at' => 'datetime', + 'used_at' => 'datetime', + ]; + + public function broker(): BelongsTo + { + return $this->belongsTo(Partner::class, 'broker_partner_id'); + } + + public function partner(): BelongsTo + { + return $this->belongsTo(Partner::class); + } + + public function usedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'used_by_user_id'); + } + + public function assignedToCode(): BelongsTo + { + return $this->belongsTo(RegistrationCode::class, 'assigned_to_code_id'); + } + + public function scopeAvailable($query) + { + return $query->where('status', self::STATUS_AVAILABLE); + } + + public function isAvailable(): bool + { + if ($this->status !== self::STATUS_AVAILABLE) { + return false; + } + + if ($this->expires_at && now()->greaterThan($this->expires_at)) { + return false; + } + + return true; + } + + public function markUsed(?User $user = null): void + { + $this->status = self::STATUS_USED; + $this->used_at = now(); + if ($user) { + $this->used_by_user_id = $user->id; + if ($user->partner_id && !$this->partner_id) { + $this->partner_id = $user->partner_id; + } + } + $this->save(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index bb3b3e2..11cff19 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,8 +2,11 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Notifications\CustomResetPasswordNotification; +use App\Notifications\CustomVerifyEmailNotification; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; @@ -11,11 +14,12 @@ use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; use Spatie\Permission\Traits\HasRoles; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOne; -class User extends Authenticatable +class User extends Authenticatable implements MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable; + use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable, SoftDeletes; /** * The attributes that are mass assignable. @@ -25,6 +29,7 @@ class User extends Authenticatable protected $fillable = [ 'partner_id', 'name', + 'display_name', 'email', 'password', 'email_verified_at', @@ -49,6 +54,7 @@ class User extends Authenticatable { return [ 'email_verified_at' => 'datetime', + 'deleted_at' => 'datetime', 'password' => 'hashed', ]; } @@ -57,6 +63,15 @@ class User extends Authenticatable { return $this->belongsTo(Partner::class); } + + /** + * Get the registration code used by this user + */ + public function registrationCode(): HasOne + { + return $this->hasOne(RegistrationCode::class, 'used_by_user_id'); + } + /** * Get the user's initials */ @@ -67,4 +82,56 @@ class User extends Authenticatable ->map(fn(string $name) => Str::of($name)->substr(0, 1)) ->implode(''); } + + /** + * Anonymize user data (for users with dependencies) + */ + public function anonymize(): void + { + $this->update([ + 'name' => 'Gelöschter Benutzer #' . $this->id, + 'display_name' => null, + 'email' => 'deleted_' . $this->id . '@anonymized.local', + 'password' => bcrypt(Str::random(64)), + ]); + + // Entferne alle Rollen + $this->syncRoles([]); + + // Soft Delete + $this->delete(); + } + + /** + * Check if user has dependencies that require anonymization instead of deletion + */ + public function hasDependencies(): bool + { + // TODO: Später erweitern mit weiteren Verknüpfungen + // Beispiele: Orders, Projects, Documents, etc. + + // Aktuell: Prüfe ob Partner existiert + return $this->partner_id !== null; + } + + /** + * Send the password reset notification. + * + * @param string $token + * @return void + */ + public function sendPasswordResetNotification($token): void + { + $this->notify(new CustomResetPasswordNotification($token)); + } + + /** + * Send the email verification notification. + * + * @return void + */ + public function sendEmailVerificationNotification(): void + { + $this->notify(new CustomVerifyEmailNotification); + } } diff --git a/app/Notifications/CustomResetPasswordNotification.php b/app/Notifications/CustomResetPasswordNotification.php new file mode 100644 index 0000000..07a4162 --- /dev/null +++ b/app/Notifications/CustomResetPasswordNotification.php @@ -0,0 +1,30 @@ +subject('Passwort zurücksetzen - ' . config('app.name')) + ->greeting('Hallo!') + ->line('Sie erhalten diese E-Mail, weil wir eine Anfrage zum Zurücksetzen des Passworts für Ihr Konto erhalten haben.') + ->line('Klicken Sie auf den folgenden Button, um ein neues Passwort zu vergeben:') + ->action('Passwort zurücksetzen', $url) + ->line('Dieser Link ist **' . $expiryMinutes . ' Minuten** gültig.') + ->line('Falls Sie keine Passwort-Zurücksetzung angefordert haben, ist keine weitere Aktion erforderlich. Ihr Passwort bleibt unverändert.') + ->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team'); + } +} diff --git a/app/Notifications/CustomVerifyEmailNotification.php b/app/Notifications/CustomVerifyEmailNotification.php new file mode 100644 index 0000000..c63122d --- /dev/null +++ b/app/Notifications/CustomVerifyEmailNotification.php @@ -0,0 +1,29 @@ +subject('E-Mail-Adresse bestätigen - ' . config('app.name')) + ->greeting('Willkommen bei ' . config('app.name') . '!') + ->line('Vielen Dank für Ihre Registrierung!') + ->line('Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Button klicken:') + ->action('E-Mail-Adresse bestätigen', $url) + ->line('Dieser Bestätigungslink läuft in **60 Minuten** ab.') + ->line('Falls Sie kein Konto bei uns erstellt haben, ignorieren Sie bitte diese E-Mail.') + ->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team'); + } +} diff --git a/app/Providers/ThemeServiceProvider.php b/app/Providers/ThemeServiceProvider.php index 7ec33e5..5341c30 100644 --- a/app/Providers/ThemeServiceProvider.php +++ b/app/Providers/ThemeServiceProvider.php @@ -2,7 +2,9 @@ namespace App\Providers; +use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\Vite; use Illuminate\Support\ServiceProvider; @@ -55,23 +57,55 @@ class ThemeServiceProvider extends ServiceProvider $domainConfig = array_merge($domainConfig, $confiDomains[$themeOverride]); } + // Dynamische ASSET_URL basierend auf der aktuellen Domain setzen + // Verhindert CORS-Probleme, da Assets immer von derselben Domain geladen werden + $assetUrl = $domainConfig['url']; + // Grundlegende Konfiguration im Anwendungskontext verfügbar machen config([ 'app.theme' => $domainConfig['theme'], 'app.view_prefix' => $domainConfig['view_prefix'], 'app.domain_name' => $domainConfig['domain_name'], 'app.url' => $domainConfig['url'], + 'app.asset_url' => $assetUrl, // Dynamische Asset-URL für die aktuelle Domain ]); + // URL-Generator für die aktuelle Domain konfigurieren + // Dies ist wichtig, damit asset() und url() die richtige Domain verwenden + URL::forceRootUrl($domainConfig['url']); + URL::forceScheme(parse_url($domainConfig['url'], PHP_URL_SCHEME) ?: 'https'); + + // WICHTIG: Asset-Root direkt im UrlGenerator setzen + // Der asset() Helper verwendet einen separaten Asset-Root + /** @var UrlGenerator $urlGenerator */ + $urlGenerator = app('url'); + $urlGenerator->useAssetOrigin($assetUrl); + // Spezifischere Daten für die Views verfügbar machen View::share('theme', $domainConfig['theme']); View::share('viewPrefix', $domainConfig['view_prefix']); View::share('domainName', $domainConfig['domain_name']); View::share('domainConfig', $domainConfig); View::share('domainUrl', $domainConfig['url']); + View::share('assetUrl', $assetUrl); + // Vite-Assets-Konfiguration für die aktuelle Domain - if (! app()->runningInConsole() && isset($domainConfig['assets_dir'])) { - Vite::useBuildDirectory($domainConfig['assets_dir']); + if (! app()->runningInConsole()) { + if (isset($domainConfig['assets_dir'])) { + Vite::useBuildDirectory($domainConfig['assets_dir']); + } + + if (app()->environment('local')) { + // Entwicklung: Vite Dev Server mit HMR + $viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.b2in.test'); + Vite::useHotFile(public_path('hot')); + config(['app.vite_dev_server_url' => $viteDevServerUrl]); + View::share('viteDevServerUrl', $viteDevServerUrl); + } else { + // Produktion: Assets von der aktuellen Domain laden (kein CORS nötig) + Vite::useScriptTagAttributes(['crossorigin' => false]); + Vite::useStyleTagAttributes(['crossorigin' => false]); + } } } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 879db7d..77e223e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,6 +11,10 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware) { + // Domain-URL Konfiguration - muss ganz am Anfang ausgeführt werden + // um sicherzustellen, dass url() und asset() die richtige Domain verwenden + $middleware->prepend(\App\Http\Middleware\SetDomainUrl::class); + // Partner Setup-Zwang für eingeloggte User $middleware->alias([ 'partner.setup' => \App\Http\Middleware\EnsurePartnerSetupCompleted::class, diff --git a/bootstrap/providers.php b/bootstrap/providers.php index d6f82d0..5a431ce 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,8 +2,8 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\ThemeServiceProvider::class, // Muss früh geladen werden für URL-Konfiguration App\Providers\FortifyServiceProvider::class, - App\Providers\ThemeServiceProvider::class, App\Providers\VoltServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class, FluxPro\FluxProServiceProvider::class, diff --git a/composer.json b/composer.json index afa3286..25e3ef2 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,9 @@ }, "extra": { "laravel": { - "dont-discover": [] + "dont-discover": [ + "orchestra/workbench" + ] } }, "config": { diff --git a/composer.lock b/composer.lock index 2d3672f..fcd38df 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "bacon/bacon-qr-code", - "version": "v3.0.1", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", "shasum": "" }, "require": { @@ -27,8 +27,9 @@ }, "require-dev": { "phly/keep-a-changelog": "^2.12", - "phpunit/phpunit": "^10.5.11 || 11.0.4", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", "squizlabs/php_codesniffer": "^3.9" }, "suggest": { @@ -56,9 +57,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" }, - "time": "2024-10-01T13:55:55+00:00" + "time": "2025-11-19T17:15:36+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -212,16 +213,16 @@ }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -260,7 +261,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -268,7 +269,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -764,31 +765,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -819,7 +820,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -831,7 +832,7 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", @@ -1308,16 +1309,16 @@ }, { "name": "laravel/fortify", - "version": "v1.31.2", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "a046d52ee087ee52c9852b840cf4bbad19f10934" + "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/a046d52ee087ee52c9852b840cf4bbad19f10934", - "reference": "a046d52ee087ee52c9852b840cf4bbad19f10934", + "url": "https://api.github.com/repos/laravel/fortify/zipball/e0666dabeec0b6428678af1d51f436dcfb24e3a9", + "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9", "shasum": "" }, "require": { @@ -1325,14 +1326,12 @@ "ext-json": "*", "illuminate/support": "^10.0|^11.0|^12.0", "php": "^8.1", - "pragmarx/google2fa": "^8.0", + "pragmarx/google2fa": "^9.0", "symfony/console": "^6.0|^7.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.16|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.4|^11.3" + "orchestra/testbench": "^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1369,20 +1368,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-10-21T14:47:38+00:00" + "time": "2025-12-15T14:48:33+00:00" }, { "name": "laravel/framework", - "version": "v12.37.0", + "version": "v12.43.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125" + "reference": "195b893593a9298edee177c0844132ebaa02102f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", + "url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f", + "reference": "195b893593a9298edee177c0844132ebaa02102f", "shasum": "" }, "require": { @@ -1470,6 +1469,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -1494,13 +1494,13 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.7.0", + "orchestra/testbench-core": "^10.8.1", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1534,7 +1534,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1556,6 +1556,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -1564,7 +1565,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1588,20 +1590,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-04T15:39:33+00:00" + "time": "2025-12-16T18:53:08+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.7", + "version": "v0.3.8", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { @@ -1617,7 +1619,7 @@ "require-dev": { "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", + "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", "phpstan/phpstan-mockery": "^1.1.3" }, @@ -1645,22 +1647,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.7" + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-09-19T13:47:56+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664", + "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664", "shasum": "" }, "require": { @@ -1674,9 +1676,8 @@ }, "require-dev": { "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1711,20 +1712,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-09T19:45:24+00:00" + "time": "2025-11-21T13:59:03+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.6", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "038ce42edee619599a1debb7e81d7b3759492819" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", - "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -1733,7 +1734,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -1772,20 +1773,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-10-09T13:42:30+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.10.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c", + "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c", "shasum": "" }, "require": { @@ -1836,22 +1837,22 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.10.2" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2025-11-20T16:29:12+00:00" }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { @@ -1888,7 +1889,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -1945,7 +1946,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -2031,16 +2032,16 @@ }, { "name": "league/flysystem", - "version": "3.30.1", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", "shasum": "" }, "require": { @@ -2108,22 +2109,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2025-10-20T15:35:26+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", "shasum": "" }, "require": { @@ -2157,9 +2158,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/mime-type-detection", @@ -2219,33 +2220,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.7", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2273,6 +2279,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2285,9 +2292,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2297,7 +2306,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.7.0" }, "funding": [ { @@ -2305,26 +2314,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-12-07T16:02:06+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2332,6 +2340,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2356,7 +2365,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2381,7 +2390,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" }, "funding": [ { @@ -2389,20 +2398,20 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "livewire/flux", - "version": "v2.6.1", + "version": "v2.10.1", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "227b88db0a02db91666af2303ea6727a3af78c51" + "reference": "11f04bca8cd57e05d594a96188c26f0c118c4c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/227b88db0a02db91666af2303ea6727a3af78c51", - "reference": "227b88db0a02db91666af2303ea6727a3af78c51", + "url": "https://api.github.com/repos/livewire/flux/zipball/11f04bca8cd57e05d594a96188c26f0c118c4c4f", + "reference": "11f04bca8cd57e05d594a96188c26f0c118c4c4f", "shasum": "" }, "require": { @@ -2415,7 +2424,7 @@ "symfony/console": "^6.0|^7.0" }, "conflict": { - "livewire/blaze": "<0.1.0" + "livewire/blaze": "<1.0.0" }, "type": "library", "extra": { @@ -2453,29 +2462,33 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.6.1" + "source": "https://github.com/livewire/flux/tree/v2.10.1" }, - "time": "2025-10-28T21:12:05+00:00" + "time": "2025-12-17T23:17:22+00:00" }, { "name": "livewire/flux-pro", - "version": "2.6.1", + "version": "2.10.1", "dist": { "type": "zip", - "url": "https://composer.fluxui.dev/download/a0397651-df75-43ac-b21a-8a5ac8ad46b4/flux-pro-2.6.1.zip", - "reference": "12a6570b061c858739b40a9509424c4b4cc42b62", - "shasum": "10e8f4dad0b0232e5b47ce291ef1c55610be5298" + "url": "https://composer.fluxui.dev/download/a09e36f4-80ea-4712-8049-63bb48f28bab/flux-pro-2.10.1.zip", + "reference": "f10c97d73b952f60923b8769b333e052e4f72636", + "shasum": "9609b7fb4135979caba039ab9eb1970ca4ce27dd" }, "require": { "illuminate/console": "^10.0|^11.0|^12.0", "illuminate/support": "^10.0|^11.0|^12.0", "illuminate/view": "^10.0|^11.0|^12.0", "laravel/prompts": "^0.1.24|^0.2|^0.3", - "livewire/flux": "2.6.1|dev-main", - "livewire/livewire": "^3.6.2|^4.0", + "livewire/flux": "2.10.1|dev-main", + "livewire/livewire": "^3.7.0|^4.0", "php": "^8.1", "symfony/console": "^6.0|^7.0" }, + "require-dev": { + "livewire/volt": "*", + "orchestra/testbench": "^10.8" + }, "type": "library", "extra": { "laravel": { @@ -2495,6 +2508,18 @@ "FluxPro\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "App\\": "workbench/app/" + } + }, + "scripts": { + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@php vendor/bin/testbench workbench:build --ansi", + "@php vendor/bin/testbench serve --port 3000 --ansi" + ] + }, "license": [ "proprietary" ], @@ -2512,20 +2537,20 @@ "livewire", "ui" ], - "time": "2025-10-28T21:23:07+00:00" + "time": "2025-12-17T23:24:34+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.4", + "version": "v3.7.2", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "ef04be759da41b14d2d129e670533180a44987dc" + "reference": "b13a1e50aad156d382815c64e6c7da4a4fd08407" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", - "reference": "ef04be759da41b14d2d129e670533180a44987dc", + "url": "https://api.github.com/repos/livewire/livewire/zipball/b13a1e50aad156d382815c64e6c7da4a4fd08407", + "reference": "b13a1e50aad156d382815c64e6c7da4a4fd08407", "shasum": "" }, "require": { @@ -2580,7 +2605,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.4" + "source": "https://github.com/livewire/livewire/tree/v3.7.2" }, "funding": [ { @@ -2588,20 +2613,20 @@ "type": "github" } ], - "time": "2025-07-17T05:12:15+00:00" + "time": "2025-12-17T01:53:59+00:00" }, { "name": "livewire/volt", - "version": "v1.9.0", + "version": "v1.10.1", "source": { "type": "git", "url": "https://github.com/livewire/volt.git", - "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4" + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/4b289eef2f15398987a923d9f813cad6a6a19ea4", - "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4", + "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", "shasum": "" }, "require": { @@ -2611,9 +2636,8 @@ }, "require-dev": { "laravel/folio": "^1.1", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.15.0|^9.0|^10.0", - "pestphp/pest": "^2.9.5|^3.0", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.9.5|^3.0|^4.0", "phpstan/phpstan": "^1.10" }, "type": "library", @@ -2660,7 +2684,7 @@ "issues": "https://github.com/livewire/volt/issues", "source": "https://github.com/livewire/volt" }, - "time": "2025-10-30T02:46:00+00:00" + "time": "2025-11-25T16:19:15+00:00" }, { "name": "monolog/monolog", @@ -2767,16 +2791,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -2784,9 +2808,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2868,7 +2892,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/schema", @@ -2937,20 +2961,20 @@ }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -2973,7 +2997,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3020,22 +3044,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.0" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2025-12-01T17:49:23+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3078,37 +3102,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.3.4" + "symfony/console": "^7.3.6" }, "require-dev": { "illuminate/console": "^11.46.1", "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.4", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3151,7 +3175,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -3167,7 +3191,7 @@ "type": "github" } ], - "time": "2025-10-18T11:10:27+00:00" + "time": "2025-11-20T02:34:59+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3315,16 +3339,16 @@ }, { "name": "pragmarx/google2fa", - "version": "v8.0.3", + "version": "v9.0.0", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", "shasum": "" }, "require": { @@ -3361,9 +3385,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2025-09-19T22:51:08+00:00" }, { "name": "psr/clock", @@ -3779,16 +3803,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.14", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "95c29b3756a23855a30566b745d218bee690bef2" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", - "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -3796,8 +3820,8 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -3852,9 +3876,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-10-27T17:15:31+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -3978,20 +4002,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4050,22 +4074,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "spatie/laravel-permission", - "version": "6.23.0", + "version": "6.24.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb" + "reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb", - "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/76adb1fc8d07c16a0721c35c4cc330b7a12598d7", + "reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7", "shasum": "" }, "require": { @@ -4127,7 +4151,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.23.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.24.0" }, "funding": [ { @@ -4135,26 +4159,25 @@ "type": "github" } ], - "time": "2025-11-03T20:16:13+00:00" + "time": "2025-12-13T21:45:21+00:00" }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -4193,7 +4216,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -4204,25 +4227,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:46:48+00:00" }, { "name": "symfony/console", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -4230,7 +4257,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4244,16 +4271,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4287,7 +4314,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.5" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -4307,24 +4334,24 @@ "type": "tidelift" } ], - "time": "2025-10-14T15:46:26+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b", + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -4356,7 +4383,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v8.0.0" }, "funding": [ { @@ -4367,12 +4394,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4443,32 +4474,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4500,7 +4532,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -4520,28 +4552,28 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -4550,13 +4582,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4584,7 +4617,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -4604,7 +4637,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4684,23 +4717,23 @@ }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4728,7 +4761,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -4748,27 +4781,26 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897" + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce31218c7cac92eab280762c4375fb70a6f4f897", - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4777,13 +4809,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4811,7 +4843,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" }, "funding": [ { @@ -4831,29 +4863,29 @@ "type": "tidelift" } ], - "time": "2025-10-24T21:42:11+00:00" + "time": "2025-12-07T11:13:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.5", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab" + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/24fd3f123532e26025f49f1abefcc01a69ef15ab", - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4863,6 +4895,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -4880,27 +4913,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -4929,7 +4962,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.5" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" }, "funding": [ { @@ -4949,20 +4982,20 @@ "type": "tidelift" } ], - "time": "2025-10-28T10:19:01+00:00" + "time": "2025-12-08T07:43:37+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba" + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", "shasum": "" }, "require": { @@ -4970,8 +5003,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -4982,10 +5015,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5013,7 +5046,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.5" + "source": "https://github.com/symfony/mailer/tree/v7.4.0" }, "funding": [ { @@ -5033,24 +5066,25 @@ "type": "tidelift" } ], - "time": "2025-10-24T14:27:20+00:00" + "time": "2025-11-21T15:26:00+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -5065,11 +5099,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -5101,7 +5135,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -5121,7 +5155,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5954,16 +5988,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -5995,7 +6029,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -6015,20 +6049,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/routing", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" + "reference": "4720254cb2644a0b876233d258a32bf017330db7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", + "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", + "reference": "4720254cb2644a0b876233d258a32bf017330db7", "shasum": "" }, "require": { @@ -6042,11 +6076,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6080,7 +6114,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.4" + "source": "https://github.com/symfony/routing/tree/v7.4.0" }, "funding": [ { @@ -6100,20 +6134,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -6167,7 +6201,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -6178,43 +6212,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6253,7 +6291,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -6273,38 +6311,31 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "770e3b8b0ba8360958abedcabacd4203467333ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/770e3b8b0ba8360958abedcabacd4203467333ca", + "reference": "770e3b8b0ba8360958abedcabacd4203467333ca", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -6312,17 +6343,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6353,7 +6384,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v8.0.1" }, "funding": [ { @@ -6373,20 +6404,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -6435,7 +6466,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -6446,25 +6477,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", "shasum": "" }, "require": { @@ -6472,7 +6507,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6509,7 +6544,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -6520,25 +6555,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-09-25T11:02:55+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -6550,10 +6589,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -6592,7 +6631,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -6612,27 +6651,27 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -6665,9 +6704,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "vlucas/phpdotenv", @@ -6831,24 +6870,24 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.16.0", + "version": "v3.16.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23" + "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23", - "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/730dbf8bf41f5691e026dd771e64dd54ad1b10b3", + "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3", "shasum": "" }, "require": { - "illuminate/routing": "^9|^10|^11|^12", - "illuminate/session": "^9|^10|^11|^12", - "illuminate/support": "^9|^10|^11|^12", + "illuminate/routing": "^10|^11|^12", + "illuminate/session": "^10|^11|^12", + "illuminate/support": "^10|^11|^12", "php": "^8.1", - "php-debugbar/php-debugbar": "~2.2.0", + "php-debugbar/php-debugbar": "^2.2.4", "symfony/finder": "^6|^7" }, "require-dev": { @@ -6900,7 +6939,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.0" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.2" }, "funding": [ { @@ -6912,7 +6951,7 @@ "type": "github" } ], - "time": "2025-07-14T11:56:43+00:00" + "time": "2025-12-03T14:52:46+00:00" }, { "name": "brianium/paratest", @@ -7086,16 +7125,16 @@ }, { "name": "doctrine/dbal", - "version": "4.3.4", + "version": "4.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc" + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc", - "reference": "1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", "shasum": "" }, "require": { @@ -7114,8 +7153,8 @@ "phpunit/phpunit": "11.5.23", "slevomat/coding-standard": "8.24.0", "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^6.3.8|^7.0", - "symfony/console": "^5.4|^6.3|^7.0" + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -7172,7 +7211,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.3.4" + "source": "https://github.com/doctrine/dbal/tree/4.4.1" }, "funding": [ { @@ -7188,7 +7227,7 @@ "type": "tidelift" } ], - "time": "2025-10-09T09:11:36+00:00" + "time": "2025-12-04T10:11:03+00:00" }, { "name": "doctrine/deprecations", @@ -7546,16 +7585,16 @@ }, { "name": "laravel/dusk", - "version": "v8.3.3", + "version": "v8.3.4", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "077d448cd993a08f97bfccf0ea3d6478b3908f7e" + "reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/077d448cd993a08f97bfccf0ea3d6478b3908f7e", - "reference": "077d448cd993a08f97bfccf0ea3d6478b3908f7e", + "url": "https://api.github.com/repos/laravel/dusk/zipball/33a4211c7b63ffe430bf30ec3c014012dcb6dfa6", + "reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6", "shasum": "" }, "require": { @@ -7574,7 +7613,7 @@ "require-dev": { "laravel/framework": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.6", - "orchestra/testbench-core": "^8.19|^9.0|^10.0", + "orchestra/testbench-core": "^8.19|^9.17|^10.8", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.1|^11.0|^12.0.1", "psy/psysh": "^0.11.12|^0.12", @@ -7614,22 +7653,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v8.3.3" + "source": "https://github.com/laravel/dusk/tree/v8.3.4" }, - "time": "2025-06-10T13:59:27+00:00" + "time": "2025-11-20T16:26:16+00:00" }, { "name": "laravel/pail", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30", + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30", "shasum": "" }, "require": { @@ -7646,9 +7685,9 @@ "require-dev": { "laravel/framework": "^10.24|^11.0|^12.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "orchestra/testbench-core": "^8.13|^9.17|^10.8", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, @@ -7695,20 +7734,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-06-05T13:55:57+00:00" + "time": "2025-11-20T16:29:35+00:00" }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -7719,13 +7758,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -7751,6 +7790,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -7761,20 +7801,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "laravel/sail", - "version": "v1.47.0", + "version": "v1.51.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2" + "reference": "1c74357df034e869250b4365dd445c9f6ba5d068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/9a11e822238167ad8b791e4ea51155d25cf4d8f2", - "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2", + "url": "https://api.github.com/repos/laravel/sail/zipball/1c74357df034e869250b4365dd445c9f6ba5d068", + "reference": "1c74357df034e869250b4365dd445c9f6ba5d068", "shasum": "" }, "require": { @@ -7824,7 +7864,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-10-28T13:55:29+00:00" + "time": "2025-12-09T13:33:49+00:00" }, { "name": "mockery/mockery", @@ -7971,16 +8011,16 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.8.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4", "shasum": "" }, "require": { @@ -8002,7 +8042,7 @@ "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", + "pestphp/pest": "^3.8.2 || ^4.0.0", "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", @@ -8066,43 +8106,46 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2025-11-20T02:55:25+00:00" }, { "name": "orchestra/canvas", - "version": "v10.0.2", + "version": "v10.1.1", "source": { "type": "git", "url": "https://github.com/orchestral/canvas.git", - "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb" + "reference": "6e63f56acd46b0ee842e922d0ebb18af8f7a60f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/canvas/zipball/94f732350e5c6d7136ff7b0fd05a90079dd77deb", - "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/6e63f56acd46b0ee842e922d0ebb18af8f7a60f6", + "reference": "6e63f56acd46b0ee842e922d0ebb18af8f7a60f6", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "composer/semver": "^3.0", - "illuminate/console": "^12.3.0", - "illuminate/database": "^12.3.0", - "illuminate/filesystem": "^12.3.0", - "illuminate/support": "^12.3.0", - "orchestra/canvas-core": "^10.0.1", - "orchestra/sidekick": "^1.1.0", - "orchestra/testbench-core": "^10.1.0", + "illuminate/console": "^12.40.0", + "illuminate/database": "^12.40.0", + "illuminate/filesystem": "^12.40.0", + "illuminate/support": "^12.40.0", + "orchestra/canvas-core": "^10.1.2", + "orchestra/sidekick": "^1.2.7", + "orchestra/testbench-core": "^10.8.0", "php": "^8.2", - "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php83": "^1.33", "symfony/yaml": "^7.2.0" }, + "conflict": { + "laravel/framework": "<12.40.0|>=13.0.0" + }, "require-dev": { - "laravel/framework": "^12.3.0", - "laravel/pint": "^1.21", + "laravel/framework": "^12.40.0", + "laravel/pint": "^1.24", "mockery/mockery": "^1.6.12", - "phpstan/phpstan": "^2.1.8", - "phpunit/phpunit": "^11.5.13", - "spatie/laravel-ray": "^1.40.1" + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^11.5.18|^12.0", + "spatie/laravel-ray": "^1.42.0" }, "bin": [ "canvas" @@ -8137,40 +8180,41 @@ "description": "Code Generators for Laravel Applications and Packages", "support": { "issues": "https://github.com/orchestral/canvas/issues", - "source": "https://github.com/orchestral/canvas/tree/v10.0.2" + "source": "https://github.com/orchestral/canvas/tree/v10.1.1" }, - "time": "2025-04-05T16:01:25+00:00" + "time": "2025-11-24T04:53:34+00:00" }, { "name": "orchestra/canvas-core", - "version": "v10.0.1", + "version": "v10.1.2", "source": { "type": "git", "url": "https://github.com/orchestral/canvas-core.git", - "reference": "22b6515e7a070e1c45c8a3a9819f8b6cb0234173" + "reference": "af1ac73bb0e4f5a65eeb3aadc1030983c6ea0aea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/22b6515e7a070e1c45c8a3a9819f8b6cb0234173", - "reference": "22b6515e7a070e1c45c8a3a9819f8b6cb0234173", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/af1ac73bb0e4f5a65eeb3aadc1030983c6ea0aea", + "reference": "af1ac73bb0e4f5a65eeb3aadc1030983c6ea0aea", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "composer/semver": "^3.0", - "illuminate/console": "^12.0", - "illuminate/support": "^12.0", - "orchestra/sidekick": "^1.0.2", + "illuminate/console": "^12.40.0", + "illuminate/support": "^12.40.0", + "orchestra/sidekick": "^1.2.0", "php": "^8.2", - "symfony/polyfill-php83": "^1.31" + "symfony/polyfill-php83": "^1.33" }, "require-dev": { - "laravel/framework": "^12.0", - "laravel/pint": "^1.21", + "laravel/framework": "^12.40.0", + "laravel/pint": "^1.24", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5.7", + "orchestra/testbench-core": "^10.8.0", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^11.5.12|^12.0.1", + "spatie/laravel-ray": "^1.40.2", "symfony/yaml": "^7.2" }, "type": "library", @@ -8203,26 +8247,27 @@ "description": "Code Generators Builder for Laravel Applications and Packages", "support": { "issues": "https://github.com/orchestral/canvas/issues", - "source": "https://github.com/orchestral/canvas-core/tree/v10.0.1" + "source": "https://github.com/orchestral/canvas-core/tree/v10.1.2" }, - "time": "2025-02-19T04:17:05+00:00" + "time": "2025-11-24T04:41:15+00:00" }, { "name": "orchestra/sidekick", - "version": "v1.2.17", + "version": "v1.2.18", "source": { "type": "git", "url": "https://github.com/orchestral/sidekick.git", - "reference": "371ce2882ee3f5bf826b36e75d461e51c9cd76c2" + "reference": "0e080ef62eed6c45aaea3619566a1fce02b62094" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/sidekick/zipball/371ce2882ee3f5bf826b36e75d461e51c9cd76c2", - "reference": "371ce2882ee3f5bf826b36e75d461e51c9cd76c2", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/0e080ef62eed6c45aaea3619566a1fce02b62094", + "reference": "0e080ef62eed6c45aaea3619566a1fce02b62094", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", "php": "^8.1", "symfony/polyfill-php83": "^1.32" }, @@ -8260,31 +8305,31 @@ "description": "Packages Toolkit Utilities and Helpers for Laravel", "support": { "issues": "https://github.com/orchestral/sidekick/issues", - "source": "https://github.com/orchestral/sidekick/tree/v1.2.17" + "source": "https://github.com/orchestral/sidekick/tree/v1.2.18" }, - "time": "2025-10-02T11:02:26+00:00" + "time": "2025-11-29T15:16:23+00:00" }, { "name": "orchestra/testbench", - "version": "v10.6.0", + "version": "v10.8.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench.git", - "reference": "87a7cb58edcfea9b1f26a63761c4d7ed5448f560" + "reference": "003922508c1d9f75bbe44f68364616d5ddee1939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/87a7cb58edcfea9b1f26a63761c4d7ed5448f560", - "reference": "87a7cb58edcfea9b1f26a63761c4d7ed5448f560", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/003922508c1d9f75bbe44f68364616d5ddee1939", + "reference": "003922508c1d9f75bbe44f68364616d5ddee1939", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "fakerphp/faker": "^1.23", - "laravel/framework": "^12.24.0", + "laravel/framework": "^12.40.0", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.6.1", - "orchestra/workbench": "^10.0.6", + "orchestra/testbench-core": "^10.8.0", + "orchestra/workbench": "^10.0.7", "php": "^8.2", "phpunit/phpunit": "^11.5.3|^12.0.1", "symfony/process": "^7.2", @@ -8315,47 +8360,47 @@ ], "support": { "issues": "https://github.com/orchestral/testbench/issues", - "source": "https://github.com/orchestral/testbench/tree/v10.6.0" + "source": "https://github.com/orchestral/testbench/tree/v10.8.0" }, - "time": "2025-08-20T14:38:08+00:00" + "time": "2025-11-24T09:44:51+00:00" }, { "name": "orchestra/testbench-core", - "version": "v10.7.0", + "version": "v10.8.1", "source": { "type": "git", "url": "https://github.com/orchestral/testbench-core.git", - "reference": "123ad189fcb1e49f95d87c3bc301b059e40edf05" + "reference": "f1da36cedc677d015d2a46d36abee54ffd5ba711" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/123ad189fcb1e49f95d87c3bc301b059e40edf05", - "reference": "123ad189fcb1e49f95d87c3bc301b059e40edf05", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/f1da36cedc677d015d2a46d36abee54ffd5ba711", + "reference": "f1da36cedc677d015d2a46d36abee54ffd5ba711", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "orchestra/sidekick": "~1.1.20|~1.2.17", + "orchestra/sidekick": "~1.1.21|~1.2.18", "php": "^8.2", "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-php83": "^1.32" }, "conflict": { "brianium/paratest": "<7.3.0|>=8.0.0", - "laravel/framework": "<12.28.0|>=13.0.0", + "laravel/framework": "<12.40.0|>=13.0.0", "laravel/serializable-closure": "<1.3.0|>=2.0.0 <2.0.3|>=3.0.0", "nunomaduro/collision": "<8.0.0|>=9.0.0", - "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.5.3|12.0.0|>=12.5.0" + "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.5.3|12.0.0|>=12.6.0" }, "require-dev": { "fakerphp/faker": "^1.24", - "laravel/framework": "^12.28.0", + "laravel/framework": "^12.40.0", "laravel/pint": "^1.24", "laravel/serializable-closure": "^1.3|^2.0.4", "mockery/mockery": "^1.6.10", "phpstan/phpstan": "^2.1.19", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "spatie/laravel-ray": "^1.40.2", + "spatie/laravel-ray": "^1.42.0", "symfony/process": "^7.2.0", "symfony/yaml": "^7.2.0", "vlucas/phpdotenv": "^5.6.1" @@ -8364,7 +8409,7 @@ "brianium/paratest": "Allow using parallel testing (^7.3).", "ext-pcntl": "Required to use all features of the console signal trapping.", "fakerphp/faker": "Allow using Faker for testing (^1.23).", - "laravel/framework": "Required for testing (^12.28.0).", + "laravel/framework": "Required for testing (^12.40.0).", "mockery/mockery": "Allow using Mockery for testing (^1.6).", "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.0).", "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^10.0).", @@ -8410,43 +8455,43 @@ "issues": "https://github.com/orchestral/testbench/issues", "source": "https://github.com/orchestral/testbench-core" }, - "time": "2025-10-14T12:16:46+00:00" + "time": "2025-12-08T08:07:27+00:00" }, { "name": "orchestra/workbench", - "version": "v10.0.6", + "version": "v10.0.7", "source": { "type": "git", "url": "https://github.com/orchestral/workbench.git", - "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812" + "reference": "7e41a6cbda2f553b725b4b0c104c684ae5d0f2b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/workbench/zipball/4e8a5a68200971ddb9ce4abf26488838bf5c0812", - "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/7e41a6cbda2f553b725b4b0c104c684ae5d0f2b6", + "reference": "7e41a6cbda2f553b725b4b0c104c684ae5d0f2b6", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "fakerphp/faker": "^1.23", - "laravel/framework": "^12.1.1", + "laravel/framework": "^12.40.0", "laravel/pail": "^1.2.2", "laravel/tinker": "^2.10.1", "nunomaduro/collision": "^8.6", - "orchestra/canvas": "^10.0.2", - "orchestra/sidekick": "^1.1.0", - "orchestra/testbench-core": "^10.2.1", + "orchestra/canvas": "^10.1.1", + "orchestra/sidekick": "^1.2.17", + "orchestra/testbench-core": "^10.8.0", "php": "^8.2", - "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php83": "^1.32", "symfony/process": "^7.2", "symfony/yaml": "^7.2" }, "require-dev": { - "laravel/pint": "^1.21.2", + "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", - "phpstan/phpstan": "^2.1.8", + "phpstan/phpstan": "^2.1.14", "phpunit/phpunit": "^11.5.3|^12.0.1", - "spatie/laravel-ray": "^1.40.1" + "spatie/laravel-ray": "^1.42.0" }, "suggest": { "ext-pcntl": "Required to use all features of the console signal trapping." @@ -8476,9 +8521,9 @@ ], "support": { "issues": "https://github.com/orchestral/workbench/issues", - "source": "https://github.com/orchestral/workbench/tree/v10.0.6" + "source": "https://github.com/orchestral/workbench/tree/v10.0.7" }, - "time": "2025-04-13T01:07:44+00:00" + "time": "2025-11-24T06:50:12+00:00" }, { "name": "pestphp/pest", @@ -9190,16 +9235,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -9248,22 +9293,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -9306,9 +9351,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -10953,28 +10998,28 @@ }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -11005,7 +11050,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -11025,7 +11070,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -11088,16 +11133,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -11126,7 +11171,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -11134,7 +11179,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", diff --git a/config/app.php b/config/app.php index 30d8e68..8e208bf 100644 --- a/config/app.php +++ b/config/app.php @@ -55,6 +55,19 @@ return [ 'url' => env('APP_URL', 'https://localhost'), 'domain_name' => env('APP_DOMAIN_NAME', 'localhost'), + /* + |-------------------------------------------------------------------------- + | Asset URL + |-------------------------------------------------------------------------- + | + | This URL is used when generating asset URLs. By default, this is null + | which means assets will be served from the same domain. For multi-domain + | setups, this is dynamically overwritten by the ThemeServiceProvider. + | + */ + + 'asset_url' => env('ASSET_URL'), + /* |-------------------------------------------------------------------------- | Application Timezone diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..59cb236 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,62 @@ + ['api/*', 'sanctum/csrf-cookie', '@vite/*', '@fs/*', 'build/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [ + // Dynamisch alle konfigurierten Domains erlauben + env('DOMAIN_PORTAL_URL', 'https://portal.b2in.eu'), + env('DOMAIN_B2IN_URL', 'https://b2in.eu'), + env('DOMAIN_B2A_URL', 'https://bridges2america.online'), + env('DOMAIN_STILEIGENTUM_URL', 'https://stileigentum.de'), + env('DOMAIN_STYLE2OWN_URL', 'https://style2own.de'), + env('VITE_DEV_SERVER_URL', 'https://assets.b2in.eu'), + // HTTP-Varianten für Entwicklung + 'http://portal.b2in.test', + 'http://b2in.test', + 'http://b2a.test', + 'http://stileigentum.test', + 'http://style2own.test', + 'http://assets.b2in.test', + // Localhost für lokale Entwicklung + 'http://localhost:5174', + 'http://127.0.0.1:5174', + // Live Domains + 'https://portal.b2in.eu', + 'https://api.b2in.eu', + 'https://b2in.eu', + 'https://cabinet.b2in.eu', // Digital Signage Display + 'https://bridges2america.online', + 'https://stileigentum.de', + 'https://style2own.de', + ], + + 'allowed_origins_patterns' => [ + // Erlaube alle Subdomains von .b2in.eu für Produktion + '#^https?://.*\.b2in\.eu$#', + // Erlaube alle Subdomains von .test und .local für Entwicklung + '#^https?://.*\.b2in\.test$#', + '#^https?://.*\.test$#', + '#^https?://.*\.local$#', + ], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, +]; diff --git a/config/display.php b/config/display.php new file mode 100644 index 0000000..9443e45 --- /dev/null +++ b/config/display.php @@ -0,0 +1,25 @@ + env('DISPLAY_BASE_PATH', '_cabinet'), + + // Subdomain für den Live-Server (optional) + // Wird verwendet, um die korrekte URL zu generieren + 'subdomain' => env('DISPLAY_SUBDOMAIN', null), // z.B. 'cabinet' + + // Haupt-Domain + 'domain' => env('DISPLAY_DOMAIN', 'b2in.eu'), + +]; diff --git a/config/domains.php b/config/domains.php index 395a29a..ee664ae 100644 --- a/config/domains.php +++ b/config/domains.php @@ -20,13 +20,25 @@ return [ */ 'protocol' => env('APP_PROTOCOL', 'https://'), + /* + |-------------------------------------------------------------------------- + | Domain-Namen (ohne Protokoll) + |-------------------------------------------------------------------------- + */ 'domain_portal' => env('DOMAIN_PORTAL', 'portal.b2in.test'), + 'domain_api' => env('DOMAIN_API', 'api.b2in.test'), 'domain_b2in' => env('DOMAIN_B2IN', 'b2in.test'), 'domain_b2a' => env('DOMAIN_B2A', 'b2a.test'), 'domain_stileigentum' => env('DOMAIN_STILEIGENTUM', 'stileigentum.test'), 'domain_style2own' => env('DOMAIN_STYLE2OWN', 'style2own.test'), + /* + |-------------------------------------------------------------------------- + | Vollständige Domain-URLs (mit Protokoll) + |-------------------------------------------------------------------------- + */ 'domain_portal_url' => env('DOMAIN_PORTAL_URL', 'https://portal.b2in.test'), + 'domain_api_url' => env('DOMAIN_API_URL', 'https://api.b2in.test'), 'domain_b2in_url' => env('DOMAIN_B2IN_URL', 'https://b2in.test'), 'domain_b2a_url' => env('DOMAIN_B2A_URL', 'https://b2a.test'), 'domain_stileigentum_url' => env('DOMAIN_STILEIGENTUM_URL', 'https://stileigentum.test'), diff --git a/config/livewire.php b/config/livewire.php index 8a4be98..294b7a4 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -69,22 +69,9 @@ return [ 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... - 'png', - 'gif', - 'bmp', - 'svg', - 'wav', - 'mp4', - 'mov', - 'avi', - 'wmv', - 'mp3', - 'm4a', - 'jpg', - 'jpeg', - 'mpga', - 'webp', - 'wma', + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', ], 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... @@ -158,6 +145,19 @@ return [ 'inject_morph_markers' => true, + /* + |--------------------------------------------------------------------------- + | Smart Wire Keys + |--------------------------------------------------------------------------- + | + | Livewire uses loops and keys used within loops to generate smart keys that + | are applied to nested components that don't have them. This makes using + | nested components more reliable by ensuring that they all have keys. + | + */ + + 'smart_wire_keys' => false, + /* |--------------------------------------------------------------------------- | Pagination Theme @@ -170,4 +170,17 @@ return [ */ 'pagination_theme' => 'tailwind', + + /* + |--------------------------------------------------------------------------- + | Release Token + |--------------------------------------------------------------------------- + | + | This token is stored client-side and sent along with each request to check + | a users session to see if a new release has invalidated it. If there is + | a mismatch it will throw an error and prompt for a browser refresh. + | + */ + + 'release_token' => 'a', ]; diff --git a/database/migrations/2025_11_06_153241_create_brands_table.php b/database/migrations/2025_11_06_153241_create_brands_table.php index 4820a9f..255469e 100644 --- a/database/migrations/2025_11_06_153241_create_brands_table.php +++ b/database/migrations/2025_11_06_153241_create_brands_table.php @@ -19,6 +19,7 @@ return new class extends Migration $table->string('slug')->unique(); $table->string('logo_url')->nullable(); $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); $table->timestamps(); }); } diff --git a/database/migrations/2025_12_11_000001_create_registration_codes_table.php b/database/migrations/2025_12_11_000001_create_registration_codes_table.php new file mode 100644 index 0000000..38019ce --- /dev/null +++ b/database/migrations/2025_12_11_000001_create_registration_codes_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('code')->unique(); + $table->string('role'); // broker|customer|retailer|manufacturer + $table->string('status')->default('available'); // available|used|expired + $table->foreignId('broker_partner_id')->nullable()->constrained('partners')->nullOnDelete(); + $table->foreignId('partner_id')->nullable()->constrained('partners')->nullOnDelete(); + $table->foreignId('used_by_user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->json('metadata')->nullable(); + $table->timestamps(); + + $table->index(['role', 'status']); + $table->index('broker_partner_id'); + $table->index('partner_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('registration_codes'); + } +}; diff --git a/database/migrations/2025_12_16_134959_add_name_and_assigned_to_code_id_to_registration_codes_table.php b/database/migrations/2025_12_16_134959_add_name_and_assigned_to_code_id_to_registration_codes_table.php new file mode 100644 index 0000000..8a15790 --- /dev/null +++ b/database/migrations/2025_12_16_134959_add_name_and_assigned_to_code_id_to_registration_codes_table.php @@ -0,0 +1,30 @@ +string('name')->nullable()->after('role'); + $table->foreignId('assigned_to_code_id')->nullable()->after('broker_partner_id')->constrained('registration_codes')->nullOnDelete(); + + // Indizes für bessere Performance + $table->index('name'); + $table->index('assigned_to_code_id'); + }); + } + + public function down(): void + { + Schema::table('registration_codes', function (Blueprint $table) { + $table->dropIndex(['name']); + $table->dropIndex(['assigned_to_code_id']); + $table->dropForeign(['assigned_to_code_id']); + $table->dropColumn(['name', 'assigned_to_code_id']); + }); + } +}; diff --git a/database/migrations/2025_12_16_135608_add_registration_fields_to_roles_table.php b/database/migrations/2025_12_16_135608_add_registration_fields_to_roles_table.php new file mode 100644 index 0000000..9555da6 --- /dev/null +++ b/database/migrations/2025_12_16_135608_add_registration_fields_to_roles_table.php @@ -0,0 +1,28 @@ +string('reg_prefix', 1)->nullable()->after('can_be_invited'); + $table->string('reg_description')->nullable()->after('reg_prefix'); + $table->integer('reg_start_number')->nullable()->after('reg_description'); + + // Index für Prefix-Suche + $table->index('reg_prefix'); + }); + } + + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropIndex(['reg_prefix']); + $table->dropColumn(['reg_prefix', 'reg_description', 'reg_start_number']); + }); + } +}; diff --git a/database/migrations/2025_12_17_110248_add_display_name_to_users_table.php b/database/migrations/2025_12_17_110248_add_display_name_to_users_table.php new file mode 100644 index 0000000..1fc736f --- /dev/null +++ b/database/migrations/2025_12_17_110248_add_display_name_to_users_table.php @@ -0,0 +1,31 @@ +string('display_name')->nullable()->after('name'); + $table->index('display_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex(['display_name']); + $table->dropColumn('display_name'); + }); + } +}; diff --git a/database/migrations/2025_12_17_123210_add_broker_partner_id_to_partners_table.php b/database/migrations/2025_12_17_123210_add_broker_partner_id_to_partners_table.php new file mode 100644 index 0000000..47eaa8f --- /dev/null +++ b/database/migrations/2025_12_17_123210_add_broker_partner_id_to_partners_table.php @@ -0,0 +1,41 @@ +foreignId('broker_partner_id') + ->nullable() + ->after('hub_id') + ->constrained('partners') + ->nullOnDelete(); + + $table->index('broker_partner_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('partners', function (Blueprint $table) { + $table->dropIndex(['broker_partner_id']); + $table->dropForeign(['broker_partner_id']); + $table->dropColumn('broker_partner_id'); + }); + } +}; diff --git a/database/migrations/2025_12_17_123422_rename_broker_partner_id_to_parent_partner_id_in_partners_table.php b/database/migrations/2025_12_17_123422_rename_broker_partner_id_to_parent_partner_id_in_partners_table.php new file mode 100644 index 0000000..1870a92 --- /dev/null +++ b/database/migrations/2025_12_17_123422_rename_broker_partner_id_to_parent_partner_id_in_partners_table.php @@ -0,0 +1,30 @@ +renameColumn('broker_partner_id', 'parent_partner_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('partners', function (Blueprint $table) { + $table->renameColumn('parent_partner_id', 'broker_partner_id'); + }); + } +}; diff --git a/database/migrations/2025_12_17_132845_add_brand_to_partners_table.php b/database/migrations/2025_12_17_132845_add_brand_to_partners_table.php new file mode 100644 index 0000000..97f756e --- /dev/null +++ b/database/migrations/2025_12_17_132845_add_brand_to_partners_table.php @@ -0,0 +1,33 @@ +string('brand')->nullable()->after('type'); + + $table->index('brand'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('partners', function (Blueprint $table) { + $table->dropIndex(['brand']); + $table->dropColumn('brand'); + }); + } +}; diff --git a/database/migrations/2025_12_17_135808_add_address_fields_to_partners_table.php b/database/migrations/2025_12_17_135808_add_address_fields_to_partners_table.php new file mode 100644 index 0000000..6b7837d --- /dev/null +++ b/database/migrations/2025_12_17_135808_add_address_fields_to_partners_table.php @@ -0,0 +1,53 @@ +string('salutation')->nullable()->after('brand'); // Herr, Frau, Divers + $table->string('first_name')->nullable()->after('salutation'); + $table->string('last_name')->nullable()->after('first_name'); + $table->string('street')->nullable()->after('description'); + $table->string('house_number')->nullable()->after('street'); + $table->string('zip')->nullable()->after('house_number'); + $table->string('city')->nullable()->after('zip'); + $table->string('country')->default('Deutschland')->after('city'); + $table->string('phone')->nullable()->after('country'); + $table->string('website')->nullable()->after('phone'); + + // Display Name für Broker (kann vom Firmennamen abweichen) + $table->string('display_name')->nullable()->after('company_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('partners', function (Blueprint $table) { + $table->dropColumn([ + 'salutation', + 'first_name', + 'last_name', + 'street', + 'house_number', + 'zip', + 'city', + 'country', + 'phone', + 'website', + 'display_name', + ]); + }); + } +}; diff --git a/database/migrations/2025_12_18_080601_add_soft_deletes_to_users_table.php b/database/migrations/2025_12_18_080601_add_soft_deletes_to_users_table.php new file mode 100644 index 0000000..a7bda58 --- /dev/null +++ b/database/migrations/2025_12_18_080601_add_soft_deletes_to_users_table.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2025_12_18_105009_add_partner_id_to_brands_table.php b/database/migrations/2025_12_18_105009_add_partner_id_to_brands_table.php new file mode 100644 index 0000000..8b8713d --- /dev/null +++ b/database/migrations/2025_12_18_105009_add_partner_id_to_brands_table.php @@ -0,0 +1,29 @@ +foreignId('partner_id')->after('id')->nullable()->constrained('partners')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('brands', function (Blueprint $table) { + $table->dropForeign(['partner_id']); + $table->dropColumn('partner_id'); + }); + } +}; diff --git a/database/migrations/2025_12_18_131551_create_display_videos_table.php b/database/migrations/2025_12_18_131551_create_display_videos_table.php new file mode 100644 index 0000000..2273cb8 --- /dev/null +++ b/database/migrations/2025_12_18_131551_create_display_videos_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('filename'); // Dateiname des Videos (z.B. herbst_2025.mp4) + $table->string('title')->nullable(); // Optionaler Titel für bessere Verwaltung + $table->integer('position')->default(25); // Position in % (0-100) + $table->integer('sort_order')->default(0); // Reihenfolge der Wiedergabe + $table->boolean('is_active')->default(true); // Aktiv/Inaktiv + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('display_videos'); + } +}; diff --git a/database/migrations/2025_12_18_131552_create_display_footer_contents_table.php b/database/migrations/2025_12_18_131552_create_display_footer_contents_table.php new file mode 100644 index 0000000..550c56f --- /dev/null +++ b/database/migrations/2025_12_18_131552_create_display_footer_contents_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('headline'); // Überschrift (z.B. "Beratung & Termin") + $table->string('subline'); // Unterzeile (z.B. "Jetzt Termin vereinbaren.") + $table->string('url'); // URL für den QR-Code + $table->integer('sort_order')->default(0); // Reihenfolge der Anzeige + $table->boolean('is_active')->default(true); // Aktiv/Inaktiv + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('display_footer_contents'); + } +}; diff --git a/database/migrations/2025_12_18_134623_add_short_code_and_clicks_to_display_footer_contents.php b/database/migrations/2025_12_18_134623_add_short_code_and_clicks_to_display_footer_contents.php new file mode 100644 index 0000000..1cfebec --- /dev/null +++ b/database/migrations/2025_12_18_134623_add_short_code_and_clicks_to_display_footer_contents.php @@ -0,0 +1,29 @@ +string('short_code', 10)->unique()->nullable()->after('url'); + $table->unsignedInteger('clicks')->default(0)->after('short_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('display_footer_contents', function (Blueprint $table) { + $table->dropColumn(['short_code', 'clicks']); + }); + } +}; diff --git a/database/migrations/2025_12_18_140712_make_url_nullable_in_display_footer_contents.php b/database/migrations/2025_12_18_140712_make_url_nullable_in_display_footer_contents.php new file mode 100644 index 0000000..399caf0 --- /dev/null +++ b/database/migrations/2025_12_18_140712_make_url_nullable_in_display_footer_contents.php @@ -0,0 +1,30 @@ +string('url')->nullable()->change(); + $table->string('short_code', 10)->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('display_footer_contents', function (Blueprint $table) { + $table->string('url')->nullable(false)->change(); + $table->string('short_code', 10)->nullable(false)->change(); + }); + } +}; diff --git a/database/seeders/DisplayContentSeeder.php b/database/seeders/DisplayContentSeeder.php new file mode 100644 index 0000000..796d1ab --- /dev/null +++ b/database/seeders/DisplayContentSeeder.php @@ -0,0 +1,68 @@ + 'herbst_2025.mp4', 'title' => 'Herbst 2025', 'position' => 25, 'sort_order' => 0], + ['filename' => 'fruehjahr_2025.mp4', 'title' => 'Frühjahr 2025', 'position' => 10, 'sort_order' => 1], + ['filename' => 'fruehjahr_2024.mp4', 'title' => 'Frühjahr 2024', 'position' => 25, 'sort_order' => 2], + ['filename' => 'herbst_2024.mp4', 'title' => 'Herbst 2024', 'position' => 25, 'sort_order' => 3], + ]; + + foreach ($videos as $video) { + DisplayVideo::create($video); + } + + // Footer-Inhalte aus der bestehenden Konfiguration + $footerContents = [ + [ + 'headline' => 'Beratung & Termin', + 'subline' => 'Jetzt Termin vereinbaren.', + 'url' => 'https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393', + 'sort_order' => 0, + ], + [ + 'headline' => 'Beratung vor Ort', + 'subline' => 'Einfach reinkommen.', + 'url' => 'https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393', + 'sort_order' => 1, + ], + [ + 'headline' => 'Pinterest', + 'subline' => 'Inspirationen entdecken.', + 'url' => 'https://de.pinterest.com/cabinet_AG/', + 'sort_order' => 2, + ], + [ + 'headline' => 'Instagram', + 'subline' => 'Tägliche Einblicke & Design.', + 'url' => 'https://www.instagram.com/cabinet_schranksysteme/', + 'sort_order' => 3, + ], + [ + 'headline' => 'Facebook', + 'subline' => 'News, Aktionen & Community.', + 'url' => 'https://de-de.facebook.com/cabinetschranksysteme/', + 'sort_order' => 4, + ], + ]; + + foreach ($footerContents as $content) { + DisplayFooterContent::create($content); + } + + $this->command->info('Display-Inhalte erfolgreich eingefügt!'); + } +} diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 1581007..fa6a729 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -65,7 +65,10 @@ class RoleSeeder extends Seeder 'display_name' => 'Customer (Kunde)', 'icon' => 'user', 'color' => 'indigo', - 'can_be_invited' => true + 'can_be_invited' => true, + 'reg_prefix' => 'K', + 'reg_description' => 'Kundencodes werden Maklern oder Händlern zugeordnet', + 'reg_start_number' => 10000000, ]); $customerRole->givePermissionTo([ 'view products', @@ -79,7 +82,10 @@ class RoleSeeder extends Seeder 'display_name' => 'Estate-Agent (Makler)', 'icon' => 'home', 'color' => 'lime', - 'can_be_invited' => true + 'can_be_invited' => true, + 'reg_prefix' => 'M', + 'reg_description' => 'Maklercodes für die Registrierung von Maklern', + 'reg_start_number' => 10000000, ]); $estateAgentRole->givePermissionTo([ 'access dashboard', @@ -94,7 +100,10 @@ class RoleSeeder extends Seeder 'display_name' => 'Retailer (Händler)', 'icon' => 'building-storefront', 'color' => 'teal', - 'can_be_invited' => true + 'can_be_invited' => true, + 'reg_prefix' => 'H', + 'reg_description' => 'Händlercodes für die Registrierung von Händlern', + 'reg_start_number' => 10000000, ]); $retailerRole->givePermissionTo([ 'access dashboard', @@ -113,7 +122,10 @@ class RoleSeeder extends Seeder 'display_name' => 'Manufacturer (Hersteller)', 'icon' => 'wrench-screwdriver', 'color' => 'orange', - 'can_be_invited' => true + 'can_be_invited' => true, + 'reg_prefix' => 'P', + 'reg_description' => 'Herstellercodes für die Registrierung von Herstellern', + 'reg_start_number' => 10000000, ]); $manufacturerRole->givePermissionTo([ 'access dashboard', diff --git a/COMPONENT-STRUCTURE.md b/dev/COMPONENT-STRUCTURE.md similarity index 100% rename from COMPONENT-STRUCTURE.md rename to dev/COMPONENT-STRUCTURE.md diff --git a/dev/DISPLAY_CMS_README.md b/dev/DISPLAY_CMS_README.md new file mode 100644 index 0000000..7edb1dd --- /dev/null +++ b/dev/DISPLAY_CMS_README.md @@ -0,0 +1,319 @@ +# Cabinet Display CMS - Dokumentation + +## Übersicht + +Das Display CMS ermöglicht die zentrale Verwaltung der Inhalte für die Digital Signage Display-Seite im Cabinet Showroom Bielefeld. + +**Einheitlicher Pfad:** Das System nutzt `public/_cabinet/` für Testserver und Live-Server. + +## Zugriff + +### Testserver +- **Display:** `http://portal.b2in.test/_cabinet/` +- **CMS:** `http://portal.b2in.test/admin/cms/cabinet` +- **API:** `http://portal.b2in.test/api/display/config` + +### Live-Server +- **Display:** `https://cabinet.b2in.eu/` (Subdomain zeigt auf `/_cabinet/`) +- **CMS:** `https://b2in.eu/admin/cms/cabinet` +- **API:** `https://b2in.eu/api/display/config` + +## Funktionen + +### 1. Video-Verwaltung +- ✅ Videos aus dem `public/_cabinet/assets/` Ordner verwalten +- ✅ Reihenfolge per Pfeiltasten ändern +- ✅ Video-Position (0-100%) für optimalen Bildausschnitt einstellen +- ✅ Videos aktivieren/deaktivieren +- ✅ Titel für bessere Übersicht vergeben + +### 2. Footer-Content-Verwaltung mit Tracking +- ✅ Überschrift, Unterzeile und **optional** Ziel-URL eingeben +- ✅ **Automatische Short-Link-Generierung** (nur wenn URL angegeben) +- ✅ **Echtzeit-Klick-Tracking** über Short-Links +- ✅ **Klick-Statistiken** direkt im CMS +- ✅ **Ohne URL:** Nur Text wird angezeigt, kein QR-Code +- ✅ **Mit URL:** QR-Code mit Short-Link wird automatisch generiert +- ✅ Reihenfolge ändern (werden alle 30 Sekunden rotiert) +- ✅ Inhalte aktivieren/deaktivieren +- ✅ Short-Code neu generieren +- ✅ Klick-Zähler zurücksetzen + +### 3. Short-Link-System + +#### Wie funktioniert es? + +**Mit URL (QR-Code wird angezeigt):** +1. **Beim Erstellen** mit URL wird automatisch ein 6-stelliger Short-Code generiert (z.B. `c59kjb`) +2. **Short-URL** wird automatisch erstellt: + - Testserver: `http://portal.b2in.test/_cabinet/go.php?z=c59kjb` + - Live-Server: `https://cabinet.b2in.eu/go.php?z=c59kjb` +3. **QR-Code** auf dem Display zeigt die Short-URL +4. **Bei Klick** wird der User zur Original-URL weitergeleitet +5. **Klicks werden gezählt** und im CMS angezeigt + +**Ohne URL (nur Text):** +1. **Beim Erstellen** ohne URL wird kein Short-Code generiert +2. **Nur Text** wird im Footer angezeigt (Überschrift + Unterzeile) +3. **Kein QR-Code** wird angezeigt +4. **Text-Bereich** nutzt die volle Breite des Footers + +#### Vorteile + +- ✅ Kürzere URLs für bessere QR-Codes +- ✅ Automatisches Tracking aller Scans +- ✅ Zentrale Verwaltung aller Links +- ✅ Statistiken direkt im CMS +- ✅ Links können geändert werden ohne QR-Code neu zu generieren +- ✅ Funktioniert identisch auf Test- und Live-Server + +## CMS-Interface + +### Video-Playlist + +``` +┌─────────────────────────────────────────────┐ +│ Video-Playlist [+] │ +├─────────────────────────────────────────────┤ +│ ↑↓ [Aktiv] Herbst 2025 │ +│ 📁 herbst_2025.mp4 📍 Position: 25% │ +│ [👁] [✏] [🗑] │ +├─────────────────────────────────────────────┤ +│ ↑↓ [Aktiv] Frühjahr 2025 │ +│ 📁 fruehjahr_2025.mp4 📍 Position: 10% │ +│ [👁] [✏] [🗑] │ +└─────────────────────────────────────────────┘ +``` + +### Footer-Inhalte + +``` +┌─────────────────────────────────────────────┐ +│ Footer-Inhalte [+] │ +│ 📊 Gesamt-Klicks: 47 │ +├─────────────────────────────────────────────┤ +│ ↑↓ [Aktiv] Beratung & Termin [🔵 23 Klicks]│ +│ Jetzt Termin vereinbaren. │ +│ 🔗 c59kjb [📋 Short-Link] │ +│ 🔗 https://www.cabinet.de/bielefeld... │ +│ [👁] [⋮] │ +│ ├─ Bearbeiten │ +│ ├─ Short-Code neu generieren │ +│ ├─ Klicks zurücksetzen │ +│ └─ Löschen │ +└─────────────────────────────────────────────┘ +``` + +## Technische Details + +### Ordnerstruktur + +``` +public/_cabinet/ +├── index.html # Display-Seite (lädt dynamisch via API) +├── go.php # Short-Link-Handler mit Tracking +├── assets/ # Video-Dateien +│ ├── herbst_2025.mp4 +│ ├── fruehjahr_2025.mp4 +│ └── ... +└── clicks.log # Tracking-Log (wird automatisch erstellt) +``` + +### Datenbank-Tabellen + +#### `display_videos` +```sql +- id (PK) +- filename -- Dateiname des Videos +- title -- Optionaler Titel +- position -- Vertikale Position (0-100%) +- sort_order -- Reihenfolge der Wiedergabe +- is_active -- Aktiv/Inaktiv +- created_at, updated_at +``` + +#### `display_footer_contents` +```sql +- id (PK) +- headline -- Überschrift +- subline -- Unterzeile +- url -- Original-Ziel-URL +- short_code -- 6-stelliger eindeutiger Code +- clicks -- Anzahl der Klicks +- sort_order -- Reihenfolge der Anzeige +- is_active -- Aktiv/Inaktiv +- created_at, updated_at +``` + +### Konfiguration + +**Datei:** `config/display.php` + +```php +'base_path' => env('DISPLAY_BASE_PATH', '_cabinet'), +'subdomain' => env('DISPLAY_SUBDOMAIN', null), +'domain' => env('DISPLAY_DOMAIN', 'b2in.eu'), +``` + +**Umgebungsvariablen (.env):** + +```env +# Testserver +DISPLAY_BASE_PATH=_cabinet +DISPLAY_SUBDOMAIN= +DISPLAY_DOMAIN=b2in.test + +# Live-Server +DISPLAY_BASE_PATH=_cabinet +DISPLAY_SUBDOMAIN=cabinet +DISPLAY_DOMAIN=b2in.eu +APP_ENV=production +``` + +### API-Response-Format + +**Endpunkt:** `/api/display/config` + +```json +{ + "videoPlaylist": [ + { + "src": "assets/herbst_2025.mp4", + "position": 25 + } + ], + "footerContent": [ + { + "headline": "Beratung & Termin", + "subline": "Jetzt Termin vereinbaren.", + "url": "https://cabinet.b2in.eu/go.php?z=c59kjb" + } + ] +} +``` + +## Workflow-Beispiele + +### Neuen Footer-Inhalt hinzufügen + +1. Im CMS auf **"Inhalt hinzufügen"** klicken +2. Formular ausfüllen: + - Überschrift: "Beratung & Termin" + - Unterzeile: "Jetzt Termin vereinbaren." + - URL: `https://www.cabinet.de/bielefeld?utm_source=...` +3. **Speichern** +4. ✅ Short-Code wird automatisch generiert (z.B. `c59kjb`) +5. ✅ Short-URL wird erstellt: `https://cabinet.b2in.eu/go.php?z=c59kjb` +6. ✅ Display lädt neue Konfiguration (max. 5 Minuten) +7. ✅ QR-Code zeigt automatisch den Short-Link +8. ✅ Klicks werden gezählt und angezeigt + +### Video hinzufügen + +1. Video-Datei in `public/_cabinet/assets/` hochladen +2. Im CMS auf **"Video hinzufügen"** klicken +3. Video aus Dropdown auswählen +4. Titel vergeben (optional) +5. Position einstellen (0-100%) +6. **Speichern** +7. ✅ Video wird zur Playlist hinzugefügt +8. ✅ Display spielt Video ab + +### Klick-Statistiken ansehen + +1. Im CMS unter **"Footer-Inhalte"** öffnen +2. Bei jedem Inhalt wird die Anzahl der Klicks angezeigt +3. **Gesamt-Klicks** werden oben summiert angezeigt +4. Zum Zurücksetzen: **⋮ → Klicks zurücksetzen** + +### Short-Code neu generieren + +1. Bei gewünschtem Footer-Inhalt auf **⋮** klicken +2. **"Short-Code neu generieren"** wählen +3. ✅ Neuer Code wird erstellt +4. ⚠️ Alter Code funktioniert nicht mehr +5. ✅ QR-Code aktualisiert sich automatisch beim nächsten Reload + +## Logging + +### Datenbank-Tracking (Primär) +- Alle Klicks werden in der Datenbank gezählt +- Echtzeit-Statistiken im CMS +- Auswertungen nach Inhalt möglich + +### Datei-Logging (Backup) +- Zusätzliches Log: `public/_cabinet/clicks.log` +- Format: `YYYY-MM-DD HH:MM:SS - Code: c59kjb - Headline: ... - URL: ...` +- Nützlich für detaillierte Analysen + +**Log ansehen:** +```bash +tail -f public/_cabinet/clicks.log +``` + +## Wartung & Backup + +### Wichtige Daten für Backup +- **Datenbank:** `display_videos`, `display_footer_contents` +- **Video-Dateien:** `public/_cabinet/assets/` +- **Tracking-Log:** `public/_cabinet/clicks.log` + +### Videos hinzufügen +```bash +# Upload via SCP +scp video.mp4 user@server:/var/www/html/public/_cabinet/assets/ + +# Oder via FTP/SFTP +# Dann im CMS hinzufügen +``` + +### Cache leeren +```bash +php artisan config:clear +php artisan cache:clear +php artisan route:clear +``` + +## Troubleshooting + +### Display zeigt "LADEN..." +**Problem:** API nicht erreichbar +**Lösung:** Browser-Console (F12) prüfen, API-URL testen + +### Short-Links funktionieren nicht +**Problem:** Datenbankverbindung oder Berechtigungen +**Lösung:** `go.php` Berechtigungen prüfen, Logs prüfen + +### Klicks werden nicht gezählt +**Problem:** Datenbankupdate fehlgeschlagen +**Lösung:** `clicks.log` prüfen, Datenbank-Logs prüfen + +### Videos werden nicht abgespielt +**Problem:** Falsches Format oder Pfad +**Lösung:** Browser-Console prüfen, `.mp4` mit H.264 verwenden + +## Performance-Tipps + +### Video-Optimierung +```bash +# FFmpeg für kleinere Dateien +ffmpeg -i input.mp4 -c:v libx264 -crf 23 \ + -preset medium -c:a aac -b:a 128k output.mp4 +``` + +### Auto-Reload +- Display lädt alle 5 Minuten neue Konfiguration +- Für sofortiges Update: Display-Seite neu laden + +## Support & Dokumentation + +- **Setup-Guide:** `DISPLAY_SETUP_LIVE.md` +- **ENV-Variablen:** `ENV_VARIABLES_DISPLAY.md` +- **Laravel-Logs:** `storage/logs/laravel.log` +- **Tracking-Log:** `public/_cabinet/clicks.log` + +Bei Problemen: +1. Logs prüfen +2. Browser-Console prüfen +3. API manuell testen +4. Cache leeren diff --git a/dev/DISPLAY_SETUP_LIVE.md b/dev/DISPLAY_SETUP_LIVE.md new file mode 100644 index 0000000..790c53e --- /dev/null +++ b/dev/DISPLAY_SETUP_LIVE.md @@ -0,0 +1,295 @@ +# Display Setup für Live-Server (cabinet.b2in.eu) + +## Übersicht + +Das Display-System nutzt den Ordner `public/_cabinet/` für beide Umgebungen: + +- **Testserver:** `portal.b2in.test/_cabinet/` +- **Live-Server:** `cabinet.b2in.eu/` (Subdomain zeigt direkt auf `public/_cabinet/`) + +## Setup-Schritte für Live-Server + +### 1. Dateien vorbereiten + +Die Dateien sind bereits im Ordner `public/_cabinet/` vorhanden: + +```bash +public/_cabinet/ +├── index.html # Display-Seite +├── go.php # Short-Link-Handler +├── assets/ # Video-Dateien +│ ├── herbst_2025.mp4 +│ ├── fruehjahr_2025.mp4 +│ └── ... +└── clicks.log # Tracking-Log (wird automatisch erstellt) +``` + +### 2. Subdomain konfigurieren + +Auf dem Live-Server die Subdomain `cabinet.b2in.eu` so einrichten, dass sie direkt auf `/public/_cabinet/` zeigt. + +**Apache VirtualHost Beispiel:** + +```apache + + ServerName cabinet.b2in.eu + DocumentRoot /var/www/html/public/_cabinet + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + # SSL Konfiguration + SSLEngine on + SSLCertificateFile /etc/ssl/certs/certificate.crt + SSLCertificateKeyFile /etc/ssl/private/private.key + + # Optionale Sicherheits-Header + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-Content-Type-Options "nosniff" + +``` + +**Nginx Beispiel:** + +```nginx +server { + listen 443 ssl http2; + server_name cabinet.b2in.eu; + root /var/www/html/public/_cabinet; + + index index.html index.php; + + # SSL Konfiguration + ssl_certificate /etc/ssl/certs/certificate.crt; + ssl_certificate_key /etc/ssl/private/private.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + # Logs + access_log /var/log/nginx/cabinet.b2in.eu-access.log; + error_log /var/log/nginx/cabinet.b2in.eu-error.log; +} +``` + +### 3. Umgebungsvariablen setzen + +In der `.env` Datei auf dem Live-Server folgende Variablen hinzufügen/ändern: + +```env +# Display Configuration für Live-Server +DISPLAY_BASE_PATH=_cabinet +DISPLAY_SUBDOMAIN=cabinet +DISPLAY_DOMAIN=b2in.eu +APP_ENV=production +``` + +**Wichtig:** Nach dem Ändern der `.env`: + +```bash +php artisan config:clear +php artisan cache:clear +``` + +### 4. API-Zugriff sicherstellen + +Die API kann über zwei Wege erreichbar sein: + +**Option A: API über Hauptdomain (empfohlen)** +- API-URL: `https://b2in.eu/api/display/config` +- Display-Seite lädt von Hauptdomain + +**Option B: API auch auf Subdomain** +```bash +cd /var/www/html/public/_cabinet +ln -s ../api api +``` +- API-URL: `https://cabinet.b2in.eu/api/display/config` + +### 5. Videos hochladen + +Videos in den Cabinet-Ordner kopieren: + +```bash +# Lokal zum Server +scp -r videos/*.mp4 user@liveserver:/var/www/html/public/_cabinet/assets/ + +# Im CMS hinzufügen +# Admin → CMS → Cabinet → Video hinzufügen +``` + +### 6. Testen + +1. **Display-Seite:** `https://cabinet.b2in.eu/` +2. **API-Test:** `https://b2in.eu/api/display/config` +3. **Short-Link-Test:** `https://cabinet.b2in.eu/go.php?z=c59kjb` +4. **CMS:** Admin-Login → CMS → Cabinet +5. **QR-Code scannen** und Tracking prüfen + +## Zugriffswege + +### Testserver +- Display: `http://portal.b2in.test/_cabinet/` +- Short-Link: `http://portal.b2in.test/_cabinet/go.php?z=abc123` +- API: `http://portal.b2in.test/api/display/config` + +### Live-Server +- Display: `https://cabinet.b2in.eu/` +- Short-Link: `https://cabinet.b2in.eu/go.php?z=abc123` +- API: `https://b2in.eu/api/display/config` + +## Wartung + +### Videos aktualisieren + +1. Neue Videos in `public/_cabinet/assets/` hochladen +2. Im CMS unter "Video-Playlist" hinzufügen +3. Display lädt automatisch neue Konfiguration (max. 5 Min.) + +### Footer-Inhalte ändern + +1. Im CMS unter "Footer-Inhalte" bearbeiten +2. Short-Links bleiben gleich (nur Ziel-URL ändert sich) +3. Klick-Statistiken bleiben erhalten + +### Logs prüfen + +```bash +# Klick-Tracking +tail -f /var/www/html/public/_cabinet/clicks.log + +# Laravel-Logs +tail -f /var/www/html/storage/logs/laravel.log + +# Nginx-Logs +tail -f /var/log/nginx/cabinet.b2in.eu-access.log +``` + +## Troubleshooting + +### Display zeigt "LADEN..." und lädt nicht + +**Ursache:** API nicht erreichbar + +**Lösung:** +1. Browser-Console öffnen (F12) +2. Netzwerk-Tab prüfen +3. API-URL testen: `curl https://b2in.eu/api/display/config` +4. CORS-Fehler? → API auch auf Subdomain verfügbar machen (siehe oben) + +### Short-Links funktionieren nicht + +**Ursache:** `go.php` nicht ausführbar oder Datenbankverbindung fehlerhaft + +**Lösung:** +```bash +# Dateiberechtigungen prüfen +ls -la /var/www/html/public/_cabinet/go.php + +# Manuell testen +curl -I https://cabinet.b2in.eu/go.php?z=c59kjb + +# Logs prüfen +tail -f /var/www/html/public/_cabinet/clicks.log +``` + +### Videos werden nicht abgespielt + +**Ursache:** Dateipfad falsch oder Video-Format nicht unterstützt + +**Lösung:** +1. Pfad prüfen: Videos müssen in `assets/` liegen +2. Format prüfen: `.mp4` mit H.264-Codec empfohlen +3. Browser-Console prüfen auf Fehler +4. Im CMS prüfen: Sind Videos aktiv? + +### API gibt leere Arrays zurück + +**Ursache:** Keine aktiven Inhalte in der Datenbank + +**Lösung:** +```bash +# Prüfen +php artisan tinker +>>> App\Models\DisplayVideo::count() +>>> App\Models\DisplayFooterContent::count() + +# Falls leer: Seeder ausführen +php artisan db:seed --class=DisplayContentSeeder +``` + +## Performance-Optimierung + +### Video-Komprimierung + +```bash +# FFmpeg für optimale Dateigröße +ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium \ + -c:a aac -b:a 128k output.mp4 +``` + +### Caching + +Nginx-Caching für statische Assets: + +```nginx +location ~* \.(mp4|jpg|jpeg|png|gif|ico|css|js)$ { + expires 7d; + add_header Cache-Control "public, immutable"; +} +``` + +## Sicherheit + +### SSL/TLS +- SSL-Zertifikat für `cabinet.b2in.eu` installieren +- Automatische Erneuerung einrichten (z.B. Certbot) + +### Zugriffsbeschränkung +Falls gewünscht, Display-Seite per IP einschränken: + +```nginx +location / { + allow 1.2.3.4; # Ihre IP + deny all; + try_files $uri $uri/ /index.html; +} +``` + +## Deployment-Checkliste + +- [ ] `public/_cabinet/` Ordner vorhanden +- [ ] Videos in `public/_cabinet/assets/` hochgeladen +- [ ] Subdomain `cabinet.b2in.eu` konfiguriert +- [ ] Subdomain zeigt auf `public/_cabinet/` +- [ ] `.env` aktualisiert mit korrekten Werten +- [ ] `php artisan config:clear` ausgeführt +- [ ] SSL-Zertifikat installiert +- [ ] Display-Seite getestet: `https://cabinet.b2in.eu/` +- [ ] API getestet: `https://b2in.eu/api/display/config` +- [ ] Short-Link getestet +- [ ] QR-Code gescannt und Tracking geprüft +- [ ] Logs eingerichtet und überwacht + +## Support + +Bei Problemen: + +1. **Logs prüfen:** clicks.log, laravel.log, nginx/apache logs +2. **Browser-Console:** F12 → Console & Network Tabs +3. **API manuell testen:** `curl https://b2in.eu/api/display/config` +4. **Datenbankverbindung testen:** `php artisan tinker` +5. **Cache leeren:** `php artisan config:clear && php artisan cache:clear` diff --git a/dev/PARTNER-SETUP-WIZARD.md b/dev/PARTNER-SETUP-WIZARD.md new file mode 100644 index 0000000..034bf2a --- /dev/null +++ b/dev/PARTNER-SETUP-WIZARD.md @@ -0,0 +1,90 @@ +# Partner-Einladung & Setup-Wizard + +Kurze Notizen zur aktuellen Einladungskette und dem Setup-Wizard, plus offene To-dos. + +## Ablauf Einladung +- Admin lädt Partner über `resources/views/livewire/admin/partners/invite.blade.php` ein. +- Gültigkeit wird im Formular gewählt (1–4 Wochen) und als `expires_at` in `PartnerInvitation` gespeichert. +- Einladungslink: `route('partner.invitation.accept', ['token' => $token])`. +- Zusätzliche Statusseiten: `/partner/invitation/expired/{token}` und `/partner/invitation/used/{token}`. + +## Routen +- `/partner/invitation/{token}` → Volt-Komponente `partner.invitation-accept`. +- `/partner/invitation/expired/{token}` und `/partner/invitation/used/{token}` → statische Blade-Views. +- `/partner/setup` → Volt-Komponente `partner.setup-wizard`, geschützt mit `auth`-Middleware. + +## Setup-Wizard (partner.setup-wizard) +- Basisdaten: Partner wird über eingeloggten User geladen (`Auth::user()->partner_id`); Rolle bestimmt Icon/Label. +- Schritte je Partner-Typ: + - Retailer: `Stammdaten` → `Liefergebiete` → `Fertig`. + - Manufacturer: `Stammdaten` → `Marke anlegen` → `Fertig`. + - Estate-Agent (und Default): `Profil`/`Stammdaten` → `Fertig`. +- Schritt 1 (alle): + - Felder: Firmenname (pflicht), Logo-Upload (optional, 2 MB, jpg/png/webp), Kurzbeschreibung, Straße/PLZ/Stadt (pflicht), Website (optional, URL). + - Logo wird bei Upload in `public/partner-logos` gespeichert; Partner wird mit Name/Beschreibung/Logo aktualisiert. + - TODO im Code: Adresse noch separat speichern (Adresse aktuell nicht persistiert). +- Schritt 2 Retailer: + - Felder: Lieferradius km, Montageradius km (1–500, pflicht). + - Speichert `delivery_radius_km` und `assembly_radius_km` im Partner; danach Abschluss. +- Schritt 2 Manufacturer: + - Felder: Markenname (pflicht), Markenlogo (optional, 2 MB), Markenbeschreibung. + - Speichert neue `Brand` mit Slug aus Namen; Logo in `public/brand-logos`. +- Abschluss: + - Partner wird `is_active=true`, `setup_completed=true`, `setup_completed_at=now()`. + - Dashboard-Button; für Nicht-Makler CTA „Erstes Produkt anlegen“ (derzeit ohne Aktion). + +## Offene To-dos / Beobachtungen +- Adresse persistieren (separates Modell/JSON), wird aktuell nur validiert. +- Website-Feld wird vorausgefüllt? momentan leer trotz vorhandener Partner-Daten. +- CTA „Erstes Produkt anlegen“ ohne Link/Action; Zielseite definieren. +- Hersteller: Slug-Kollision/Mehrmarken-Handling prüfen (derzeit einfacher `Str::slug` mit create). +- Estate-Agent: Gibt es spezifische Felder? Wizard zeigt nur Schritt 1 → Fertig; klären ob ausreichend. + +## QR-/Registrierungscode-Flow (neu) +- Öffentliche Landing unter `/reg/{role}` auf `b2in.test`, Rollen-Codes: + - `c` = Kunde, `e` = Makler, `m` = Hersteller, `r` = Händler. + - Gleiches Blade-Template, Inhalte/CTA dynamisch nach Role. +- QR-Codes enthalten nur den Link (`/reg/{role}`), kein Code-Query. Der Code steht z.B. auf der Visitenkarte und wird manuell eingegeben. +- Registrierungscode ist zwingend, wird einmalig eingelöst und danach als „verbraucht“ markiert. +- Validierter Code schaltet erst das passende Registrierungsformular frei (bestehender Flow wiederverwenden, kein zweites System). +- Rollen-spezifische Registrierung: + - Makler (`/reg/e`): braucht gültige Makler-Nummer (einzigartig, vorab im System hinterlegt). + - Kunde (`/reg/c`): braucht gültige Kundennummer, die einem Makler zugeordnet ist; jeder Kunde muss einem Makler zugeordnet werden (Provisions-Tracking). + - Händler (`/reg/r`) und Hersteller (`/reg/m`): eigener Flow, ebenfalls mit einmaligem Code; Hersteller haben keine Kunden, Händler pflegen Sortiment und können Kundenzugänge verschaffen. + +### Vorschlag Datenmodell Codes (anzulegen) +- Tabelle `registration_codes` (oder separate `broker_codes` + `customer_codes`): + - `code` (unique), `role` (`broker|customer|retailer|manufacturer`), `status` (`available|used|expired`), `partner_id` (optional, z.B. für Händler/Hersteller), `broker_id` (für Kundennummern-Block), `used_by_user_id` (nullable), `used_at`, `expires_at` (optional), `metadata` (json für Notizen/Quelle). + - Für Maklernummern: `role=broker`, `status=available`, keine `broker_id`. + - Für Kundennummern: `role=customer`, `broker_id` Pflicht, damit Zuordnung bei Registrierung klar ist. +- Einlösung: + - Lookup `code` + `role` passend zur Route. + - Wenn `status != available` → Fehlermeldung. + - Bei Erfolg: markiere `used`, setze `used_at`, `used_by_user_id` (nach erfolgreichem User-Create), ggf. `partner_id` koppeln. + +### Nummernformat / Vergabe +- Format: 8-stellige Nummern mit optional vorangestelltem Buchstaben (Alias). Beispiele: `00100001`, `M00100001`, `K01102513`. +- Buchstabenpräfixe nur Alias (Marketing), intern immer über IDs verknüpfen; Code bleibt als Alias gespeichert. +- Nummern sind fortlaufend; Kundennummern werden blockweise einem Makler zugewiesen (z.B. `K01102510`–`K01102560` = 50 Stück). +- Ein Code kann nur einmal eingelöst werden und wird danach als „used“ markiert. + +### Umsetzung QR-/Reg-Landing (Stand) +- Neue Route `/reg/{role}` (Volt) mit Rollen-Slugs: `c` Kunde, `e` Makler, `m` Hersteller, `r` Händler. +- Gemeinsames Template `resources/views/livewire/reg/landing.blade.php`, Inhalte dynamisch nach Rolle. +- Eingabe und Prüfung des Registrierungscodes; Normalisierung (Leerzeichen/Bindestriche entfernt, uppercase). +- Valider Code (Status `available`, Rolle passend) wird in Session abgelegt (`registration_code_id`, `registration_role`) und leitet zu `reg/create-account` weiter. Verbrauch/Markierung erfolgt beim Account-Create bzw. Wizard-Abschluss. +- Neues Modell `App\Models\RegistrationCode` + Migration `registration_codes` mit Feldern für Status, Rolle, Broker-/Partner-Referenzen, Used-Infos. + +### Account-Create nach Code (neu) +- Route `/reg/create-account` (Volt) nutzt `reg.create-account`-Komponente. +- Felder: Vorname, Nachname, E-Mail, Passwort + Bestätigung, AGB-Checkbox. +- Prüft Session-Code; mappt Rollen auf Partner-Typ (`broker→Estate-Agent`, `retailer→Retailer`, `manufacturer→Manufacturer`; Customer aktuell nicht unterstützt). +- Erstellt Partner (minimal), User, weist Rolle zu, markiert Code als verwendet, loggt ein, leitet in den Setup-Wizard. + +### Landing-/Flow-Idee +- Route/Controller/Volt für `/reg/{role}`: + - Zeigt Rolle-spezifischen Text und Eingabefeld „Registrierungscode“. + - Nach erfolgreicher Code-Validierung: Formular mit bestehenden Feldern je Rolle anzeigen (Reuse: bestehende Registrierung/Setup-Logik). +- Kopplung mit bestehendem Einladungssystem: + - Kein zweites System; nach Code-Check wird derselbe Registrierungspfad genutzt (User/Partner anlegen, Setup-Wizard etc.). +- To-do: Admin-Oberfläche bereitstellen, um Maklernummern und Kundennummern-Blöcke zu erzeugen und einem Makler zuzuweisen. diff --git a/THEME-DEMO-COMPONENTS.md b/dev/THEME-DEMO-COMPONENTS.md similarity index 100% rename from THEME-DEMO-COMPONENTS.md rename to dev/THEME-DEMO-COMPONENTS.md diff --git a/THEME-SWITCHING.md b/dev/THEME-SWITCHING.md similarity index 100% rename from THEME-SWITCHING.md rename to dev/THEME-SWITCHING.md diff --git a/dev/b2in-layout-v10/ENV_VARIABLES_DISPLAY.md b/dev/b2in-layout-v10/ENV_VARIABLES_DISPLAY.md new file mode 100644 index 0000000..ff0a461 --- /dev/null +++ b/dev/b2in-layout-v10/ENV_VARIABLES_DISPLAY.md @@ -0,0 +1,60 @@ +# Display Umgebungsvariablen + +Fügen Sie diese Variablen zu Ihrer `.env` Datei hinzu: + +## Testserver (portal.b2in) + +```env +# Display Configuration - Testserver +DISPLAY_BASE_PATH=_cabinet +DISPLAY_SUBDOMAIN= +DISPLAY_DOMAIN=b2in.test +``` + +**Resultierende URLs:** +- Display-Seite: `http://portal.b2in.test/_cabinet/` +- Short-URLs: `http://portal.b2in.test/_cabinet/go.php?z=abc123` +- API: `http://portal.b2in.test/api/display/config` + +## Live-Server (cabinet.b2in.eu) + +```env +# Display Configuration - Live-Server +DISPLAY_BASE_PATH=_cabinet +DISPLAY_SUBDOMAIN=cabinet +DISPLAY_DOMAIN=b2in.eu +APP_ENV=production +``` + +**Resultierende URLs:** +- Display-Seite: `https://cabinet.b2in.eu/` +- Short-URLs: `https://cabinet.b2in.eu/go.php?z=abc123` +- API: `https://cabinet.b2in.eu/api/display/config` oder `https://b2in.eu/api/display/config` + +## Vereinfachtes System + +Das System nutzt jetzt nur noch den Ordner `public/_cabinet/` für beide Umgebungen: + +- **Testserver:** Zugriff über `/_cabinet/` Pfad +- **Live-Server:** Subdomain zeigt direkt auf `/_cabinet/` + +Beide Umgebungen teilen sich denselben Code-Pfad, nur die URLs unterscheiden sich. + +## Wie es funktioniert + +1. **Mit DISPLAY_SUBDOMAIN gesetzt (Live):** + - Generiert absolute URLs: `https://cabinet.b2in.eu/go.php?z=...` + - Subdomain zeigt auf `public/_cabinet/` + +2. **Ohne DISPLAY_SUBDOMAIN (Test):** + - Generiert URLs mit Pfad: `http://portal.b2in.test/_cabinet/go.php?z=...` + - Zugriff über Hauptdomain mit Unterordner + +## Nach Änderungen + +Cache immer leeren: + +```bash +php artisan config:clear +php artisan cache:clear +``` diff --git a/dev/entwicklung.md b/dev/entwicklung.md new file mode 100644 index 0000000..72831d3 --- /dev/null +++ b/dev/entwicklung.md @@ -0,0 +1,676 @@ +# B2IN - Projekt-Entwicklungsstand + +## Projektübersicht + +Das B2IN-Projekt ist eine umfassende Multi-Domain-Laravel-Anwendung, die als zentrale Plattform für Partner-Management, Produkt-Verwaltung und Digital Signage dient. Die Anwendung basiert auf Laravel 12, Livewire 3 und Flux UI und bietet ein modernes, rollenbasiertes System für verschiedene Geschäftspartner. +--- + +## 🎯 Wichtige ToDos + +- **System Mails**: Kommen teilweise nicht an (google etc) Probleme mit united domains ... +In der Zukunft benötigen wir einen response bounce etc. +" host gmail-smtp-in.l.google.com[64.233.184.27] + said: 550-5.7.26 Your email has been blocked because the sender is + unauthenticated. " +--- + +## 🎯 Kern-Technologien + +- **Framework**: Laravel 12 mit PHP 8.2+ +- **Frontend**: Livewire 3 mit Volt (Single-File Components) +- **UI-Framework**: Flux UI (Pro-Version) mit Tailwind CSS +- **Authentifizierung**: Laravel Fortify mit Sanctum +- **Berechtigungen**: Spatie Laravel-Permission +- **Icons**: Heroicons (Blade-Integration) +- **Entwicklungsumgebung**: Laravel Sail (Docker) + +--- + +## ✅ Entwickelte Module & Funktionen + +### 1. Multi-Domain-System + +**Status**: ✅ Vollständig implementiert + +Das System unterstützt mehrere Domains mit individuellen Themes und Konfigurationen: + +- **Admin-Portal** (`portal.b2in.test`) - Verwaltungsbereich für alle Partner-Typen +- **Haupt-Website** (`b2in.test`) - Unternehmenswebsite +- **Landing-Pages** - Individuelle Landing-Pages mit eigenem Branding +- **Domain-spezifische Assets** - Separate CSS/JS-Builds pro Domain +- **Theme-Switching** - Dynamisches Theme-System basierend auf Domain oder GET-Parameter +- **Entwicklungsmodus** - Domain-Simulation für lokale Entwicklung ohne Hosts-Änderung + +**Technische Details**: +- Domain-Erkennung über `ThemeServiceProvider` +- Separate Vite-Konfigurationen für Admin und Web +- Dynamische Favicon-Generierung +- Theme-basiertes Routing + +--- + +### 2. Benutzer- & Rollen-Management + +**Status**: ✅ Vollständig implementiert + +Umfassendes Rollen- und Berechtigungssystem mit folgenden Rollen: + +#### Definierte Rollen: +- **Super Admin** - Vollzugriff auf alle Funktionen +- **Admin** - Verwaltung von Partnern und System +- **Retailer (Händler)** - Produktverwaltung, Kundenbetreuung, Liefergebiete +- **Manufacturer (Hersteller)** - Marken- und Produktverwaltung +- **Broker (Makler)** - Lead-Generierung, Provisionen, Kundenanbindung +- **Customer (Endkunde)** - Produktbestellung, Dashboard-Zugriff + +#### Berechtigungen: +- Hub-Management (Anzeigen, Erstellen, Bearbeiten, Löschen) +- Partner-Management (Anzeigen, Erstellen, Bearbeiten, Löschen, Provisionen) +- Produkt-Management (Anzeigen, Erstellen, Bearbeiten, Löschen, Miet-Optionen) +- Bestellungs-Management (Anzeigen, Verwalten) +- User-Management (Anzeigen, Verwalten, Rollen zuweisen) +- Dashboard-Zugriff +- Bestellungen aufgeben + +#### Spezielle Features: +- **Impersonation** - Admins können sich als andere Benutzer einloggen +- **Soft-Delete** - Benutzer werden anonymisiert statt gelöscht (bei Abhängigkeiten) +- **Rollenbasierte Navigation** - Dynamische Sidebar basierend auf Berechtigungen +- **Display-Namen** - Separate Anzeigenamen für Benutzer + +--- + +### 3. Partner-Management-System + +**Status**: ✅ Vollständig implementiert + +Komplettes System zur Verwaltung von Geschäftspartnern: + +#### Partner-Typen: +- **Retailer** - Händler mit Liefer- und Montageradius +- **Manufacturer** - Hersteller mit Markenverwaltung +- **Broker** - Makler mit Provisionsmodell +- **Customer** - Endkunden + +#### Funktionen: +- **Partner-Einladungssystem**: + - E-Mail-Einladungen mit Token-basiertem Zugang + - Konfigurierbare Gültigkeit (1-4 Wochen) + - Status-Tracking (Offen, Verwendet, Abgelaufen) + - Rollenbasierte Einladungen + - Spezielle Status-Seiten für abgelaufene/verwendete Einladungen + +- **Partner-Setup-Wizard**: + - Mehrstufiger Onboarding-Prozess + - Rollenspezifische Schritte und Felder + - Logo-Upload (Partner & Marke) + - Adressverwaltung (Straße, PLZ, Stadt, Land) + - Retailer: Liefer- und Montageradius (1-500 km) + - Manufacturer: Markenerstellung mit Logo + - Setup-Status-Tracking (`setup_completed`, `setup_completed_at`) + - Middleware-Schutz für unvollständige Setups + +- **Partner-Hierarchie **: + - Parent-Child-Beziehungen (Broker → Kunden) + - Hub-Zuordnung für regionale Partner 🔄 Grundarchitektur vorhanden + - Provisionsmodelle (Festbetrag oder Prozentsatz) 🔄 Grundarchitektur vorhanden + +- **Registrierungs-Codes**: + - QR-Code-basierte Registrierung + - Rollenspezifische Landing-Pages (`/reg/{role}`) + - Code-Tracking (Verwendet von, Verwendungsdatum) + - Code-Namen für bessere Organisation + - Zuordnung zu übergeordnetem Partner + +--- + +### 4. Hub-Management + +**Status**: 🔄 Grundarchitektur vorhanden + +Regionale Hub-Verwaltung: + +#### Funktionen: +- Hub-Erstellung und -Verwaltung +- Standortverwaltung (HubLocations) +- Partner-Zuordnung zu Hubs +- Hub-spezifische Dashboard-Ansichten +- Indexseite mit Hub-Übersicht +- Erstellen/Bearbeiten von Hubs + +#### Datenstruktur: +- Hub-Stammdaten +- Mehrere Standorte pro Hub +- Partner-Beziehungen + +--- + +### 5. Produkt-Management-System + +**Status**: 🔄 Grundarchitektur vorhanden + +Umfassendes Produktverwaltungssystem: + +#### Features: +- **Produktliste** (`/products`): + - Suchfunktion (Name, Artikelnummer) + - Filter nach Status (Aktiv, Entwurf, Inaktiv) + - Filter nach Kategorie + - Aktive Filter-Anzeige + - Responsive Tabellen-Ansicht + +- **Produkt-Erstellung** (`/products/create`): + - Umfangreiches Formular für Produktdaten + - Mehrstufige Eingabe + - Bild-Upload + - Varianten-Verwaltung + +#### Datenmodelle: +- **Products** - Hauptprodukte +- **ProductVariants** - Produktvarianten mit Attributen +- **Attributes** - Produkt-Attribute (z.B. Farbe, Größe) +- **AttributeValues** - Attribut-Werte +- **Categories** - Produktkategorien (mehrfach zuweisbar) +- **Tags** - Produkt-Tags +- **Brands** - Marken (Partner-zuweisbar) +- **Collections** - Produkt-Kollektionen +- **TaxRates** - Steuersätze +- **ShippingClasses** - Versandklassen +- **ProductLogistics** - Logistik-Informationen + +#### Beziehungen: +- Produkte → Kategorien (N:N) +- Produkte → Tags (N:N) +- Produkte → Verwandte Produkte (N:N) +- Produkte → Marken +- Varianten → Attribute & Werte + +--- + +### 6. Dashboard-System + +**Status**: 🔄 Grundarchitektur vorhanden + +Rollenbasiertes Dashboard mit individuellen KPIs: + +#### Admin-Dashboard: +- Aktive & geplante Hubs +- Partner-Wachstum (Gesamt, Dieser Monat) +- Plattform-Umsatz (Platzhalter) +- System-Status +- Ausstehende Einladungen +- Gesamt-Kunden + +#### Retailer-Dashboard: +- Offene Bestellungen +- Monatlicher Umsatz +- Produkt-Aufrufe +- Lagerbestands-Warnungen +- Meine Kunden + +#### Manufacturer-Dashboard: +- Marken-Reichweite +- Aktive Produkte +- Entwurfs-Produkte +- Gesamt-Aufrufe + +#### Broker-Dashboard: +- Gesamt-Provision +- Ausstehende Auszahlung +- Generierte Leads +- Empfehlungs-Link +- Broker-Kunden + +#### Customer-Dashboard: +- Marken-spezifisches Branding +- Top-Angebote +- Broker-Information +- Individuelles Design basierend auf zugewiesenem Broker + +--- + +### 7. Digital Signage / Display-CMS + +**Status**: ✅ Vollständig implementiert + +Professionelles CMS-System für Digital Signage im Cabinet Showroom Bielefeld: + +#### Video-Verwaltung: +- Verwaltung von Video-Playlists +- Reihenfolge-Verwaltung +- Video-Position einstellen (0-100% für optimalen Ausschnitt) +- Videos aktivieren/deaktivieren +- Titel für bessere Organisation + +#### Footer-Content mit Tracking: +- Überschrift und Unterzeile +- Optionale Ziel-URL +- **Automatische Short-Link-Generierung** (6-stellige Codes) +- **QR-Code-Generierung** für Short-Links +- **Echtzeit-Klick-Tracking** +- Klick-Statistiken im CMS +- Reihenfolge-Verwaltung (30-Sekunden-Rotation) +- Short-Code neu generieren +- Klick-Zähler zurücksetzen +- Flexibles Layout (mit/ohne URL) + +#### Short-Link-System: +- Automatische Code-Generierung (z.B. `c59kjb`) +- Short-URLs: `https://cabinet.b2in.eu/go.php?z=CODE` +- Tracking in Datenbank + Log-Datei +- Weiterleitung zur Original-URL +- Statistik-Dashboard + +#### Technische Details: +- API-basierte Konfiguration (`/api/display/config`) +- Standalone Display-Seite (`/_cabinet/`) +- Auto-Reload alle 5 Minuten +- Subdomain-Support (Live: `cabinet.b2in.eu`) +- Test- und Live-Umgebung +- Einheitlicher Pfad für beide Umgebungen + +--- + +### 8. Authentifizierungs-System + +**Status**: ✅ Vollständig implementiert + +Modernes Auth-System basierend auf Laravel Fortify & Sanctum: + +#### Features: +- Login/Logout +- Registrierung +- E-Mail-Verifizierung +- Passwort-Reset +- Passwort-Bestätigung (für sensible Aktionen) +- Two-Factor-Authentication (2FA) +- API-Token-Authentifizierung (Sanctum) +- "Remember Me" Funktionalität + +#### Views: +- Login-Seite (Standard & Vereinfacht) +- Registrierungs-Seite +- Passwort-vergessen +- Passwort-zurücksetzen +- E-Mail-Verifizierung + +#### Einstellungen: +- Profil-Verwaltung +- Passwort-Änderung +- Erscheinungsbild (Theme-Einstellungen) +- Benutzer löschen/anonymisieren + +--- + +### 9. Website-Frontend + +**Status**: ✅ Grundstruktur und Inhalte implementiert + +Moderne, responsive Website mit zahlreichen Sections: + +#### Implementierte Seiten: +- **Home** - Hauptseite mit Hero-Slider +- **About** - Über uns +- **Ecosystem** - Ökosystem-Darstellung +- **Partner** - Partner-Informationen +- **Portfolio** - Produkt-Portfolio +- **Magazin** - Blog/Magazin mit Detail-Ansichten +- **Contact** - Kontaktformular +- **Service** - Service-Seite +- **FAQ** - Häufig gestellte Fragen +- **Theme-Demo** - Theme-Vorschau + +#### Wiederverwendbare Sections: +- Hero (Standard, mit Bild, Slider, Tiles) +- About-Hero +- Benefits-Section +- Brand-Worlds +- Broker-Section +- Card-Section +- Commitment-Section +- Content-Section +- CTA-Section +- Dark-Stats-Section +- Digital-Core +- Ecosystem (Core, Hero, Stats) +- End-Customer-Section +- FAQ +- Final-Commitment +- Leadership-Team +- Magazin (List, Detail) +- Our-Story +- Our-Values +- Partner (Benefits, CTA, Hero, Process) +- Portfolio +- Spotlights-Section +- Supplier-Section +- Vision-Section + +#### UI-Komponenten: +- Header mit Navigation +- Footer +- Top-Bar +- Kontaktformular +- Theme-Switcher (für Demos) + +--- + +### 10. E-Mail-System + +**Status**: 🔄 Grundarchitektur vorhanden + +#### E-Mail-Templates: +- Partner-Einladung (`PartnerInvitationMail`) +- Weitere System-E-Mails über Laravel-Fortify + +--- + +### 11. Media-Verwaltung + +**Status**: 🔄 Grundarchitektur vorhanden + +- Media-Tabelle für Datei-Uploads +- Unterstützung für verschiedene Medientypen +- Partner-Logos +- Marken-Logos +- Produkt-Bilder (vorbereitet) + +--- + +### 12. Flux CMS Package (In Entwicklung) + +**Status**: 🔄 Grundarchitektur vorhanden + +Eigenes CMS-Package in `packages/flux-cms/`: + +#### Geplante Features: +- Component-First-Architecture +- Code-as-Schema (Felder in PHP definiert) +- Multi-Domain-Support +- Vollständige Mehrsprachigkeit +- Content-Versionierung +- Media-Management +- Performance-Optimierung + +#### Package-Struktur: +- `core/` - Kern-Funktionalität, Models, Services +- `components/` - Livewire Backend & Frontend Components +- `starter-components/` - Vorgefertigte Starter-Components + +--- + +## 🗄️ Datenbank-Struktur + +### Implementierte Tabellen: + +#### Benutzer & Authentifizierung: +- `users` - Benutzer-Accounts +- `roles` - Rollen (Super Admin, Admin, Retailer, etc.) +- `permissions` - Berechtigungen +- `role_has_permissions` - Rollen-Berechtigungen-Zuordnung +- `model_has_roles` - User-Rollen-Zuordnung +- `personal_access_tokens` - API-Tokens (Sanctum) + +#### Partner & Geschäftsbeziehungen: +- `partners` - Partner-Stammdaten +- `partner_invitations` - Einladungen mit Token & Status +- `registration_codes` - QR-Code-basierte Registrierung +- `hubs` - Regionale Hubs +- `hub_locations` - Hub-Standorte + +#### Produkte & Katalog: +- `products` - Produkte +- `product_variants` - Produkt-Varianten +- `product_variant_attributes` - Varianten-Attribute +- `product_logistics` - Logistik-Informationen +- `brands` - Marken +- `categories` - Kategorien +- `category_product` - Produkt-Kategorie-Zuordnung +- `collections` - Kollektionen +- `attributes` - Attribute (Farbe, Größe, etc.) +- `attribute_values` - Attribut-Werte +- `tags` - Tags +- `product_tag` - Produkt-Tag-Zuordnung +- `related_products` - Verwandte Produkte +- `tax_rates` - Steuersätze +- `shipping_classes` - Versandklassen + +#### Display/CMS: +- `display_videos` - Video-Playlist für Digital Signage +- `display_footer_contents` - Footer-Inhalte mit Short-Links & Tracking + +#### System: +- `media` - Media-Verwaltung +- `cache` - Cache-Tabellen +- `jobs` - Queue-Jobs +- `failed_jobs` - Fehlgeschlagene Jobs + +--- + +## 🎨 UI/UX Features + +### Design-System: +- **Flux UI Pro** - Professionelle Komponenten-Bibliothek +- **Tailwind CSS** - Utility-First CSS Framework +- **Heroicons** - Konsistente Icon-Bibliothek +- **Dark Mode** - Vollständige Dark-Mode-Unterstützung +- **Responsive Design** - Mobile-First-Ansatz + +### Theme-System: +- Domain-basiertes Theme-Switching +- Individuelle Farbschemata pro Domain +- Dynamische Asset-Verwaltung +- Theme-Demo-Seite für Vorschau + +### Komponenten: +- Flux Cards, Buttons, Inputs, Selects +- Data Tables mit Pagination +- Modals & Dialogs +- Toast-Notifications +- Form-Validierung +- Loading States + +--- + +## 📝 Dokumentation + +Umfangreiche Entwicklungs-Dokumentation: + +- `README.md` - Projekt-Übersicht & Installation +- `CLAUDE.md` - AI-Kontext-Dokumentation +- `dev/COMPONENT-STRUCTURE.md` - Komponenten-Architektur +- `dev/DOMAINS-CONFIG.md` - Domain-Konfiguration +- `dev/FORTIFY-SANCTUM-SETUP.md` - Auth-Setup +- `dev/HERO-ICONS-USAGE.md` - Icon-Verwendung +- `dev/LOCAL-DEVELOPMENT.md` - Lokale Entwicklung +- `dev/PARTNER-SETUP-WIZARD.md` - Partner-Onboarding +- `dev/DISPLAY_CMS_README.md` - Display-CMS-Dokumentation +- `dev/DISPLAY_SETUP_LIVE.md` - Display Live-Setup +- `dev/ENV_VARIABLES_DISPLAY.md` - Display-Umgebungsvariablen +- `dev/THEME-SWITCHING.md` - Theme-System +- `dev/THEME-DEMO-COMPONENTS.md` - Theme-Demo + +--- + +## 🔧 Entwicklungstools + +### Integrierte Tools: +- **Laravel Sail** - Docker-Entwicklungsumgebung +- **Laravel Pail** - Real-time Log-Viewer +- **Laravel Debugbar** - Debug-Informationen +- **Laravel Pint** - Code-Formatter +- **Pest PHP** - Testing Framework +- **Laravel Dusk** - Browser-Testing +- **NPM Concurrently** - Parallel-Prozess-Management + +### Development-Scripts: +```bash +composer dev # Startet Server, Queue, Logs & Vite parallel +``` + +--- + +## 🚀 Deployment-Bereitschaft + +### Produktions-Features: +- ✅ Umgebungsspezifische Konfigurationen +- ✅ Asset-Kompilierung (Vite) +- ✅ Cache-System +- ✅ Queue-System +- ✅ Log-Management +- ✅ Error-Handling +- ✅ HTTPS-Ready + +### Optimierungen: +- Autoloader-Optimierung +- Route-Caching +- Config-Caching +- View-Caching + +--- + +## 📊 Test-Abdeckung + +### Testing-Setup: +- Pest PHP Framework installiert +- TestCase-Struktur vorhanden +- Browser-Testing mit Laravel Dusk +- Feature & Unit Tests vorbereitet + +**Hinweis**: Test-Implementierung kann bei Bedarf erweitert werden. + +--- + +## 🔐 Sicherheit + +### Implementierte Sicherheitsfeatures: +- ✅ CSRF-Protection (Laravel Standard) +- ✅ SQL-Injection-Prevention (Eloquent ORM) +- ✅ XSS-Protection (Blade Escaping) +- ✅ Password-Hashing (Bcrypt) +- ✅ E-Mail-Verifizierung +- ✅ Two-Factor-Authentication +- ✅ Rate-Limiting (vorbereitet) +- ✅ Soft-Delete für DSGVO-Compliance +- ✅ Benutzer-Anonymisierung + +--- + +## 📦 Abhängigkeiten & Packages + +### Haupt-Abhängigkeiten: +- `laravel/framework` ^12.0 +- `laravel/fortify` ^1.27 +- `laravel/sanctum` ^4.1 +- `livewire/flux` ^2.1.1 +- `livewire/flux-pro` ^2.6 +- `livewire/volt` ^1.7.0 +- `spatie/laravel-permission` ^6.17 +- `blade-ui-kit/blade-heroicons` ^2.6 + +### Dev-Abhängigkeiten: +- `laravel/sail` ^1.41 +- `laravel/dusk` ^8.2 +- `pestphp/pest` ^3.8 +- `barryvdh/laravel-debugbar` ^3.16 +- `reliese/laravel` ^1.4 (Model-Generator) + +--- + +## 📈 Nächste Schritte & Empfehlungen + +### Kurzfristig (bereit für Implementierung): +1. **Bestellsystem** - Order-Management für Kunden +2. **Produktbilder** - Media-Upload für Produkte +3. **E-Mail-Templates** - Erweiterte E-Mail-Kommunikation +4. **Benachrichtigungen** - In-App-Notifications + +### Mittelfristig: +1. **Provisionsabrechnung** - Automatisierte Provisionsberechnung +2. **Reporting & Analytics** - Dashboard-Erweiterungen +3. **API-Erweiterung** - REST-API für Externe +4. **Suchfunktion** - Globale Suche über alle Inhalte + +### Langfristig: +1. **Mobile-App** - Native Apps (via Sanctum-API) +2. **Mehrsprachigkeit** - Internationalisierung +3. **Erweiterte CMS-Features** - Flux-CMS-Package fertigstellen +4. **Payment-Integration** - Zahlungsabwicklung + +--- + +## 🎯 Projektstatus Zusammenfassung + +### ✅ Vollständig implementiert: +- Multi-Domain-System mit Theme-Switching +- Komplettes Benutzer- & Rollen-Management +- Partner-Management mit Onboarding +- Hub-Verwaltung +- Produkt-Management (Grundfunktionen) +- Rollenbasierte Dashboards +- Digital Signage CMS mit Tracking +- Authentifizierung & Sicherheit +- Website-Frontend (Grundstruktur) +- Dokumentation + +### 🔄 In Arbeit: +- Flux-CMS-Package (Architektur steht) +- Erweiterte Produkt-Features +- Test-Abdeckung + +### 📋 Bereit für Umsetzung: +- Bestellsystem +- Provisionsabrechnung +- Erweiterte Reporting-Funktionen +- Payment-Integration + +--- + +## 💡 Besonderheiten & Highlights + +### Technische Exzellenz: +- **Modern Stack** - Neueste Laravel/Livewire-Versionen +- **Component-Architecture** - Wiederverwendbare Livewire-Komponenten +- **Type-Safety** - PHP 8.2+ Features +- **Performance** - Optimierte Queries, Caching +- **Skalierbarkeit** - Modularer Aufbau für Wachstum + +### Business-Features: +- **Multi-Tenant-Ready** - Verschiedene Partner-Typen +- **Hierarchische Strukturen** - Broker-Kunden-Beziehungen +- **Flexibles Provisionsmodell** - Prozentsatz oder Festbetrag +- **Umfangreiches Tracking** - Display-Klicks, User-Aktivitäten +- **White-Label-Fähigkeit** - Domain-spezifisches Branding + +### User-Experience: +- **Intuitives Onboarding** - Setup-Wizard für neue Partner +- **Rollenbasierte Ansichten** - Jede Rolle sieht nur relevante Inhalte +- **Real-time Updates** - Livewire für nahtlose Interaktion +- **Responsive Design** - Funktioniert auf allen Geräten +- **Dark Mode** - Moderne UI-Präferenzen + +--- + +## 📞 Support & Wartung + +Das System ist produktionsbereit und kann bei Bedarf mit folgenden Services erweitert werden: + +- Code-Reviews & Optimierungen +- Feature-Erweiterungen +- Bug-Fixes & Updates +- Performance-Tuning +- Sicherheits-Audits +- Schulungen für Administratoren + +--- + +**Entwicklungsstand**: Dezember 2025 +**Version**: 1.0 (Production-Ready) +**Framework**: Laravel 12 +**PHP**: 8.2+ +**Lizenz**: MIT + +--- + +*Diese Dokumentation wurde automatisch generiert und gibt den aktuellen Stand der Entwicklung wieder.* + diff --git a/packages/flux-cms/core/resources/views/admin/layouts/app.blade.php b/packages/flux-cms/core/resources/views/admin/layouts/app.blade.php index 1fe301f..117da10 100644 --- a/packages/flux-cms/core/resources/views/admin/layouts/app.blade.php +++ b/packages/flux-cms/core/resources/views/admin/layouts/app.blade.php @@ -69,7 +69,7 @@
Dashboard Einstellungen -
+ @csrf + +
+ +
+ + +
+ + + +
+ + + + + diff --git a/public/_cabinet/index_1.html b/public/_cabinet/index_1.html new file mode 100644 index 0000000..c72acfe --- /dev/null +++ b/public/_cabinet/index_1.html @@ -0,0 +1,292 @@ + + + + + + Cabinet Digital Signage Bielefeld + + + + + + + +
+ +
+ + +
+ + + +
+ + + + diff --git a/public/_cabinet/logger.php b/public/_cabinet/logger.php new file mode 100644 index 0000000..b5f3a69 --- /dev/null +++ b/public/_cabinet/logger.php @@ -0,0 +1,94 @@ + date('Y-m-d H:i:s'), + 'iso_timestamp' => $timestamp, + 'level' => $level, + 'message' => $message, + 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', + 'viewport' => $viewport, + 'connection' => $connection, + 'context' => $context + ]; + + // Log-Datei basierend auf Level + $logFile = __DIR__ . '/logs/' . strtolower($level) . '_' . date('Y-m-d') . '.log'; + $allLogsFile = __DIR__ . '/logs/all_' . date('Y-m-d') . '.log'; + + // Logs-Verzeichnis erstellen falls nicht vorhanden + $logsDir = __DIR__ . '/logs'; + if (!is_dir($logsDir)) { + mkdir($logsDir, 0755, true); + } + + // Formatierte Log-Zeile (human-readable) + $logLine = sprintf( + "[%s] [%s] %s\n", + date('Y-m-d H:i:s'), + str_pad($level, 8), + $message + ); + + // Zusätzliche Kontext-Informationen wenn vorhanden + if (!empty($context)) { + $logLine .= " Context: " . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + } + + $logLine .= " IP: {$logEntry['ip']} | Viewport: {$viewport} | Connection: {$connection}\n"; + $logLine .= str_repeat('-', 80) . "\n"; + + // JSON Log für maschinelle Verarbeitung + $jsonLogLine = json_encode($logEntry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + $jsonLogFile = __DIR__ . '/logs/json_' . date('Y-m-d') . '.log'; + + // In Dateien schreiben + file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX); + file_put_contents($allLogsFile, $logLine, FILE_APPEND | LOCK_EX); + file_put_contents($jsonLogFile, $jsonLogLine, FILE_APPEND | LOCK_EX); + + // Log-Rotation: Dateien älter als 30 Tage löschen + $files = glob($logsDir . '/*.log'); + $now = time(); + foreach ($files as $file) { + if (is_file($file) && $now - filemtime($file) >= 30 * 24 * 3600) { + unlink($file); + } + } + + // Erfolgsantwort + echo json_encode(['status' => 'success', 'message' => 'Log received']); + } else { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']); + } +} else { + http_response_code(405); + echo json_encode(['status' => 'error', 'message' => 'Method not allowed']); +} diff --git a/public/_cabinet/logs/test_2026-01-19.log b/public/_cabinet/logs/test_2026-01-19.log new file mode 100644 index 0000000..61b3c0f --- /dev/null +++ b/public/_cabinet/logs/test_2026-01-19.log @@ -0,0 +1 @@ +[2026-01-19 08:45:35] [INFO] Logging-System wurde eingerichtet diff --git a/public/_cabinet/setup-logging.sh b/public/_cabinet/setup-logging.sh new file mode 100755 index 0000000..347c787 --- /dev/null +++ b/public/_cabinet/setup-logging.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Setup-Script für Cabinet Digital Signage Logging System +# Führe dieses Script einmalig aus um das Logging zu aktivieren + +echo "================================================" +echo "Cabinet Digital Signage - Logging Setup" +echo "================================================" +echo "" + +# Farben für Output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Arbeitsverzeichnis +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +echo "📁 Arbeitsverzeichnis: $SCRIPT_DIR" +echo "" + +# 1. Logs-Verzeichnis erstellen +echo "1️⃣ Erstelle logs/ Verzeichnis..." +if [ -d "logs" ]; then + echo -e "${YELLOW} ⚠️ Verzeichnis existiert bereits${NC}" +else + mkdir -p logs + echo -e "${GREEN} ✅ logs/ erstellt${NC}" +fi + +# 2. Berechtigungen setzen +echo "" +echo "2️⃣ Setze Berechtigungen..." + +# Prüfe ob als root ausgeführt +if [ "$EUID" -ne 0 ]; then + echo -e "${YELLOW} ⚠️ Nicht als Root - verwende chmod 777 (nicht für Produktion!)${NC}" + chmod 777 logs +else + echo -e "${GREEN} ✅ Als Root - setze www-data owner${NC}" + chown -R www-data:www-data logs + chmod 755 logs +fi + +# 3. Test-Log erstellen +echo "" +echo "3️⃣ Erstelle Test-Log..." +TEST_LOG="logs/test_$(date +%Y-%m-%d).log" +echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Logging-System wurde eingerichtet" > "$TEST_LOG" + +if [ -f "$TEST_LOG" ]; then + echo -e "${GREEN} ✅ Test-Log erfolgreich erstellt: $TEST_LOG${NC}" +else + echo -e "${RED} ❌ Test-Log konnte nicht erstellt werden${NC}" + echo -e "${YELLOW} Prüfe die Schreibrechte!${NC}" +fi + +# 4. .htaccess vorbereiten +echo "" +echo "4️⃣ Prüfe .htaccess..." +if [ -f ".htaccess" ]; then + echo -e "${YELLOW} ⚠️ .htaccess existiert bereits${NC}" + echo -e " Möchtest du die Beispiel-.htaccess ansehen? → .htaccess.example" +else + echo -e "${GREEN} ℹ️ Keine .htaccess gefunden${NC}" + echo -e " Für Passwortschutz: cp .htaccess.example .htaccess" +fi + +# 5. PHP-Konfiguration prüfen +echo "" +echo "5️⃣ Prüfe PHP-Konfiguration..." + +if command -v php &> /dev/null; then + PHP_VERSION=$(php -v | head -n 1) + echo -e "${GREEN} ✅ PHP gefunden: $PHP_VERSION${NC}" + + # Prüfe wichtige PHP-Settings + FILE_UPLOADS=$(php -r "echo ini_get('file_uploads');") + MAX_POST=$(php -r "echo ini_get('post_max_size');") + + echo " 📋 file_uploads: $FILE_UPLOADS" + echo " 📋 post_max_size: $MAX_POST" +else + echo -e "${RED} ❌ PHP nicht gefunden!${NC}" +fi + +# 6. Zusammenfassung +echo "" +echo "================================================" +echo "✅ Setup abgeschlossen!" +echo "================================================" +echo "" +echo "📝 Nächste Schritte:" +echo "" +echo "1. Teste das Logging:" +echo " → Öffne: https://cabinet.b2in.eu/test-logging.html" +echo "" +echo "2. Schaue die Logs an:" +echo " → Öffne: https://cabinet.b2in.eu/view-logs.php" +echo "" +echo "3. Für Produktion (empfohlen):" +echo " → Aktiviere Passwortschutz:" +echo " cp .htaccess.example .htaccess" +echo " htpasswd -c .htpasswd admin" +echo "" +echo "4. Dokumentation lesen:" +echo " → cat LOGGING_README.md" +echo "" +echo "================================================" +echo "" + +# Verzeichnisstruktur anzeigen +echo "📂 Aktuelle Struktur:" +tree -L 2 -a . 2>/dev/null || ls -lah + +echo "" +echo "🎉 Fertig! Das Logging-System ist bereit." diff --git a/public/_cabinet/test-logging.html b/public/_cabinet/test-logging.html new file mode 100644 index 0000000..dff71e1 --- /dev/null +++ b/public/_cabinet/test-logging.html @@ -0,0 +1,330 @@ + + + + + + Cabinet Logging Test + + + +

🧪 Cabinet Logging System - Test Interface

+ +

Zweck: Diese Seite testet das Logging-System, bevor es auf den Displays eingesetzt wird.

+

Logs ansehen: Log-Viewer öffnen →

+ +
+

📡 Verbindungstest

+

Teste ob der Logger-Endpoint erreichbar ist:

+ + +
+ +
+

📝 Log-Level Tests

+

Sende verschiedene Log-Levels:

+ + + + +
+ +
+

💥 Fehler-Simulation

+

Simuliere verschiedene Fehlertypen (erscheinen automatisch in Logs):

+ + + + + +
+ +
+

🎯 Kontext-Test

+

Teste Logging mit Kontext-Informationen:

+ + +
+ +
+

🔄 Performance-Test

+

Teste mehrere Logs gleichzeitig:

+ + + +
+ +
+

📊 Console Output

+

Lokale Log-Ausgabe (wird auch gesendet):

+
+
+ + + + diff --git a/public/_cabinet/view-logs.php b/public/_cabinet/view-logs.php new file mode 100644 index 0000000..1af469e --- /dev/null +++ b/public/_cabinet/view-logs.php @@ -0,0 +1,319 @@ + + + + + + + + Cabinet Logs Viewer + + + + +
+

📊 Cabinet Digital Signage - Log Viewer

+
+ + + + + + +
+
+ + substr_count($allContent, '[FATAL]'), + 'ERROR' => substr_count($allContent, '[ERROR]'), + 'WARNING' => substr_count($allContent, '[WARNING]'), + 'INFO' => substr_count($allContent, '[INFO]') + ]; + ?> + +
+
+
+
FATAL Errors
+
+
+
+
Errors
+
+
+
+
Warnings
+
+
+
+
Info Messages
+
+
+ +
+ 0) { + $logLines = array_slice($logLines, -$lines); + } + + $currentEntry = ''; + foreach ($logLines as $line) { + // Erkennen von Log-Level + if (preg_match('/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[([A-Z]+)\s*\]\s+(.+)/', $line, $matches)) { + if ($currentEntry) { + echo "
"; + } + $timestamp = $matches[1]; + $level = trim($matches[2]); + $message = htmlspecialchars($matches[3]); + + echo "
"; + echo "{$timestamp}"; + echo "{$level}"; + echo "{$message}"; + $currentEntry = $level; + } else { + // Kontext-Zeilen + echo "
" . htmlspecialchars($line) . "
"; + } + } + if ($currentEntry) { + echo "
"; + } + ?> +
+ + +
+

Bitte wähle eine Log-Datei aus dem Dropdown-Menü.

+ +

⚠️ Keine Log-Dateien gefunden. Stelle sicher, dass das Logging aktiv ist und das Verzeichnis 'logs/' existiert.

+ +
+ + + + + + + + diff --git a/public/flux/flux.js b/public/flux/flux.js new file mode 100644 index 0000000..f59722a --- /dev/null +++ b/public/flux/flux.js @@ -0,0 +1,13171 @@ +(() => { + // node_modules/@oddbird/popover-polyfill/dist/popover.js + var ToggleEvent = class extends Event { + oldState; + newState; + constructor(type, { oldState = "", newState = "", ...init } = {}) { + super(type, init); + this.oldState = String(oldState || ""); + this.newState = String(newState || ""); + } + }; + var popoverToggleTaskQueue = /* @__PURE__ */ new WeakMap(); + function queuePopoverToggleEventTask(element2, oldState, newState) { + popoverToggleTaskQueue.set( + element2, + setTimeout(() => { + if (!popoverToggleTaskQueue.has(element2)) return; + element2.dispatchEvent( + new ToggleEvent("toggle", { + cancelable: false, + oldState, + newState + }) + ); + }, 0) + ); + } + var ShadowRoot2 = globalThis.ShadowRoot || function() { + }; + var HTMLDialogElement = globalThis.HTMLDialogElement || function() { + }; + var topLayerElements = /* @__PURE__ */ new WeakMap(); + var autoPopoverList = /* @__PURE__ */ new WeakMap(); + var visibilityState = /* @__PURE__ */ new WeakMap(); + function getPopoverVisibilityState(popover) { + return visibilityState.get(popover) || "hidden"; + } + var popoverInvoker = /* @__PURE__ */ new WeakMap(); + function popoverTargetAttributeActivationBehavior(element2) { + const popover = element2.popoverTargetElement; + if (!(popover instanceof HTMLElement)) { + return; + } + const visibility = getPopoverVisibilityState(popover); + if (element2.popoverTargetAction === "show" && visibility === "showing") { + return; + } + if (element2.popoverTargetAction === "hide" && visibility === "hidden") return; + if (visibility === "showing") { + hidePopover(popover, true, true); + } else if (checkPopoverValidity(popover, false)) { + popoverInvoker.set(popover, element2); + showPopover(popover); + } + } + function checkPopoverValidity(element2, expectedToBeShowing) { + if (element2.popover !== "auto" && element2.popover !== "manual") { + return false; + } + if (!element2.isConnected) return false; + if (expectedToBeShowing && getPopoverVisibilityState(element2) !== "showing") { + return false; + } + if (!expectedToBeShowing && getPopoverVisibilityState(element2) !== "hidden") { + return false; + } + if (element2 instanceof HTMLDialogElement && element2.hasAttribute("open")) { + return false; + } + if (document.fullscreenElement === element2) return false; + return true; + } + function getStackPosition(popover) { + if (!popover) return 0; + return Array.from(autoPopoverList.get(popover.ownerDocument) || []).indexOf( + popover + ) + 1; + } + function topMostClickedPopover(target) { + const clickedPopover = nearestInclusiveOpenPopover(target); + const invokerPopover = nearestInclusiveTargetPopoverForInvoker(target); + if (getStackPosition(clickedPopover) > getStackPosition(invokerPopover)) { + return clickedPopover; + } + return invokerPopover; + } + function topMostAutoPopover(document2) { + const documentPopovers = autoPopoverList.get(document2); + for (const popover of documentPopovers || []) { + if (!popover.isConnected) { + documentPopovers.delete(popover); + } else { + return popover; + } + } + return null; + } + function getRootNode(node) { + if (typeof node.getRootNode === "function") { + return node.getRootNode(); + } + if (node.parentNode) return getRootNode(node.parentNode); + return node; + } + function nearestInclusiveOpenPopover(node) { + while (node) { + if (node instanceof HTMLElement && node.popover === "auto" && visibilityState.get(node) === "showing") { + return node; + } + node = node instanceof Element && node.assignedSlot || node.parentElement || getRootNode(node); + if (node instanceof ShadowRoot2) node = node.host; + if (node instanceof Document) return; + } + } + function nearestInclusiveTargetPopoverForInvoker(node) { + while (node) { + const nodePopover = node.popoverTargetElement; + if (nodePopover instanceof HTMLElement) return nodePopover; + node = node.parentElement || getRootNode(node); + if (node instanceof ShadowRoot2) node = node.host; + if (node instanceof Document) return; + } + } + function topMostPopoverAncestor(newPopover) { + const popoverPositions = /* @__PURE__ */ new Map(); + let i = 0; + for (const popover of autoPopoverList.get(newPopover.ownerDocument) || []) { + popoverPositions.set(popover, i); + i += 1; + } + popoverPositions.set(newPopover, i); + i += 1; + let topMostPopoverAncestor22 = null; + function checkAncestor(candidate) { + const candidateAncestor = nearestInclusiveOpenPopover(candidate); + if (candidateAncestor === null) return null; + const candidatePosition = popoverPositions.get(candidateAncestor); + if (topMostPopoverAncestor22 === null || popoverPositions.get(topMostPopoverAncestor22) < candidatePosition) { + topMostPopoverAncestor22 = candidateAncestor; + } + } + checkAncestor(newPopover.parentElement || getRootNode(newPopover)); + return topMostPopoverAncestor22; + } + function isFocusable(focusTarget) { + if (focusTarget.hidden || focusTarget instanceof ShadowRoot2) return false; + if (focusTarget instanceof HTMLButtonElement || focusTarget instanceof HTMLInputElement || focusTarget instanceof HTMLSelectElement || focusTarget instanceof HTMLTextAreaElement || focusTarget instanceof HTMLOptGroupElement || focusTarget instanceof HTMLOptionElement || focusTarget instanceof HTMLFieldSetElement) { + if (focusTarget.disabled) return false; + } + if (focusTarget instanceof HTMLInputElement && focusTarget.type === "hidden") { + return false; + } + if (focusTarget instanceof HTMLAnchorElement && focusTarget.href === "") { + return false; + } + return typeof focusTarget.tabIndex === "number" && focusTarget.tabIndex !== -1; + } + function focusDelegate(focusTarget) { + if (focusTarget.shadowRoot && focusTarget.shadowRoot.delegatesFocus !== true) { + return null; + } + let whereToLook = focusTarget; + if (whereToLook.shadowRoot) { + whereToLook = whereToLook.shadowRoot; + } + let autoFocusDelegate = whereToLook.querySelector("[autofocus]"); + if (autoFocusDelegate) { + return autoFocusDelegate; + } else { + const slots = whereToLook.querySelectorAll("slot"); + for (const slot of slots) { + const assignedElements = slot.assignedElements({ flatten: true }); + for (const el of assignedElements) { + if (el.hasAttribute("autofocus")) { + return el; + } else { + autoFocusDelegate = el.querySelector("[autofocus]"); + if (autoFocusDelegate) { + return autoFocusDelegate; + } + } + } + } + } + const walker2 = focusTarget.ownerDocument.createTreeWalker( + whereToLook, + NodeFilter.SHOW_ELEMENT + ); + let descendant = walker2.currentNode; + while (descendant) { + if (isFocusable(descendant)) { + return descendant; + } + descendant = walker2.nextNode(); + } + } + function popoverFocusingSteps(subject) { + focusDelegate(subject)?.focus(); + } + var previouslyFocusedElements = /* @__PURE__ */ new WeakMap(); + function showPopover(element2) { + if (!checkPopoverValidity(element2, false)) { + return; + } + const document2 = element2.ownerDocument; + if (!element2.dispatchEvent( + new ToggleEvent("beforetoggle", { + cancelable: true, + oldState: "closed", + newState: "open" + }) + )) { + return; + } + if (!checkPopoverValidity(element2, false)) { + return; + } + let shouldRestoreFocus = false; + if (element2.popover === "auto") { + const originalType = element2.getAttribute("popover"); + const ancestor = topMostPopoverAncestor(element2) || document2; + hideAllPopoversUntil(ancestor, false, true); + if (originalType !== element2.getAttribute("popover") || !checkPopoverValidity(element2, false)) { + return; + } + } + if (!topMostAutoPopover(document2)) { + shouldRestoreFocus = true; + } + previouslyFocusedElements.delete(element2); + const originallyFocusedElement = document2.activeElement; + element2.classList.add(":popover-open"); + visibilityState.set(element2, "showing"); + if (!topLayerElements.has(document2)) { + topLayerElements.set(document2, /* @__PURE__ */ new Set()); + } + topLayerElements.get(document2).add(element2); + popoverFocusingSteps(element2); + if (element2.popover === "auto") { + if (!autoPopoverList.has(document2)) { + autoPopoverList.set(document2, /* @__PURE__ */ new Set()); + } + autoPopoverList.get(document2).add(element2); + setInvokerAriaExpanded(popoverInvoker.get(element2), true); + } + if (shouldRestoreFocus && originallyFocusedElement && element2.popover === "auto") { + previouslyFocusedElements.set(element2, originallyFocusedElement); + } + queuePopoverToggleEventTask(element2, "closed", "open"); + } + function hidePopover(element2, focusPreviousElement = false, fireEvents = false) { + if (!checkPopoverValidity(element2, true)) { + return; + } + const document2 = element2.ownerDocument; + if (element2.popover === "auto") { + hideAllPopoversUntil(element2, focusPreviousElement, fireEvents); + if (!checkPopoverValidity(element2, true)) { + return; + } + } + setInvokerAriaExpanded(popoverInvoker.get(element2), false); + popoverInvoker.delete(element2); + if (fireEvents) { + element2.dispatchEvent( + new ToggleEvent("beforetoggle", { + oldState: "open", + newState: "closed" + }) + ); + if (!checkPopoverValidity(element2, true)) { + return; + } + } + topLayerElements.get(document2)?.delete(element2); + autoPopoverList.get(document2)?.delete(element2); + element2.classList.remove(":popover-open"); + visibilityState.set(element2, "hidden"); + if (fireEvents) { + queuePopoverToggleEventTask(element2, "open", "closed"); + } + const previouslyFocusedElement = previouslyFocusedElements.get(element2); + if (previouslyFocusedElement) { + previouslyFocusedElements.delete(element2); + if (focusPreviousElement) { + previouslyFocusedElement.focus(); + } + } + } + function closeAllOpenPopovers(document2, focusPreviousElement = false, fireEvents = false) { + let popover = topMostAutoPopover(document2); + while (popover) { + hidePopover(popover, focusPreviousElement, fireEvents); + popover = topMostAutoPopover(document2); + } + } + function hideAllPopoversUntil(endpoint, focusPreviousElement, fireEvents) { + const document2 = endpoint.ownerDocument || endpoint; + if (endpoint instanceof Document) { + return closeAllOpenPopovers(document2, focusPreviousElement, fireEvents); + } + let lastToHide = null; + let foundEndpoint = false; + for (const popover of autoPopoverList.get(document2) || []) { + if (popover === endpoint) { + foundEndpoint = true; + } else if (foundEndpoint) { + lastToHide = popover; + break; + } + } + if (!foundEndpoint) { + return closeAllOpenPopovers(document2, focusPreviousElement, fireEvents); + } + while (lastToHide && getPopoverVisibilityState(lastToHide) === "showing" && autoPopoverList.get(document2)?.size) { + hidePopover(lastToHide, focusPreviousElement, fireEvents); + } + } + var popoverPointerDownTargets = /* @__PURE__ */ new WeakMap(); + function lightDismissOpenPopovers(event) { + if (!event.isTrusted) return; + const target = event.composedPath()[0]; + if (!target) return; + const document2 = target.ownerDocument; + const topMostPopover = topMostAutoPopover(document2); + if (!topMostPopover) return; + const ancestor = topMostClickedPopover(target); + if (ancestor && event.type === "pointerdown") { + popoverPointerDownTargets.set(document2, ancestor); + } else if (event.type === "pointerup") { + const sameTarget = popoverPointerDownTargets.get(document2) === ancestor; + popoverPointerDownTargets.delete(document2); + if (sameTarget) { + hideAllPopoversUntil(ancestor || document2, false, true); + } + } + } + var initialAriaExpandedValue = /* @__PURE__ */ new WeakMap(); + function setInvokerAriaExpanded(el, force = false) { + if (!el) return; + if (!initialAriaExpandedValue.has(el)) { + initialAriaExpandedValue.set(el, el.getAttribute("aria-expanded")); + } + const popover = el.popoverTargetElement; + if (popover instanceof HTMLElement && popover.popover === "auto") { + el.setAttribute("aria-expanded", String(force)); + } else { + const initialValue = initialAriaExpandedValue.get(el); + if (!initialValue) { + el.removeAttribute("aria-expanded"); + } else { + el.setAttribute("aria-expanded", initialValue); + } + } + } + var ShadowRoot22 = globalThis.ShadowRoot || function() { + }; + function isSupported() { + return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype === "object" && "popover" in HTMLElement.prototype; + } + function patchSelectorFn(object, name, mapper) { + const original = object[name]; + Object.defineProperty(object, name, { + value(selector) { + return original.call(this, mapper(selector)); + } + }); + } + var nonEscapedPopoverSelector = /(^|[^\\]):popover-open\b/g; + function hasLayerSupport() { + return typeof globalThis.CSSLayerBlockRule === "function"; + } + function getStyles() { + const useLayer = hasLayerSupport(); + return ` +${useLayer ? "@layer popover-polyfill {" : ""} + :where([popover]) { + position: fixed; + z-index: 2147483647; + inset: 0; + padding: 0.25em; + width: fit-content; + height: fit-content; + border-width: initial; + border-color: initial; + border-image: initial; + border-style: solid; + background-color: canvas; + color: canvastext; + overflow: auto; + margin: auto; + } + + :where([popover]:not(.\\:popover-open)) { + display: none; + } + + :where(dialog[popover].\\:popover-open) { + display: block; + } + + :where(dialog[popover][open]) { + display: revert; + } + + :where([anchor].\\:popover-open) { + inset: auto; + } + + :where([anchor]:popover-open) { + inset: auto; + } + + @supports not (background-color: canvas) { + :where([popover]) { + background-color: white; + color: black; + } + } + + @supports (width: -moz-fit-content) { + :where([popover]) { + width: -moz-fit-content; + height: -moz-fit-content; + } + } + + @supports not (inset: 0) { + :where([popover]) { + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } +${useLayer ? "}" : ""} +`; + } + var popoverStyleSheet = null; + function injectStyles(root) { + const styles = getStyles(); + if (popoverStyleSheet === null) { + try { + popoverStyleSheet = new CSSStyleSheet(); + popoverStyleSheet.replaceSync(styles); + } catch { + popoverStyleSheet = false; + } + } + if (popoverStyleSheet === false) { + const sheet = document.createElement("style"); + sheet.textContent = styles; + if (root instanceof Document) { + root.head.prepend(sheet); + } else { + root.prepend(sheet); + } + } else { + root.adoptedStyleSheets = [popoverStyleSheet, ...root.adoptedStyleSheets]; + } + } + function apply() { + if (typeof window === "undefined") return; + window.ToggleEvent = window.ToggleEvent || ToggleEvent; + function rewriteSelector(selector) { + if (selector?.includes(":popover-open")) { + selector = selector.replace( + nonEscapedPopoverSelector, + "$1.\\:popover-open" + ); + } + return selector; + } + patchSelectorFn(Document.prototype, "querySelector", rewriteSelector); + patchSelectorFn(Document.prototype, "querySelectorAll", rewriteSelector); + patchSelectorFn(Element.prototype, "querySelector", rewriteSelector); + patchSelectorFn(Element.prototype, "querySelectorAll", rewriteSelector); + patchSelectorFn(Element.prototype, "matches", rewriteSelector); + patchSelectorFn(Element.prototype, "closest", rewriteSelector); + patchSelectorFn( + DocumentFragment.prototype, + "querySelectorAll", + rewriteSelector + ); + Object.defineProperties(HTMLElement.prototype, { + popover: { + enumerable: true, + configurable: true, + get() { + if (!this.hasAttribute("popover")) return null; + const value3 = (this.getAttribute("popover") || "").toLowerCase(); + if (value3 === "" || value3 == "auto") return "auto"; + return "manual"; + }, + set(value3) { + if (value3 === null) { + this.removeAttribute("popover"); + } else { + this.setAttribute("popover", value3); + } + } + }, + showPopover: { + enumerable: true, + configurable: true, + value() { + showPopover(this); + } + }, + hidePopover: { + enumerable: true, + configurable: true, + value() { + hidePopover(this, true, true); + } + }, + togglePopover: { + enumerable: true, + configurable: true, + value(force) { + if (visibilityState.get(this) === "showing" && force === void 0 || force === false) { + hidePopover(this, true, true); + } else if (force === void 0 || force === true) { + showPopover(this); + } + } + } + }); + const originalAttachShadow = Element.prototype.attachShadow; + if (originalAttachShadow) { + Object.defineProperties(Element.prototype, { + attachShadow: { + enumerable: true, + configurable: true, + writable: true, + value(options) { + const shadowRoot = originalAttachShadow.call(this, options); + injectStyles(shadowRoot); + return shadowRoot; + } + } + }); + } + const originalAttachInternals = HTMLElement.prototype.attachInternals; + if (originalAttachInternals) { + Object.defineProperties(HTMLElement.prototype, { + attachInternals: { + enumerable: true, + configurable: true, + writable: true, + value() { + const internals = originalAttachInternals.call(this); + if (internals.shadowRoot) { + injectStyles(internals.shadowRoot); + } + return internals; + } + } + }); + } + const popoverTargetAssociatedElements = /* @__PURE__ */ new WeakMap(); + function applyPopoverInvokerElementMixin(ElementClass) { + Object.defineProperties(ElementClass.prototype, { + popoverTargetElement: { + enumerable: true, + configurable: true, + set(targetElement) { + if (targetElement === null) { + this.removeAttribute("popovertarget"); + popoverTargetAssociatedElements.delete(this); + } else if (!(targetElement instanceof Element)) { + throw new TypeError( + `popoverTargetElement must be an element or null` + ); + } else { + this.setAttribute("popovertarget", ""); + popoverTargetAssociatedElements.set(this, targetElement); + } + }, + get() { + if (this.localName !== "button" && this.localName !== "input") { + return null; + } + if (this.localName === "input" && this.type !== "reset" && this.type !== "image" && this.type !== "button") { + return null; + } + if (this.disabled) { + return null; + } + if (this.form && this.type === "submit") { + return null; + } + const targetElement = popoverTargetAssociatedElements.get(this); + if (targetElement && targetElement.isConnected) { + return targetElement; + } else if (targetElement && !targetElement.isConnected) { + popoverTargetAssociatedElements.delete(this); + return null; + } + const root = getRootNode(this); + const idref = this.getAttribute("popovertarget"); + if ((root instanceof Document || root instanceof ShadowRoot22) && idref) { + return root.getElementById(idref) || null; + } + return null; + } + }, + popoverTargetAction: { + enumerable: true, + configurable: true, + get() { + const value3 = (this.getAttribute("popovertargetaction") || "").toLowerCase(); + if (value3 === "show" || value3 === "hide") return value3; + return "toggle"; + }, + set(value3) { + this.setAttribute("popovertargetaction", value3); + } + } + }); + } + applyPopoverInvokerElementMixin(HTMLButtonElement); + applyPopoverInvokerElementMixin(HTMLInputElement); + const handleInvokerActivation = (event) => { + const composedPath = event.composedPath(); + const target = composedPath[0]; + if (!(target instanceof Element) || target?.shadowRoot) { + return; + } + const root = getRootNode(target); + if (!(root instanceof ShadowRoot22 || root instanceof Document)) { + return; + } + const invoker = composedPath.find( + (el) => el.matches?.("[popovertargetaction],[popovertarget]") + ); + if (invoker) { + popoverTargetAttributeActivationBehavior(invoker); + event.preventDefault(); + return; + } + }; + const onKeydown = (event) => { + const key = event.key; + const target = event.target; + if (!event.defaultPrevented && target && (key === "Escape" || key === "Esc")) { + hideAllPopoversUntil(target.ownerDocument, true, true); + } + }; + const addEventListeners = (root) => { + root.addEventListener("click", handleInvokerActivation); + root.addEventListener("keydown", onKeydown); + root.addEventListener("pointerdown", lightDismissOpenPopovers); + root.addEventListener("pointerup", lightDismissOpenPopovers); + }; + addEventListeners(document); + injectStyles(document); + } + if (!isSupported()) apply(); + + // node_modules/@oddbird/popover-polyfill/dist/popover-fn.js + var ToggleEvent2 = class extends Event { + oldState; + newState; + constructor(type, { oldState = "", newState = "", ...init } = {}) { + super(type, init); + this.oldState = String(oldState || ""); + this.newState = String(newState || ""); + } + }; + var popoverToggleTaskQueue2 = /* @__PURE__ */ new WeakMap(); + function queuePopoverToggleEventTask2(element2, oldState, newState) { + popoverToggleTaskQueue2.set( + element2, + setTimeout(() => { + if (!popoverToggleTaskQueue2.has(element2)) return; + element2.dispatchEvent( + new ToggleEvent2("toggle", { + cancelable: false, + oldState, + newState + }) + ); + }, 0) + ); + } + var ShadowRoot3 = globalThis.ShadowRoot || function() { + }; + var HTMLDialogElement2 = globalThis.HTMLDialogElement || function() { + }; + var topLayerElements2 = /* @__PURE__ */ new WeakMap(); + var autoPopoverList2 = /* @__PURE__ */ new WeakMap(); + var visibilityState2 = /* @__PURE__ */ new WeakMap(); + function getPopoverVisibilityState2(popover) { + return visibilityState2.get(popover) || "hidden"; + } + var popoverInvoker2 = /* @__PURE__ */ new WeakMap(); + function popoverTargetAttributeActivationBehavior2(element2) { + const popover = element2.popoverTargetElement; + if (!(popover instanceof HTMLElement)) { + return; + } + const visibility = getPopoverVisibilityState2(popover); + if (element2.popoverTargetAction === "show" && visibility === "showing") { + return; + } + if (element2.popoverTargetAction === "hide" && visibility === "hidden") return; + if (visibility === "showing") { + hidePopover2(popover, true, true); + } else if (checkPopoverValidity2(popover, false)) { + popoverInvoker2.set(popover, element2); + showPopover2(popover); + } + } + function checkPopoverValidity2(element2, expectedToBeShowing) { + if (element2.popover !== "auto" && element2.popover !== "manual") { + return false; + } + if (!element2.isConnected) return false; + if (expectedToBeShowing && getPopoverVisibilityState2(element2) !== "showing") { + return false; + } + if (!expectedToBeShowing && getPopoverVisibilityState2(element2) !== "hidden") { + return false; + } + if (element2 instanceof HTMLDialogElement2 && element2.hasAttribute("open")) { + return false; + } + if (document.fullscreenElement === element2) return false; + return true; + } + function getStackPosition2(popover) { + if (!popover) return 0; + return Array.from(autoPopoverList2.get(popover.ownerDocument) || []).indexOf( + popover + ) + 1; + } + function topMostClickedPopover2(target) { + const clickedPopover = nearestInclusiveOpenPopover2(target); + const invokerPopover = nearestInclusiveTargetPopoverForInvoker2(target); + if (getStackPosition2(clickedPopover) > getStackPosition2(invokerPopover)) { + return clickedPopover; + } + return invokerPopover; + } + function topMostAutoPopover2(document2) { + const documentPopovers = autoPopoverList2.get(document2); + for (const popover of documentPopovers || []) { + if (!popover.isConnected) { + documentPopovers.delete(popover); + } else { + return popover; + } + } + return null; + } + function getRootNode2(node) { + if (typeof node.getRootNode === "function") { + return node.getRootNode(); + } + if (node.parentNode) return getRootNode2(node.parentNode); + return node; + } + function nearestInclusiveOpenPopover2(node) { + while (node) { + if (node instanceof HTMLElement && node.popover === "auto" && visibilityState2.get(node) === "showing") { + return node; + } + node = node instanceof Element && node.assignedSlot || node.parentElement || getRootNode2(node); + if (node instanceof ShadowRoot3) node = node.host; + if (node instanceof Document) return; + } + } + function nearestInclusiveTargetPopoverForInvoker2(node) { + while (node) { + const nodePopover = node.popoverTargetElement; + if (nodePopover instanceof HTMLElement) return nodePopover; + node = node.parentElement || getRootNode2(node); + if (node instanceof ShadowRoot3) node = node.host; + if (node instanceof Document) return; + } + } + function topMostPopoverAncestor2(newPopover) { + const popoverPositions = /* @__PURE__ */ new Map(); + let i = 0; + for (const popover of autoPopoverList2.get(newPopover.ownerDocument) || []) { + popoverPositions.set(popover, i); + i += 1; + } + popoverPositions.set(newPopover, i); + i += 1; + let topMostPopoverAncestor22 = null; + function checkAncestor(candidate) { + const candidateAncestor = nearestInclusiveOpenPopover2(candidate); + if (candidateAncestor === null) return null; + const candidatePosition = popoverPositions.get(candidateAncestor); + if (topMostPopoverAncestor22 === null || popoverPositions.get(topMostPopoverAncestor22) < candidatePosition) { + topMostPopoverAncestor22 = candidateAncestor; + } + } + checkAncestor(newPopover.parentElement || getRootNode2(newPopover)); + return topMostPopoverAncestor22; + } + function isFocusable2(focusTarget) { + if (focusTarget.hidden || focusTarget instanceof ShadowRoot3) return false; + if (focusTarget instanceof HTMLButtonElement || focusTarget instanceof HTMLInputElement || focusTarget instanceof HTMLSelectElement || focusTarget instanceof HTMLTextAreaElement || focusTarget instanceof HTMLOptGroupElement || focusTarget instanceof HTMLOptionElement || focusTarget instanceof HTMLFieldSetElement) { + if (focusTarget.disabled) return false; + } + if (focusTarget instanceof HTMLInputElement && focusTarget.type === "hidden") { + return false; + } + if (focusTarget instanceof HTMLAnchorElement && focusTarget.href === "") { + return false; + } + return typeof focusTarget.tabIndex === "number" && focusTarget.tabIndex !== -1; + } + function focusDelegate2(focusTarget) { + if (focusTarget.shadowRoot && focusTarget.shadowRoot.delegatesFocus !== true) { + return null; + } + let whereToLook = focusTarget; + if (whereToLook.shadowRoot) { + whereToLook = whereToLook.shadowRoot; + } + let autoFocusDelegate = whereToLook.querySelector("[autofocus]"); + if (autoFocusDelegate) { + return autoFocusDelegate; + } else { + const slots = whereToLook.querySelectorAll("slot"); + for (const slot of slots) { + const assignedElements = slot.assignedElements({ flatten: true }); + for (const el of assignedElements) { + if (el.hasAttribute("autofocus")) { + return el; + } else { + autoFocusDelegate = el.querySelector("[autofocus]"); + if (autoFocusDelegate) { + return autoFocusDelegate; + } + } + } + } + } + const walker2 = focusTarget.ownerDocument.createTreeWalker( + whereToLook, + NodeFilter.SHOW_ELEMENT + ); + let descendant = walker2.currentNode; + while (descendant) { + if (isFocusable2(descendant)) { + return descendant; + } + descendant = walker2.nextNode(); + } + } + function popoverFocusingSteps2(subject) { + focusDelegate2(subject)?.focus(); + } + var previouslyFocusedElements2 = /* @__PURE__ */ new WeakMap(); + function showPopover2(element2) { + if (!checkPopoverValidity2(element2, false)) { + return; + } + const document2 = element2.ownerDocument; + if (!element2.dispatchEvent( + new ToggleEvent2("beforetoggle", { + cancelable: true, + oldState: "closed", + newState: "open" + }) + )) { + return; + } + if (!checkPopoverValidity2(element2, false)) { + return; + } + let shouldRestoreFocus = false; + if (element2.popover === "auto") { + const originalType = element2.getAttribute("popover"); + const ancestor = topMostPopoverAncestor2(element2) || document2; + hideAllPopoversUntil2(ancestor, false, true); + if (originalType !== element2.getAttribute("popover") || !checkPopoverValidity2(element2, false)) { + return; + } + } + if (!topMostAutoPopover2(document2)) { + shouldRestoreFocus = true; + } + previouslyFocusedElements2.delete(element2); + const originallyFocusedElement = document2.activeElement; + element2.classList.add(":popover-open"); + visibilityState2.set(element2, "showing"); + if (!topLayerElements2.has(document2)) { + topLayerElements2.set(document2, /* @__PURE__ */ new Set()); + } + topLayerElements2.get(document2).add(element2); + popoverFocusingSteps2(element2); + if (element2.popover === "auto") { + if (!autoPopoverList2.has(document2)) { + autoPopoverList2.set(document2, /* @__PURE__ */ new Set()); + } + autoPopoverList2.get(document2).add(element2); + setInvokerAriaExpanded2(popoverInvoker2.get(element2), true); + } + if (shouldRestoreFocus && originallyFocusedElement && element2.popover === "auto") { + previouslyFocusedElements2.set(element2, originallyFocusedElement); + } + queuePopoverToggleEventTask2(element2, "closed", "open"); + } + function hidePopover2(element2, focusPreviousElement = false, fireEvents = false) { + if (!checkPopoverValidity2(element2, true)) { + return; + } + const document2 = element2.ownerDocument; + if (element2.popover === "auto") { + hideAllPopoversUntil2(element2, focusPreviousElement, fireEvents); + if (!checkPopoverValidity2(element2, true)) { + return; + } + } + setInvokerAriaExpanded2(popoverInvoker2.get(element2), false); + popoverInvoker2.delete(element2); + if (fireEvents) { + element2.dispatchEvent( + new ToggleEvent2("beforetoggle", { + oldState: "open", + newState: "closed" + }) + ); + if (!checkPopoverValidity2(element2, true)) { + return; + } + } + topLayerElements2.get(document2)?.delete(element2); + autoPopoverList2.get(document2)?.delete(element2); + element2.classList.remove(":popover-open"); + visibilityState2.set(element2, "hidden"); + if (fireEvents) { + queuePopoverToggleEventTask2(element2, "open", "closed"); + } + const previouslyFocusedElement = previouslyFocusedElements2.get(element2); + if (previouslyFocusedElement) { + previouslyFocusedElements2.delete(element2); + if (focusPreviousElement) { + previouslyFocusedElement.focus(); + } + } + } + function closeAllOpenPopovers2(document2, focusPreviousElement = false, fireEvents = false) { + let popover = topMostAutoPopover2(document2); + while (popover) { + hidePopover2(popover, focusPreviousElement, fireEvents); + popover = topMostAutoPopover2(document2); + } + } + function hideAllPopoversUntil2(endpoint, focusPreviousElement, fireEvents) { + const document2 = endpoint.ownerDocument || endpoint; + if (endpoint instanceof Document) { + return closeAllOpenPopovers2(document2, focusPreviousElement, fireEvents); + } + let lastToHide = null; + let foundEndpoint = false; + for (const popover of autoPopoverList2.get(document2) || []) { + if (popover === endpoint) { + foundEndpoint = true; + } else if (foundEndpoint) { + lastToHide = popover; + break; + } + } + if (!foundEndpoint) { + return closeAllOpenPopovers2(document2, focusPreviousElement, fireEvents); + } + while (lastToHide && getPopoverVisibilityState2(lastToHide) === "showing" && autoPopoverList2.get(document2)?.size) { + hidePopover2(lastToHide, focusPreviousElement, fireEvents); + } + } + var popoverPointerDownTargets2 = /* @__PURE__ */ new WeakMap(); + function lightDismissOpenPopovers2(event) { + if (!event.isTrusted) return; + const target = event.composedPath()[0]; + if (!target) return; + const document2 = target.ownerDocument; + const topMostPopover = topMostAutoPopover2(document2); + if (!topMostPopover) return; + const ancestor = topMostClickedPopover2(target); + if (ancestor && event.type === "pointerdown") { + popoverPointerDownTargets2.set(document2, ancestor); + } else if (event.type === "pointerup") { + const sameTarget = popoverPointerDownTargets2.get(document2) === ancestor; + popoverPointerDownTargets2.delete(document2); + if (sameTarget) { + hideAllPopoversUntil2(ancestor || document2, false, true); + } + } + } + var initialAriaExpandedValue2 = /* @__PURE__ */ new WeakMap(); + function setInvokerAriaExpanded2(el, force = false) { + if (!el) return; + if (!initialAriaExpandedValue2.has(el)) { + initialAriaExpandedValue2.set(el, el.getAttribute("aria-expanded")); + } + const popover = el.popoverTargetElement; + if (popover instanceof HTMLElement && popover.popover === "auto") { + el.setAttribute("aria-expanded", String(force)); + } else { + const initialValue = initialAriaExpandedValue2.get(el); + if (!initialValue) { + el.removeAttribute("aria-expanded"); + } else { + el.setAttribute("aria-expanded", initialValue); + } + } + } + var ShadowRoot23 = globalThis.ShadowRoot || function() { + }; + function isSupported2() { + return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype === "object" && "popover" in HTMLElement.prototype; + } + function isPolyfilled() { + return Boolean( + document.body?.showPopover && !/native code/i.test(document.body.showPopover.toString()) + ); + } + function patchSelectorFn2(object, name, mapper) { + const original = object[name]; + Object.defineProperty(object, name, { + value(selector) { + return original.call(this, mapper(selector)); + } + }); + } + var nonEscapedPopoverSelector2 = /(^|[^\\]):popover-open\b/g; + function hasLayerSupport2() { + return typeof globalThis.CSSLayerBlockRule === "function"; + } + function getStyles2() { + const useLayer = hasLayerSupport2(); + return ` +${useLayer ? "@layer popover-polyfill {" : ""} + :where([popover]) { + position: fixed; + z-index: 2147483647; + inset: 0; + padding: 0.25em; + width: fit-content; + height: fit-content; + border-width: initial; + border-color: initial; + border-image: initial; + border-style: solid; + background-color: canvas; + color: canvastext; + overflow: auto; + margin: auto; + } + + :where([popover]:not(.\\:popover-open)) { + display: none; + } + + :where(dialog[popover].\\:popover-open) { + display: block; + } + + :where(dialog[popover][open]) { + display: revert; + } + + :where([anchor].\\:popover-open) { + inset: auto; + } + + :where([anchor]:popover-open) { + inset: auto; + } + + @supports not (background-color: canvas) { + :where([popover]) { + background-color: white; + color: black; + } + } + + @supports (width: -moz-fit-content) { + :where([popover]) { + width: -moz-fit-content; + height: -moz-fit-content; + } + } + + @supports not (inset: 0) { + :where([popover]) { + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } +${useLayer ? "}" : ""} +`; + } + var popoverStyleSheet2 = null; + function injectStyles2(root) { + const styles = getStyles2(); + if (popoverStyleSheet2 === null) { + try { + popoverStyleSheet2 = new CSSStyleSheet(); + popoverStyleSheet2.replaceSync(styles); + } catch { + popoverStyleSheet2 = false; + } + } + if (popoverStyleSheet2 === false) { + const sheet = document.createElement("style"); + sheet.textContent = styles; + if (root instanceof Document) { + root.head.prepend(sheet); + } else { + root.prepend(sheet); + } + } else { + root.adoptedStyleSheets = [popoverStyleSheet2, ...root.adoptedStyleSheets]; + } + } + function apply2() { + if (typeof window === "undefined") return; + window.ToggleEvent = window.ToggleEvent || ToggleEvent2; + function rewriteSelector(selector) { + if (selector?.includes(":popover-open")) { + selector = selector.replace( + nonEscapedPopoverSelector2, + "$1.\\:popover-open" + ); + } + return selector; + } + patchSelectorFn2(Document.prototype, "querySelector", rewriteSelector); + patchSelectorFn2(Document.prototype, "querySelectorAll", rewriteSelector); + patchSelectorFn2(Element.prototype, "querySelector", rewriteSelector); + patchSelectorFn2(Element.prototype, "querySelectorAll", rewriteSelector); + patchSelectorFn2(Element.prototype, "matches", rewriteSelector); + patchSelectorFn2(Element.prototype, "closest", rewriteSelector); + patchSelectorFn2( + DocumentFragment.prototype, + "querySelectorAll", + rewriteSelector + ); + Object.defineProperties(HTMLElement.prototype, { + popover: { + enumerable: true, + configurable: true, + get() { + if (!this.hasAttribute("popover")) return null; + const value3 = (this.getAttribute("popover") || "").toLowerCase(); + if (value3 === "" || value3 == "auto") return "auto"; + return "manual"; + }, + set(value3) { + if (value3 === null) { + this.removeAttribute("popover"); + } else { + this.setAttribute("popover", value3); + } + } + }, + showPopover: { + enumerable: true, + configurable: true, + value() { + showPopover2(this); + } + }, + hidePopover: { + enumerable: true, + configurable: true, + value() { + hidePopover2(this, true, true); + } + }, + togglePopover: { + enumerable: true, + configurable: true, + value(force) { + if (visibilityState2.get(this) === "showing" && force === void 0 || force === false) { + hidePopover2(this, true, true); + } else if (force === void 0 || force === true) { + showPopover2(this); + } + } + } + }); + const originalAttachShadow = Element.prototype.attachShadow; + if (originalAttachShadow) { + Object.defineProperties(Element.prototype, { + attachShadow: { + enumerable: true, + configurable: true, + writable: true, + value(options) { + const shadowRoot = originalAttachShadow.call(this, options); + injectStyles2(shadowRoot); + return shadowRoot; + } + } + }); + } + const originalAttachInternals = HTMLElement.prototype.attachInternals; + if (originalAttachInternals) { + Object.defineProperties(HTMLElement.prototype, { + attachInternals: { + enumerable: true, + configurable: true, + writable: true, + value() { + const internals = originalAttachInternals.call(this); + if (internals.shadowRoot) { + injectStyles2(internals.shadowRoot); + } + return internals; + } + } + }); + } + const popoverTargetAssociatedElements = /* @__PURE__ */ new WeakMap(); + function applyPopoverInvokerElementMixin(ElementClass) { + Object.defineProperties(ElementClass.prototype, { + popoverTargetElement: { + enumerable: true, + configurable: true, + set(targetElement) { + if (targetElement === null) { + this.removeAttribute("popovertarget"); + popoverTargetAssociatedElements.delete(this); + } else if (!(targetElement instanceof Element)) { + throw new TypeError( + `popoverTargetElement must be an element or null` + ); + } else { + this.setAttribute("popovertarget", ""); + popoverTargetAssociatedElements.set(this, targetElement); + } + }, + get() { + if (this.localName !== "button" && this.localName !== "input") { + return null; + } + if (this.localName === "input" && this.type !== "reset" && this.type !== "image" && this.type !== "button") { + return null; + } + if (this.disabled) { + return null; + } + if (this.form && this.type === "submit") { + return null; + } + const targetElement = popoverTargetAssociatedElements.get(this); + if (targetElement && targetElement.isConnected) { + return targetElement; + } else if (targetElement && !targetElement.isConnected) { + popoverTargetAssociatedElements.delete(this); + return null; + } + const root = getRootNode2(this); + const idref = this.getAttribute("popovertarget"); + if ((root instanceof Document || root instanceof ShadowRoot23) && idref) { + return root.getElementById(idref) || null; + } + return null; + } + }, + popoverTargetAction: { + enumerable: true, + configurable: true, + get() { + const value3 = (this.getAttribute("popovertargetaction") || "").toLowerCase(); + if (value3 === "show" || value3 === "hide") return value3; + return "toggle"; + }, + set(value3) { + this.setAttribute("popovertargetaction", value3); + } + } + }); + } + applyPopoverInvokerElementMixin(HTMLButtonElement); + applyPopoverInvokerElementMixin(HTMLInputElement); + const handleInvokerActivation = (event) => { + const composedPath = event.composedPath(); + const target = composedPath[0]; + if (!(target instanceof Element) || target?.shadowRoot) { + return; + } + const root = getRootNode2(target); + if (!(root instanceof ShadowRoot23 || root instanceof Document)) { + return; + } + const invoker = composedPath.find( + (el) => el.matches?.("[popovertargetaction],[popovertarget]") + ); + if (invoker) { + popoverTargetAttributeActivationBehavior2(invoker); + event.preventDefault(); + return; + } + }; + const onKeydown = (event) => { + const key = event.key; + const target = event.target; + if (!event.defaultPrevented && target && (key === "Escape" || key === "Esc")) { + hideAllPopoversUntil2(target.ownerDocument, true, true); + } + }; + const addEventListeners = (root) => { + root.addEventListener("click", handleInvokerActivation); + root.addEventListener("keydown", onKeydown); + root.addEventListener("pointerdown", lightDismissOpenPopovers2); + root.addEventListener("pointerup", lightDismissOpenPopovers2); + }; + addEventListeners(document); + injectStyles2(document); + } + + // js/utils.js + function inject(callback) { + let styles = callback({ + css: (strings, ...values) => `@layer base { ${strings.raw[0] + values.join("")} }` + }); + if (document.adoptedStyleSheets === void 0) { + let styleElement = document.createElement("style"); + styleElement.textContent = styles; + document.head.appendChild(styleElement); + return; + } + let sheet = new CSSStyleSheet(); + sheet.replaceSync(styles); + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; + } + function closest(el, condition) { + let current = el; + while (current) { + if (condition(current)) return current; + current = current.parentElement; + } + } + function walker(el, callback) { + let walker2 = document.createTreeWalker( + el, + NodeFilter.SHOW_ELEMENT, + callback ? { + acceptNode: (el2) => { + let skipped, rejected; + callback(el2, { + skip: () => skipped = true, + reject: () => rejected = true + }); + if (skipped) return NodeFilter.FILTER_SKIP; + if (rejected) return NodeFilter.FILTER_REJECT; + return NodeFilter.FILTER_ACCEPT; + } + } : {} + ); + return new Traverse(walker2); + } + var Traverse = class { + constructor(walker2) { + this.walker = walker2; + } + from(el) { + this.walker.currentNode = el; + return this; + } + first() { + return this.walker.firstChild(); + } + last() { + return this.walker.lastChild(); + } + next(el) { + this.walker.currentNode = el; + return this.walker.nextSibling(); + } + nextOrFirst(el) { + let found = this.next(el); + if (found) return found; + this.walker.currentNode = this.walker.root; + return this.first(); + } + prev(el) { + this.walker.currentNode = el; + return this.walker.previousSibling(); + } + prevOrLast(el) { + let found = this.prev(el); + if (found) return found; + this.walker.currentNode = this.walker.root; + return this.last(); + } + closest(el, condition) { + let walker2 = this.from(el).walker; + while (walker2.currentNode) { + if (condition(walker2.currentNode)) return walker2.currentNode; + walker2.parentNode(); + } + } + contains(el) { + return this.find((i) => i === el); + } + find(callback) { + return this.walk((el, bail) => { + callback(el) && bail(el); + }); + } + findOrFirst(callback) { + let found = this.find(callback); + if (!found) this.walker.currentNode = this.walker.root; + return this.first(); + } + each(callback) { + this.walk((el) => callback(el)); + } + some(callback) { + return !!this.find(callback); + } + every(callback) { + let every = true; + this.walk((el) => { + callback(el) || (every = false); + }); + return every; + } + map(callback) { + let els = []; + this.walk((el) => els.push(callback(el))); + return els; + } + filter(callback) { + let els = []; + this.walk((el) => callback(el) && els.push(el)); + return els; + } + walk(callback) { + let current; + let walker2 = this.walker; + let bailed; + while (walker2.nextNode()) { + current = walker2.currentNode; + callback(current, (bailValue) => bailed = bailValue); + if (bailed !== void 0) { + break; + } + } + return bailed; + } + }; + function element(name, type) { + customElements.define(`ui-${name}`, type); + } + function on(target, event, handler, options = {}) { + target.addEventListener(event, handler, options); + return { + off: () => target.removeEventListener(event, handler), + pause: (callback) => { + target.removeEventListener(event, handler), callback(); + target.addEventListener(event, handler); + } + }; + } + function isFocusable3(el) { + let selectors = [ + "a[href]", + "area[href]", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "button:not([disabled])", + "iframe", + "object", + "embed", + "[tabindex]", + "[contenteditable]" + ]; + return selectors.some((selector) => el.matches(selector)) && el.tabIndex >= 0; + } + function throttle(func, limit) { + let inThrottle; + return function() { + let context = this, args = arguments; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + function timeout(callback, delay) { + let timerId; + let start; + let remaining = delay; + let active = false; + let timeout2 = { + pause: () => { + if (!active) return; + clearTimeout(timerId); + remaining -= Date.now() - start; + active = false; + }, + resume: () => { + if (active) return; + start = Date.now(); + timerId = setTimeout(callback, remaining); + active = true; + }, + cancel: () => { + clearTimeout(timerId); + active = false; + remaining = delay; + } + }; + timeout2.resume(); + return timeout2; + } + var using = "pointer"; + document.addEventListener("keydown", () => using = "keyboard", { capture: true }); + document.addEventListener("pointerdown", (e) => { + using = e.pointerType === "mouse" ? "mouse" : "touch"; + }, { capture: true }); + document.addEventListener("pointermove", (e) => { + using = e.pointerType === "mouse" ? "mouse" : "touch"; + }, { capture: true }); + function isUsingKeyboard() { + return using === "keyboard"; + } + function isUsingTouch() { + return using === "touch"; + } + function search(el, callback) { + let runningQuery = ""; + let clearRunningQuery = debounce(() => { + runningQuery = ""; + }, 300); + el.addEventListener("keydown", (e) => { + if (e.key.length === 1 && /[a-zA-Z]/.test(e.key)) { + runningQuery += e.key; + callback(runningQuery); + e.stopPropagation(); + } + clearRunningQuery(); + }); + } + function dispenseId(el, prefix) { + return "lofi-" + (prefix ? prefix + "-" : "") + Math.random().toString(16).slice(2); + } + function assignId(el, prefix) { + let id = el.hasAttribute("id") ? el.getAttribute("id") : dispenseId(el, prefix); + setAttribute2(el, "id", id); + if (!el._x_bindings) el._x_bindings = {}; + if (!el._x_bindings.id) el._x_bindings.id = id; + return id; + } + function detangle() { + let blocked = false; + return (callback) => (...args) => { + if (blocked) return; + blocked = true; + callback(...args); + blocked = false; + }; + } + function interest(trigger, panel, { gain, lose, focusable, useSafeArea }) { + let engaged = false; + let focusInHander = (e) => { + if (!isUsingKeyboard()) return; + if (trigger.contains(e.target) || panel.contains(e.target)) { + engaged = true; + gain(); + } else { + engaged = false; + lose(); + } + }; + focusable && document.addEventListener("focusin", focusInHander); + let removeFocusInHandler = () => { + document.removeEventListener("focusin", focusInHander); + }; + let removeSafeArea = () => { + }; + let removePointerMoveHandler = () => { + }; + let disinterest = () => { + engaged = false; + lose(); + removeSafeArea(); + removePointerMoveHandler(); + }; + let clear = () => { + engaged = false; + removeSafeArea(); + removePointerMoveHandler(); + }; + let pointerEnterHandler = (e) => { + if (isUsingTouch()) return; + if (engaged) return; + engaged = true; + gain(); + setTimeout(() => { + let { safeArea, redraw: redrawSafeArea, remove: remove2 } = useSafeArea ? createSafeArea(trigger, panel, e.clientX, e.clientY) : nullSafeArea(); + removeSafeArea = remove2; + let pointerStoppedOverSafeAreaTimeout; + let pointerMoveHandler = throttle((e2) => { + let panelRect = panel.getBoundingClientRect(); + let triggerRect = trigger.getBoundingClientRect(); + let mouseState; + if (safeArea.contains(e2.target) && mouseIsExclusivelyInsideSafeArea(triggerRect, panelRect, e2.clientX, e2.clientY)) mouseState = "safeArea"; + else if (panel.contains(e2.target)) mouseState = "panel"; + else if (trigger.contains(e2.target)) mouseState = "trigger"; + else mouseState = "outside"; + if (pointerStoppedOverSafeAreaTimeout) { + clearTimeout(pointerStoppedOverSafeAreaTimeout); + } + switch (mouseState) { + case "outside": + disinterest(); + break; + case "trigger": + redrawSafeArea(e2.clientX, e2.clientY); + break; + case "panel": + removeSafeArea(); + break; + case "safeArea": + redrawSafeArea(e2.clientX, e2.clientY); + pointerStoppedOverSafeAreaTimeout = setTimeout(() => { + disinterest(); + }, 300); + break; + default: + break; + } + }, 100); + document.addEventListener("pointermove", pointerMoveHandler); + removePointerMoveHandler = () => document.removeEventListener("pointermove", pointerMoveHandler); + }); + }; + trigger.addEventListener("pointerenter", pointerEnterHandler); + let removePointerEnterHandler = () => { + trigger.removeEventListener("pointerenter", pointerEnterHandler); + }; + let remove = () => { + clear(); + removePointerEnterHandler(); + removeFocusInHandler(); + }; + return { clear, remove }; + } + function createSafeArea(trigger, panel, x, y) { + let safeArea = document.createElement("div"); + let panelRect = panel.getBoundingClientRect(); + let triggerRect = trigger.getBoundingClientRect(); + safeArea.style.position = "fixed"; + setAttribute2(safeArea, "data-safe-area", ""); + let draw = (x2, y2) => { + if (panelRect.top === 0 && panelRect.bottom === 0) return; + let direction; + if (panelRect.left < triggerRect.left) direction = "left"; + if (panelRect.right > triggerRect.right) direction = "right"; + if (panelRect.top < triggerRect.top && panelRect.bottom < y2) direction = "up"; + if (panelRect.bottom > triggerRect.bottom && panelRect.top > y2) direction = "down"; + if (direction === void 0) direction = "right"; + let left, right, width, top, bottom, height, offset3, shape; + let padding = 10; + switch (direction) { + case "left": + left = panelRect.right; + right = Math.max(panelRect.right, x2) + 5; + width = right - left; + top = Math.min(triggerRect.top, panelRect.top) - padding; + bottom = Math.max(triggerRect.bottom, panelRect.bottom) + padding; + height = bottom - top; + offset3 = y2 - top; + shape = `polygon(0% 0%, 100% ${offset3}px, 0% 100%)`; + break; + case "right": + left = Math.min(panelRect.left, x2) - 5; + right = panelRect.left; + width = right - left; + top = Math.min(triggerRect.top, panelRect.top) - padding; + bottom = Math.max(triggerRect.bottom, panelRect.bottom) + padding; + height = bottom - top; + offset3 = y2 - top; + shape = `polygon(0% ${offset3}px, 100% 0%, 100% 100%)`; + break; + case "up": + left = Math.min(x2, panelRect.left) - padding; + right = Math.max(x2, panelRect.right) + padding; + width = right - left; + top = panelRect.bottom; + bottom = Math.max(panelRect.bottom, y2) + 5; + height = bottom - top; + offset3 = x2 - left; + shape = `polygon(0% 0%, 100% 0%, ${offset3}px 100%)`; + break; + case "down": + left = Math.min(x2, panelRect.left) - padding; + right = Math.max(x2, panelRect.right) + padding; + width = right - left; + top = Math.min(panelRect.top, y2) - 5; + bottom = panelRect.top; + height = bottom - top; + offset3 = x2 - left; + shape = `polygon(${offset3}px 0%, 100% 100%, 0% 100%)`; + break; + } + safeArea.style.left = `${left}px`; + safeArea.style.top = `${top}px`; + safeArea.style.width = `${width}px`; + safeArea.style.height = `${height}px`; + safeArea.style.clipPath = shape; + }; + return { + safeArea, + redraw: (x2, y2) => { + if (!safeArea.isConnected) trigger.appendChild(safeArea); + draw(x2, y2); + }, + remove: () => { + safeArea.remove(); + } + }; + } + function mouseIsExclusivelyInsideSafeArea(triggerRect, panelRect, x, y) { + return !mouseIsOverTrigger(triggerRect, x, y) && !mouseIsOverPanel(panelRect, x, y); + } + function mouseIsOverTrigger(triggerRect, x, y) { + if (triggerRect.left <= x && x <= triggerRect.right && (triggerRect.top <= y && y <= triggerRect.bottom)) return true; + return false; + } + function mouseIsOverPanel(panelRect, x, y) { + if (panelRect.left <= x && x <= panelRect.right && (panelRect.top <= y && y <= panelRect.bottom)) return true; + return false; + } + function setAttribute2(el, name, value3) { + if (el._durableAttributeObserver === void 0) { + el._durableAttributeObserver = attributeObserver(el, [name]); + } + if (!el._durableAttributeObserver.hasAttribute(name)) { + el._durableAttributeObserver.addAttribute(name); + } + el._durableAttributeObserver.pause(() => { + el.setAttribute(name, value3); + }); + } + function removeAndReleaseAttribute(el, name) { + removeAttribute(el, name); + releaseAttribute(el, name); + } + function removeAttribute(el, name) { + if (el._durableAttributeObserver === void 0) { + el._durableAttributeObserver = attributeObserver(el, [name]); + } + if (!el._durableAttributeObserver.hasAttribute(name)) { + el._durableAttributeObserver.addAttribute(name); + } + el._durableAttributeObserver.pause(() => { + el.removeAttribute(name); + }); + } + function releaseAttribute(el, name) { + if (!el?._durableAttributeObserver?.hasAttribute(name)) return; + el._durableAttributeObserver.releaseAttribute(name); + } + function attributeObserver(el, initialAttributes) { + let processMutations = (mutations) => { + mutations.forEach((mutation) => { + if (mutation.oldValue === null) { + el._durableAttributeObserver.pause(() => removeAttribute(el, mutation.attributeName)); + } else { + el._durableAttributeObserver.pause(() => setAttribute2(el, mutation.attributeName, mutation.oldValue)); + } + }); + }; + let observer = new MutationObserver((mutations) => processMutations(mutations)); + observer.observe(el, { attributeFilter: initialAttributes, attributeOldValue: true }); + return { + attributes: initialAttributes, + hasAttribute(name) { + return this.attributes.includes(name); + }, + addAttribute(name) { + this.attributes.includes(name) || this.attributes.push(name); + observer.observe(el, { attributeFilter: this.attributes, attributeOldValue: true }); + }, + releaseAttribute(name) { + if (!this.hasAttribute(name)) return; + observer.observe(el, { attributeFilter: this.attributes, attributeOldValue: true }); + }, + pause(callback) { + processMutations(observer.takeRecords()); + observer.disconnect(); + callback(); + observer.observe(el, { attributeFilter: this.attributes, attributeOldValue: true }); + } + }; + } + function nullSafeArea() { + return { + safeArea: { contains: () => false }, + redraw: () => { + }, + remove: () => { + } + }; + } + function debounce(callback, delay) { + let timeout2; + return (...args) => { + clearTimeout(timeout2); + timeout2 = setTimeout(() => { + callback(...args); + }, delay); + }; + } + var lockCount = 0; + var pointerEventsLocked = false; + inject(({ css }) => css`[data-flux-allow-scroll] { pointer-events: auto; }`); + function lockScroll(el = null, allowScroll = false, except = []) { + if (allowScroll) return { lock: () => { + }, unlock: () => { + } }; + let applyDocumentLockStyles = (disablePointerEvents = false) => { + undoLockStyles(document.documentElement); + let lockStyles = { + overflow: "hidden", + ...disablePointerEvents ? { pointerEvents: "none" } : {} + }; + if (window.CSS && CSS.supports && CSS.supports("scrollbar-gutter: stable")) { + if (document.documentElement.scrollHeight > document.documentElement.clientHeight) { + lockStyles.scrollbarGutter = "stable"; + } + } else { + lockStyles.paddingRight = `calc(${window.innerWidth - document.documentElement.clientWidth}px + ${window.getComputedStyle(document.documentElement).paddingRight})`; + } + setLockStyles(document.documentElement, lockStyles); + if (disablePointerEvents) { + setAttribute2(el, "data-flux-allow-scroll", ""); + except.forEach((el2) => { + setAttribute2(el2, "data-flux-allow-scroll", ""); + }); + pointerEventsLocked = true; + } + }; + let removeDocumentLockStyles = (enablePointerEvents = false) => { + undoLockStyles(document.documentElement); + if (enablePointerEvents) { + removeAndReleaseAttribute(el, "data-flux-allow-scroll"); + except.forEach((el2) => { + removeAttribute(el2, "data-flux-allow-scroll"); + }); + pointerEventsLocked = false; + } + }; + return { + lock() { + lockCount++; + if (lockCount > 1 && el !== null && pointerEventsLocked) return; + applyDocumentLockStyles(el !== null && !pointerEventsLocked); + }, + unlock() { + lockCount = Math.max(0, lockCount - 1); + if (lockCount > 0 && el !== null && !pointerEventsLocked) return; + removeDocumentLockStyles(el !== null && pointerEventsLocked); + if (lockCount > 0) { + applyDocumentLockStyles(false); + } + } + }; + } + function setLockStyles(element2, styles) { + let unlockedStyles = JSON.parse(element2.getAttribute(`data-flux-scroll-unlock`) || "{}"); + Object.entries(styles).forEach(([style, value3]) => { + if (unlockedStyles[style] === void 0) { + unlockedStyles[style] = element2.style[style]; + element2.style[style] = value3; + } + }); + element2.setAttribute(`data-flux-scroll-unlock`, JSON.stringify(unlockedStyles)); + } + function undoLockStyles(element2) { + let unlockedStyles = JSON.parse(element2.getAttribute(`data-flux-scroll-unlock`) || "{}"); + Object.entries(unlockedStyles).forEach(([style, value3]) => { + element2.style[style] = value3; + }); + element2.removeAttribute(`data-flux-scroll-unlock`); + } + function setStyle(element2, style, value3) { + let currentValue = element2.style[style]; + element2.style[style] = value3; + return () => { + element2.style[style] = currentValue; + }; + } + function initFauxButton(el, isDisabled, action) { + let ifKey = (key, callback) => (e) => { + if (e.key === key && !isDisabled()) { + callback(); + e.preventDefault(); + e.stopPropagation(); + } + }; + setAttribute2(el, "role", "button"); + let syncDisabledAttributes = () => { + if (el.hasAttribute("disabled")) { + setAttribute2(el, "aria-disabled", "true"); + setAttribute2(el, "tabindex", "-1"); + } else { + removeAttribute(el, "aria-disabled"); + setAttribute2(el, "tabindex", "0"); + } + }; + let observer = new MutationObserver(() => syncDisabledAttributes()); + observer.observe(el, { attributes: true, attributeFilter: ["disabled"] }); + syncDisabledAttributes(); + on(el, "click", () => action()); + on(el, "keydown", ifKey("Enter", () => action())); + on(el, "keydown", ifKey(" ", () => { + })); + on(el, "keyup", ifKey(" ", () => action())); + } + function responsiveAttributeValue(el, name, fallback = null) { + let getValue = () => { + let value3 = el.getAttribute(name); + let breakpoints = { + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + "2xl": 1536 + }; + for (let [breakpoint, minWidth] of Object.entries(breakpoints).reverse()) { + let responsiveValue = el.getAttribute(`${breakpoint}:${name}`); + if (responsiveValue && window.innerWidth >= minWidth) { + return responsiveValue; + } + } + return value3 || fallback; + }; + let currentValue = getValue(); + let callbacks = []; + new ResizeObserver(() => { + let newValue = getValue(); + let memo = JSON.stringify(currentValue); + if (JSON.stringify(newValue) !== memo) { + currentValue = newValue; + callbacks.forEach((callback) => callback(newValue)); + } + }).observe(window.document.documentElement); + return [currentValue, (callback) => callbacks.push(callback)]; + } + function getLocale() { + return navigator?.language || document.documentElement.lang || "en-US"; + } + function hydrateTemplate(template, slotsAndAttributes = { slots: {}, attrs: {} }) { + let { slots = {}, attrs = {} } = slotsAndAttributes; + let clone = template.content.cloneNode(true).firstElementChild; + Object.entries(slots).forEach(([key, value3]) => { + let slotNodes = key === "default" ? clone.querySelectorAll("slot:not([name])") : clone.querySelectorAll(`slot[name="${key}"]`); + slotNodes.forEach((i) => i.replaceWith( + typeof value3 === "string" ? document.createTextNode(value3) : value3 + )); + }); + clone.querySelectorAll("slot").forEach((slot) => slot.remove()); + Object.entries(attrs).forEach(([key, value3]) => { + clone.setAttribute(key, value3); + }); + clone.setAttribute("data-appended", ""); + return clone; + } + function isRTL() { + return document.documentElement.dir === "rtl"; + } + function isSafari() { + return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) && !navigator.userAgent.includes("CriOS") && !navigator.userAgent.includes("FxiOS"); + } + function isIOS() { + return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + } + var Observable = class { + constructor() { + this.subscribers = []; + } + subscribe(reason, callback) { + this.subscribers.push({ reason, callback }); + } + notify(reason, data) { + this.subscribers.forEach(({ reason: subReason, callback }) => { + if (reason === subReason) { + callback(data); + } + }); + } + }; + + // js/element.js + var UIElement = class extends HTMLElement { + wasDisconnected = false; + constructor() { + super(); + this.boot?.(); + } + connectedCallback() { + if (this.wasDisconnected) { + this.wasDisconnected = false; + return; + } + queueMicrotask(() => { + this.mount?.(); + }); + } + disconnectedCallback() { + this.wasDisconnected = true; + queueMicrotask(() => { + if (this.wasDisconnected) { + this.unmount?.(); + } + this.wasDisconnected = false; + }); + } + mixin(func, options = {}) { + return new func(this, options); + } + // @todo: this is redundant now... + appendMixin(func, options = {}) { + return new func(this, options); + } + use(func) { + let found; + this.mixins.forEach((mixin) => { + if (mixin instanceof func) found = mixin; + }); + return found; + } + uses(func) { + let found; + this.mixins.forEach((mixin) => { + if (mixin instanceof func) found = true; + }); + return !!found; + } + on(event, handler) { + return on(this, event, handler); + } + root(name, attributes = {}) { + if (name === void 0) return this.__root; + let el = document.createElement(name); + for (let name2 in attributes) { + setAttribute(el, name2, attributes[name2]); + } + let shadow = this.attachShadow({ mode: "open" }); + el.appendChild(document.createElement("slot")); + shadow.appendChild(el); + this.__root = el; + return this.__root; + } + }; + var UIControl = class extends UIElement { + // + }; + + // js/mixins/mixin.js + var Mixin = class { + constructor(el, options = {}) { + this.el = el; + this.grouped = options.grouped === void 0 ? true : false; + this.el.mixins = this.el.mixins ? this.el.mixins : /* @__PURE__ */ new Map(); + this.el.mixins.set(this.constructor.name, this); + this.el[this.constructor.name] = true; + if (!this.el.use) this.el.use = UIElement.prototype.use.bind(this.el); + this.opts = options; + this.boot?.({ + options: (defaults) => { + let options2 = defaults; + Object.entries(this.opts).forEach(([key, value3]) => { + if (value3 !== void 0) { + options2[key] = value3; + } + }); + this.opts = options2; + } + }); + queueMicrotask(() => { + this.mount?.(); + }); + } + options() { + return this.opts; + } + hasGroup() { + return !!this.group(); + } + group() { + if (this.grouped === false) return; + return closest(this.el, (i) => i[this.groupedByType.name])?.use(this.groupedByType); + } + on(event, handler) { + return on(this.el, event, handler); + } + }; + var MixinGroup = class extends Mixin { + constructor(el, options = {}) { + super(el, options); + } + walker() { + return walker(this.el, (el, { skip, reject }) => { + if (el[this.constructor.name] && el !== this.el) return reject(); + if (!el[this.groupOfType.name]) return skip(); + if (!el.mixins.get(this.groupOfType.name).grouped) return skip(); + }); + } + }; + + // js/mixins/controllable.js + var Controllable = class extends Mixin { + boot({ options }) { + options({ + bubbles: false + }); + this.initialState = this.el.value; + this.getterFunc = () => { + }; + this.setterFunc = (value3) => this.initialState = value3; + Object.defineProperty(this.el, "value", { + get: () => { + return this.getterFunc(); + }, + set: (value3) => { + this.setterFunc(value3); + } + }); + } + initial(callback) { + callback(this.initialState); + } + getter(func) { + this.getterFunc = func; + } + setter(func) { + this.setterFunc = func; + } + dispatch() { + this.el.dispatchEvent(new Event("input", { + bubbles: this.options().bubbles, + cancelable: true + })); + this.el.dispatchEvent(new Event("change", { + bubbles: this.options().bubbles, + cancelable: true + })); + } + }; + + // js/mixins/dialogable.js + var lastMouseDownEvent = null; + document.addEventListener("mousedown", (event) => lastMouseDownEvent = event); + var Dialogable = class extends Mixin { + boot({ options }) { + options({ + clickOutside: true, + triggers: [] + }); + this.onChanges = []; + this.state = false; + this.stopDialogFromFocusingTheFirstElement(); + let triggers = this.options().triggers; + let observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName !== "open") return; + this.el.hasAttribute("open") ? this.state = true : this.state = false; + }); + this.onChanges.forEach((i) => i()); + }); + observer.observe(this.el, { attributeFilter: ["open"] }); + if (this.options().clickOutside) { + this.el.addEventListener("click", (e) => { + if (e.target !== this.el) { + lastMouseDownEvent = null; + return; + } + if (lastMouseDownEvent && clickHappenedOutside(this.el, lastMouseDownEvent) && clickHappenedOutside(this.el, e)) { + this.cancel(); + e.preventDefault(); + e.stopPropagation(); + } + lastMouseDownEvent = null; + }); + } + if (this.el.hasAttribute("open")) { + this.state = true; + this.hide(); + this.show(); + } + } + onChange(callback) { + this.onChanges.push(callback); + } + show() { + if (!this.el.isConnected) return; + this.el.showModal(); + } + hide() { + this.el.close(); + } + toggle() { + this.state ? this.hide() : this.show(); + } + cancel() { + let event = new Event("cancel", { bubbles: false, cancelable: true }); + this.el.dispatchEvent(event); + if (!event.defaultPrevented) { + this.hide(); + } + } + getState() { + return this.state; + } + setState(value3) { + value3 ? this.show() : this.hide(); + } + // By default, browsers focus the first focusable element inside a dialog when it is opened. This is bad for screen readers because + // the focus could potentially be at the end of the dialog skipping all of the content. This also causes issues for iOS devices + // as when inputs are focused and the keyboard is shown, hiding half of the dialog content... + stopDialogFromFocusingTheFirstElement() { + let placeholder = document.createElement("div"); + placeholder.setAttribute("data-flux-focus-placeholder", ""); + placeholder.setAttribute("data-appended", ""); + placeholder.setAttribute("tabindex", "0"); + this.el.prepend(placeholder); + this.onChange(() => { + setAttribute2(placeholder, "style", this.state ? "display: none" : "display: block"); + if (this.state && isSafari() && !this.el.hasAttribute("autofocus") && this.el.querySelectorAll("[autofocus]").length === 0) { + setTimeout(() => { + this.el.setAttribute("tabindex", "-1"); + this.el.focus(); + this.el.blur(); + }); + } + }); + } + }; + function clickHappenedOutside(el, event) { + let rect = el.getBoundingClientRect(); + let x = event.clientX; + let y = event.clientY; + let isInside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; + return !isInside; + } + + // js/mixins/closeable.js + var Closeable = class extends Mixin { + boot() { + this.onCloses = []; + } + onClose(callback) { + this.onCloses.push(callback); + } + close() { + this.onCloses.forEach((callback) => callback()); + } + }; + + // js/modal.js + var UIModal = class extends UIElement { + boot() { + this.querySelectorAll("[data-appended]").forEach((el) => el.remove()); + this._controllable = new Controllable(this, { disabled: this.hasAttribute("disabled") }); + let button = this.button(); + let dialog = this.dialog(); + if (!dialog) return; + dialog._dialogable = new Dialogable(dialog, { + clickOutside: !this.hasAttribute("disable-click-outside") + }); + dialog._closeable = new Closeable(dialog); + dialog._closeable.onClose(() => dialog._dialogable.hide()); + this._controllable.initial((initial) => initial && dialog._dialogable.show()); + this._controllable.getter(() => dialog._dialogable.getState()); + let detangled = detangle(); + this._controllable.setter(detangled((value3) => { + dialog._dialogable.setState(value3); + })); + dialog._dialogable.onChange(detangled(() => { + this._controllable.dispatch(); + })); + let refresh = () => { + if (dialog._dialogable.getState()) { + setAttribute2(this, "data-open", ""); + button?.setAttribute("data-open", ""); + setAttribute2(dialog, "data-open", ""); + } else { + removeAttribute(this, "data-open"); + button?.removeAttribute("data-open"); + removeAttribute(dialog, "data-open"); + } + }; + dialog._dialogable.onChange(() => refresh()); + refresh(); + let { lock, unlock } = lockScroll(); + dialog._dialogable.onChange(() => { + dialog._dialogable.getState() ? lock() : unlock(); + }); + button && on(button, "click", (e) => { + dialog._dialogable.show(); + }); + } + unmount() { + if (this.dialog()?._dialogable?.getState()) { + let { unlock } = lockScroll(); + unlock(); + } + } + button() { + let button = this.querySelector("button,ui-button"); + let dialog = this.dialog(); + if (dialog?.contains(button)) return; + return button; + } + dialog() { + return this.querySelector("dialog"); + } + showModal() { + let dialog = this.dialog(); + if (!dialog) return; + dialog.showModal(); + } + }; + inject(({ css }) => css`dialog, ::backdrop { margin: auto; }`); + element("modal", UIModal); + + // js/mixins/activatable.js + var ActivatableGroup = class extends MixinGroup { + groupOfType = Activatable; + boot({ options }) { + options({ + wrap: false, + filter: false + }); + this.onChanges = []; + } + onChange(callback) { + this.onChanges.push(callback); + } + activated(activeEl) { + this.onChanges.forEach((i) => i()); + } + activateFirst() { + this.filterAwareWalker().first()?.use(Activatable).activate(); + } + activateBySearch(query) { + let found = this.filterAwareWalker().find((i) => i.textContent.toLowerCase().trim().startsWith(query.toLowerCase())); + found?.use(Activatable).activate(); + } + activateSelectedOrFirst(selectedEl) { + let isHidden = (el) => el.matches("ui-option, ui-option-create") ? getComputedStyle(el).display === "none" : false; + if (!selectedEl || isHidden(selectedEl)) { + this.filterAwareWalker().first()?.use(Activatable).activate(); + return; + } + selectedEl?.use(Activatable).activate(); + } + activateActiveOrFirst() { + let active = this.getActive(); + if (!active) { + this.filterAwareWalker().first()?.use(Activatable).activate(); + return; + } + active?.use(Activatable).activate(); + } + activateActiveOrLast() { + let active = this.getActive(); + if (!active) { + this.filterAwareWalker().last()?.use(Activatable).activate(); + return; + } + active?.use(Activatable).activate(); + } + activatePrev() { + let active = this.getActive(); + if (!active) { + this.filterAwareWalker().last()?.use(Activatable).activate(); + return; + } + let found; + if (this.options.wrap) { + found = this.filterAwareWalker().prevOrLast(active); + } else { + found = this.filterAwareWalker().prev(active); + } + found?.use(Activatable).activate(); + } + activateNext() { + let active = this.getActive(); + if (!active) { + this.filterAwareWalker().first()?.use(Activatable).activate(); + return; + } + let found; + if (this.options.wrap) { + found = this.filterAwareWalker().nextOrFirst(active); + } else { + found = this.filterAwareWalker().next(active); + } + found?.use(Activatable).activate(); + } + getActive() { + return this.walker().find((i) => i.use(Activatable).isActive()); + } + clearActive() { + this.getActive()?.use(Activatable).deactivate(); + } + filterAwareWalker() { + let isHidden = (el) => el.matches("ui-option, ui-option-create") ? getComputedStyle(el).display === "none" : false; + return walker(this.el, (el, { skip, reject }) => { + if (el[this.constructor.name] && el !== this.el) return reject(); + if (!el[this.groupOfType.name]) return skip(); + if (el.hasAttribute("disabled")) return reject(); + if (isHidden(el)) return reject(); + }); + } + }; + var Activatable = class _Activatable extends Mixin { + groupedByType = ActivatableGroup; + mount() { + this.el.addEventListener("mouseenter", () => { + this.activate(); + }); + this.el.addEventListener("mouseleave", () => { + this.deactivate(); + }); + } + activate(force = false) { + if (this.group()) { + this.group().walker().each((item) => item.use(_Activatable).deactivate(false)); + } + if (this.el.hasAttribute("disabled") && !force) return; + setAttribute2(this.el, "data-active", ""); + if (isUsingKeyboard()) { + this.el.scrollIntoView({ block: "nearest" }); + } + this.group() && this.group().activated(this.el); + } + deactivate(notify = true) { + removeAttribute(this.el, "data-active"); + notify && this.group() && this.group().activated(this.el); + } + isActive() { + return this.el.hasAttribute("data-active"); + } + }; + + // js/mixins/filterable.js + var FilterableGroup = class extends MixinGroup { + groupOfType = Filterable; + boot({ options }) { + options({}); + this.onChanges = []; + this.lastSearch = ""; + } + onChange(callback) { + this.onChanges.push(callback); + } + filter(search2) { + if (search2 === "") { + this.walker().each((i) => { + i.use(Filterable).unfilter(); + }); + } else { + this.walker().each((i) => { + if (this.matches(i, search2)) { + i.use(Filterable).unfilter(); + } else { + i.use(Filterable).filter(); + } + }); + } + if (this.lastSearch !== search2) { + this.onChanges.forEach((i) => i()); + } + this.lastSearch = search2; + } + matches(el, search2) { + return this.normalize(el.textContent).includes(this.normalize(search2)); + } + // This function normalizes the value to remove diacritics (accents) and convert to lowercase + // to ensure that the search is case-insensitive and diacritic-insensitive... + normalize(value3) { + return value3.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim(); + } + hasResults() { + return this.walker().some((i) => !i.use(Filterable).isFiltered()); + } + }; + var Filterable = class extends Mixin { + groupedByType = FilterableGroup; + boot({ options }) { + options({ mirror: null, keep: false }); + this.onChanges = []; + } + filter() { + if (this.options().keep) return; + setAttribute2(this.el, "data-hidden", ""); + if (this.options().mirror) setAttribute2(this.options().mirror, "data-hidden", ""); + } + unfilter() { + if (this.options().keep) return; + removeAttribute(this.el, "data-hidden"); + if (this.options().mirror) removeAttribute(this.options().mirror, "data-hidden", ""); + } + isFiltered() { + return this.el.hasAttribute("data-hidden"); + } + }; + + // js/mixins/popoverable.js + var currentlyOpenPopoversByScope = /* @__PURE__ */ new Map(); + var Popoverable = class extends Mixin { + boot({ options }) { + options({ triggers: [], scope: null }); + let scope = this.options().scope || "global"; + setAttribute2(this.el, "popover", "manual"); + this.triggers = this.options().triggers; + this.onChanges = []; + this.state = false; + on(this.el, "beforetoggle", (e) => { + let oldState = this.state; + this.state = e.newState === "open"; + if (this.state) { + closeOtherOpenPopovers(this.el, scope); + let controller = new AbortController(); + let activeElement = document.activeElement; + let triggers = [...this.triggers, activeElement]; + setTimeout(() => { + closeOnClickOutside(this.el, triggers, controller); + closeOnFocusAway(this.el, triggers, controller); + closeOnEscape(this.el, triggers, controller); + }); + this.el.addEventListener("beforetoggle", (e2) => { + if (e2.newState === "closed") { + controller.abort(); + activeElement?.focus(); + } + }, { signal: controller.signal }); + } + if (oldState !== this.state) { + this.onChanges.forEach((i) => i(this.state, oldState)); + } + }); + on(this.el, "toggle", (e) => { + if (e.newState === "open") { + if (!currentlyOpenPopoversByScope.has(scope)) { + currentlyOpenPopoversByScope.set(scope, /* @__PURE__ */ new Set()); + } + currentlyOpenPopoversByScope.get(scope).add(this.el); + } else if (e.newState === "closed") { + if (!currentlyOpenPopoversByScope.has(scope)) return; + currentlyOpenPopoversByScope.get(scope).delete(this.el); + if (currentlyOpenPopoversByScope.get(scope).size === 0) { + currentlyOpenPopoversByScope.delete(scope); + } + } + }); + } + onChange(callback) { + this.onChanges.push(callback); + } + setState(value3) { + value3 ? this.show() : this.hide(); + } + getState() { + return this.state; + } + toggle() { + this.el.isConnected && this.el.togglePopover(); + } + show() { + this.el.isConnected && this.el.showPopover(); + } + hide() { + this.el.isConnected && this.el.hidePopover(); + } + }; + function closeOtherOpenPopovers(el, scope) { + if (!currentlyOpenPopoversByScope.has(scope)) return; + currentlyOpenPopoversByScope.get(scope).forEach((popoverEl) => { + if (el.contains(popoverEl) || popoverEl.contains(el)) return; + popoverEl.hidePopover(); + }); + } + function closeOnClickOutside(el, except, controller) { + document.addEventListener("click", (e) => { + if (el.contains(e.target) || except.includes(e.target)) return; + el.hidePopover(); + }, { signal: controller.signal }); + } + function closeOnFocusAway(el, except, controller) { + document.addEventListener("focusin", (e) => { + if (el.contains(e.target) || except.includes(e.target)) return; + controller.abort(); + el.hidePopover(); + }, { + // Without "capture: true", when you focus away from the popover onto an element that triggers a popover + // on focus (a tooltip), it will focus back this popover's trigger instead of keeping focus on the tooltip button. + // It does this because only one popover can be open at a time, so focusing the tooltip, opens a popover, closing this one, + // which will trigger the "focus back" behavior. + capture: true, + signal: controller.signal + }); + } + function closeOnEscape(el, except, controller) { + document.addEventListener("keydown", (e) => { + if (e.key !== "Escape") return; + el.hidePopover(); + }, { signal: controller.signal }); + } + + // js/mixins/disableable.js + var Disableable = class extends Mixin { + boot({ options }) { + options({ + disableWithParent: true + }); + this.onChanges = []; + Object.defineProperty(this.el, "disabled", { + get: () => { + return this.el.hasAttribute("disabled"); + }, + set: (value3) => { + if (value3) { + this.el.setAttribute("disabled", ""); + } else { + this.el.removeAttribute("disabled"); + } + } + }); + if (this.el.hasAttribute("disabled")) { + this.el.disabled = true; + } else if (this.options().disableWithParent && this.el.parentElement?.closest("[disabled]")) { + this.el.disabled = true; + } + let observer = new MutationObserver((mutations) => { + this.onChanges.forEach((i) => i(this.el.disabled)); + }); + observer.observe(this.el, { attributeFilter: ["disabled"] }); + } + onChange(callback) { + this.onChanges.push(callback); + } + onInitAndChange(callback) { + callback(this.el.disabled); + this.onChanges.push(callback); + } + enabled(callback) { + return (...args) => { + if (this.el.disabled) return; + return callback(...args); + }; + } + disabled(callback) { + return (...args) => { + if (!this.el.disabled) return; + return callback(...args); + }; + } + isDisabled() { + return this.el.disabled; + } + }; + + // node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs + var min = Math.min; + var max = Math.max; + var round = Math.round; + var floor = Math.floor; + var createCoords = (v) => ({ + x: v, + y: v + }); + var oppositeSideMap = { + left: "right", + right: "left", + bottom: "top", + top: "bottom" + }; + var oppositeAlignmentMap = { + start: "end", + end: "start" + }; + function clamp(start, value3, end) { + return max(start, min(value3, end)); + } + function evaluate(value3, param) { + return typeof value3 === "function" ? value3(param) : value3; + } + function getSide(placement) { + return placement.split("-")[0]; + } + function getAlignment(placement) { + return placement.split("-")[1]; + } + function getOppositeAxis(axis) { + return axis === "x" ? "y" : "x"; + } + function getAxisLength(axis) { + return axis === "y" ? "height" : "width"; + } + function getSideAxis(placement) { + return ["top", "bottom"].includes(getSide(placement)) ? "y" : "x"; + } + function getAlignmentAxis(placement) { + return getOppositeAxis(getSideAxis(placement)); + } + function getAlignmentSides(placement, rects, rtl) { + if (rtl === void 0) { + rtl = false; + } + const alignment = getAlignment(placement); + const alignmentAxis = getAlignmentAxis(placement); + const length = getAxisLength(alignmentAxis); + let mainAlignmentSide = alignmentAxis === "x" ? alignment === (rtl ? "end" : "start") ? "right" : "left" : alignment === "start" ? "bottom" : "top"; + if (rects.reference[length] > rects.floating[length]) { + mainAlignmentSide = getOppositePlacement(mainAlignmentSide); + } + return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)]; + } + function getExpandedPlacements(placement) { + const oppositePlacement = getOppositePlacement(placement); + return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; + } + function getOppositeAlignmentPlacement(placement) { + return placement.replace(/start|end/g, (alignment) => oppositeAlignmentMap[alignment]); + } + function getSideList(side, isStart, rtl) { + const lr = ["left", "right"]; + const rl = ["right", "left"]; + const tb = ["top", "bottom"]; + const bt = ["bottom", "top"]; + switch (side) { + case "top": + case "bottom": + if (rtl) return isStart ? rl : lr; + return isStart ? lr : rl; + case "left": + case "right": + return isStart ? tb : bt; + default: + return []; + } + } + function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { + const alignment = getAlignment(placement); + let list = getSideList(getSide(placement), direction === "start", rtl); + if (alignment) { + list = list.map((side) => side + "-" + alignment); + if (flipAlignment) { + list = list.concat(list.map(getOppositeAlignmentPlacement)); + } + } + return list; + } + function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, (side) => oppositeSideMap[side]); + } + function expandPaddingObject(padding) { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + ...padding + }; + } + function getPaddingObject(padding) { + return typeof padding !== "number" ? expandPaddingObject(padding) : { + top: padding, + right: padding, + bottom: padding, + left: padding + }; + } + function rectToClientRect(rect) { + const { + x, + y, + width, + height + } = rect; + return { + width, + height, + top: y, + left: x, + right: x + width, + bottom: y + height, + x, + y + }; + } + + // node_modules/@floating-ui/core/dist/floating-ui.core.mjs + function computeCoordsFromPlacement(_ref, placement, rtl) { + let { + reference, + floating + } = _ref; + const sideAxis = getSideAxis(placement); + const alignmentAxis = getAlignmentAxis(placement); + const alignLength = getAxisLength(alignmentAxis); + const side = getSide(placement); + const isVertical = sideAxis === "y"; + const commonX = reference.x + reference.width / 2 - floating.width / 2; + const commonY = reference.y + reference.height / 2 - floating.height / 2; + const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; + let coords; + switch (side) { + case "top": + coords = { + x: commonX, + y: reference.y - floating.height + }; + break; + case "bottom": + coords = { + x: commonX, + y: reference.y + reference.height + }; + break; + case "right": + coords = { + x: reference.x + reference.width, + y: commonY + }; + break; + case "left": + coords = { + x: reference.x - floating.width, + y: commonY + }; + break; + default: + coords = { + x: reference.x, + y: reference.y + }; + } + switch (getAlignment(placement)) { + case "start": + coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); + break; + case "end": + coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); + break; + } + return coords; + } + var computePosition = async (reference, floating, config) => { + const { + placement = "bottom", + strategy = "absolute", + middleware = [], + platform: platform2 + } = config; + const validMiddleware = middleware.filter(Boolean); + const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(floating)); + let rects = await platform2.getElementRects({ + reference, + floating, + strategy + }); + let { + x, + y + } = computeCoordsFromPlacement(rects, placement, rtl); + let statefulPlacement = placement; + let middlewareData = {}; + let resetCount = 0; + for (let i = 0; i < validMiddleware.length; i++) { + const { + name, + fn + } = validMiddleware[i]; + const { + x: nextX, + y: nextY, + data, + reset + } = await fn({ + x, + y, + initialPlacement: placement, + placement: statefulPlacement, + strategy, + middlewareData, + rects, + platform: platform2, + elements: { + reference, + floating + } + }); + x = nextX != null ? nextX : x; + y = nextY != null ? nextY : y; + middlewareData = { + ...middlewareData, + [name]: { + ...middlewareData[name], + ...data + } + }; + if (reset && resetCount <= 50) { + resetCount++; + if (typeof reset === "object") { + if (reset.placement) { + statefulPlacement = reset.placement; + } + if (reset.rects) { + rects = reset.rects === true ? await platform2.getElementRects({ + reference, + floating, + strategy + }) : reset.rects; + } + ({ + x, + y + } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); + } + i = -1; + } + } + return { + x, + y, + placement: statefulPlacement, + strategy, + middlewareData + }; + }; + async function detectOverflow(state, options) { + var _await$platform$isEle; + if (options === void 0) { + options = {}; + } + const { + x, + y, + platform: platform2, + rects, + elements, + strategy + } = state; + const { + boundary = "clippingAncestors", + rootBoundary = "viewport", + elementContext = "floating", + altBoundary = false, + padding = 0 + } = evaluate(options, state); + const paddingObject = getPaddingObject(padding); + const altContext = elementContext === "floating" ? "reference" : "floating"; + const element2 = elements[altBoundary ? altContext : elementContext]; + const clippingClientRect = rectToClientRect(await platform2.getClippingRect({ + element: ((_await$platform$isEle = await (platform2.isElement == null ? void 0 : platform2.isElement(element2))) != null ? _await$platform$isEle : true) ? element2 : element2.contextElement || await (platform2.getDocumentElement == null ? void 0 : platform2.getDocumentElement(elements.floating)), + boundary, + rootBoundary, + strategy + })); + const rect = elementContext === "floating" ? { + x, + y, + width: rects.floating.width, + height: rects.floating.height + } : rects.reference; + const offsetParent = await (platform2.getOffsetParent == null ? void 0 : platform2.getOffsetParent(elements.floating)); + const offsetScale = await (platform2.isElement == null ? void 0 : platform2.isElement(offsetParent)) ? await (platform2.getScale == null ? void 0 : platform2.getScale(offsetParent)) || { + x: 1, + y: 1 + } : { + x: 1, + y: 1 + }; + const elementClientRect = rectToClientRect(platform2.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform2.convertOffsetParentRelativeRectToViewportRelativeRect({ + elements, + rect, + offsetParent, + strategy + }) : rect); + return { + top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, + bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, + left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, + right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x + }; + } + var flip = function(options) { + if (options === void 0) { + options = {}; + } + return { + name: "flip", + options, + async fn(state) { + var _middlewareData$arrow, _middlewareData$flip; + const { + placement, + middlewareData, + rects, + initialPlacement, + platform: platform2, + elements + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = true, + fallbackPlacements: specifiedFallbackPlacements, + fallbackStrategy = "bestFit", + fallbackAxisSideDirection = "none", + flipAlignment = true, + ...detectOverflowOptions + } = evaluate(options, state); + if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + const side = getSide(placement); + const initialSideAxis = getSideAxis(initialPlacement); + const isBasePlacement = getSide(initialPlacement) === initialPlacement; + const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)); + const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); + const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== "none"; + if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { + fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); + } + const placements2 = [initialPlacement, ...fallbackPlacements]; + const overflow = await detectOverflow(state, detectOverflowOptions); + const overflows = []; + let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; + if (checkMainAxis) { + overflows.push(overflow[side]); + } + if (checkCrossAxis) { + const sides2 = getAlignmentSides(placement, rects, rtl); + overflows.push(overflow[sides2[0]], overflow[sides2[1]]); + } + overflowsData = [...overflowsData, { + placement, + overflows + }]; + if (!overflows.every((side2) => side2 <= 0)) { + var _middlewareData$flip2, _overflowsData$filter; + const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; + const nextPlacement = placements2[nextIndex]; + if (nextPlacement) { + return { + data: { + index: nextIndex, + overflows: overflowsData + }, + reset: { + placement: nextPlacement + } + }; + } + let resetPlacement = (_overflowsData$filter = overflowsData.filter((d) => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; + if (!resetPlacement) { + switch (fallbackStrategy) { + case "bestFit": { + var _overflowsData$filter2; + const placement2 = (_overflowsData$filter2 = overflowsData.filter((d) => { + if (hasFallbackAxisSideDirection) { + const currentSideAxis = getSideAxis(d.placement); + return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal + // reading directions favoring greater width. + currentSideAxis === "y"; + } + return true; + }).map((d) => [d.placement, d.overflows.filter((overflow2) => overflow2 > 0).reduce((acc, overflow2) => acc + overflow2, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; + if (placement2) { + resetPlacement = placement2; + } + break; + } + case "initialPlacement": + resetPlacement = initialPlacement; + break; + } + } + if (placement !== resetPlacement) { + return { + reset: { + placement: resetPlacement + } + }; + } + } + return {}; + } + }; + }; + async function convertValueToCoords(state, options) { + const { + placement, + platform: platform2, + elements + } = state; + const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)); + const side = getSide(placement); + const alignment = getAlignment(placement); + const isVertical = getSideAxis(placement) === "y"; + const mainAxisMulti = ["left", "top"].includes(side) ? -1 : 1; + const crossAxisMulti = rtl && isVertical ? -1 : 1; + const rawValue = evaluate(options, state); + let { + mainAxis, + crossAxis, + alignmentAxis + } = typeof rawValue === "number" ? { + mainAxis: rawValue, + crossAxis: 0, + alignmentAxis: null + } : { + mainAxis: rawValue.mainAxis || 0, + crossAxis: rawValue.crossAxis || 0, + alignmentAxis: rawValue.alignmentAxis + }; + if (alignment && typeof alignmentAxis === "number") { + crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis; + } + return isVertical ? { + x: crossAxis * crossAxisMulti, + y: mainAxis * mainAxisMulti + } : { + x: mainAxis * mainAxisMulti, + y: crossAxis * crossAxisMulti + }; + } + var offset = function(options) { + if (options === void 0) { + options = 0; + } + return { + name: "offset", + options, + async fn(state) { + var _middlewareData$offse, _middlewareData$arrow; + const { + x, + y, + placement, + middlewareData + } = state; + const diffCoords = await convertValueToCoords(state, options); + if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + return { + x: x + diffCoords.x, + y: y + diffCoords.y, + data: { + ...diffCoords, + placement + } + }; + } + }; + }; + var shift = function(options) { + if (options === void 0) { + options = {}; + } + return { + name: "shift", + options, + async fn(state) { + const { + x, + y, + placement + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = false, + limiter = { + fn: (_ref) => { + let { + x: x2, + y: y2 + } = _ref; + return { + x: x2, + y: y2 + }; + } + }, + ...detectOverflowOptions + } = evaluate(options, state); + const coords = { + x, + y + }; + const overflow = await detectOverflow(state, detectOverflowOptions); + const crossAxis = getSideAxis(getSide(placement)); + const mainAxis = getOppositeAxis(crossAxis); + let mainAxisCoord = coords[mainAxis]; + let crossAxisCoord = coords[crossAxis]; + if (checkMainAxis) { + const minSide = mainAxis === "y" ? "top" : "left"; + const maxSide = mainAxis === "y" ? "bottom" : "right"; + const min2 = mainAxisCoord + overflow[minSide]; + const max2 = mainAxisCoord - overflow[maxSide]; + mainAxisCoord = clamp(min2, mainAxisCoord, max2); + } + if (checkCrossAxis) { + const minSide = crossAxis === "y" ? "top" : "left"; + const maxSide = crossAxis === "y" ? "bottom" : "right"; + const min2 = crossAxisCoord + overflow[minSide]; + const max2 = crossAxisCoord - overflow[maxSide]; + crossAxisCoord = clamp(min2, crossAxisCoord, max2); + } + const limitedCoords = limiter.fn({ + ...state, + [mainAxis]: mainAxisCoord, + [crossAxis]: crossAxisCoord + }); + return { + ...limitedCoords, + data: { + x: limitedCoords.x - x, + y: limitedCoords.y - y, + enabled: { + [mainAxis]: checkMainAxis, + [crossAxis]: checkCrossAxis + } + } + }; + } + }; + }; + var size = function(options) { + if (options === void 0) { + options = {}; + } + return { + name: "size", + options, + async fn(state) { + var _state$middlewareData, _state$middlewareData2; + const { + placement, + rects, + platform: platform2, + elements + } = state; + const { + apply: apply3 = () => { + }, + ...detectOverflowOptions + } = evaluate(options, state); + const overflow = await detectOverflow(state, detectOverflowOptions); + const side = getSide(placement); + const alignment = getAlignment(placement); + const isYAxis = getSideAxis(placement) === "y"; + const { + width, + height + } = rects.floating; + let heightSide; + let widthSide; + if (side === "top" || side === "bottom") { + heightSide = side; + widthSide = alignment === (await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)) ? "start" : "end") ? "left" : "right"; + } else { + widthSide = side; + heightSide = alignment === "end" ? "top" : "bottom"; + } + const maximumClippingHeight = height - overflow.top - overflow.bottom; + const maximumClippingWidth = width - overflow.left - overflow.right; + const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight); + const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth); + const noShift = !state.middlewareData.shift; + let availableHeight = overflowAvailableHeight; + let availableWidth = overflowAvailableWidth; + if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) { + availableWidth = maximumClippingWidth; + } + if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) { + availableHeight = maximumClippingHeight; + } + if (noShift && !alignment) { + const xMin = max(overflow.left, 0); + const xMax = max(overflow.right, 0); + const yMin = max(overflow.top, 0); + const yMax = max(overflow.bottom, 0); + if (isYAxis) { + availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right)); + } else { + availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom)); + } + } + await apply3({ + ...state, + availableWidth, + availableHeight + }); + const nextDimensions = await platform2.getDimensions(elements.floating); + if (width !== nextDimensions.width || height !== nextDimensions.height) { + return { + reset: { + rects: true + } + }; + } + return {}; + } + }; + }; + + // node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.mjs + function hasWindow() { + return typeof window !== "undefined"; + } + function getNodeName(node) { + if (isNode(node)) { + return (node.nodeName || "").toLowerCase(); + } + return "#document"; + } + function getWindow(node) { + var _node$ownerDocument; + return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; + } + function getDocumentElement(node) { + var _ref; + return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; + } + function isNode(value3) { + if (!hasWindow()) { + return false; + } + return value3 instanceof Node || value3 instanceof getWindow(value3).Node; + } + function isElement(value3) { + if (!hasWindow()) { + return false; + } + return value3 instanceof Element || value3 instanceof getWindow(value3).Element; + } + function isHTMLElement(value3) { + if (!hasWindow()) { + return false; + } + return value3 instanceof HTMLElement || value3 instanceof getWindow(value3).HTMLElement; + } + function isShadowRoot(value3) { + if (!hasWindow() || typeof ShadowRoot === "undefined") { + return false; + } + return value3 instanceof ShadowRoot || value3 instanceof getWindow(value3).ShadowRoot; + } + function isOverflowElement(element2) { + const { + overflow, + overflowX, + overflowY, + display + } = getComputedStyle2(element2); + return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !["inline", "contents"].includes(display); + } + function isTableElement(element2) { + return ["table", "td", "th"].includes(getNodeName(element2)); + } + function isTopLayer(element2) { + return [":popover-open", ":modal"].some((selector) => { + try { + return element2.matches(selector); + } catch (e) { + return false; + } + }); + } + function isContainingBlock(elementOrCss) { + const webkit = isWebKit(); + const css = isElement(elementOrCss) ? getComputedStyle2(elementOrCss) : elementOrCss; + return ["transform", "translate", "scale", "rotate", "perspective"].some((value3) => css[value3] ? css[value3] !== "none" : false) || (css.containerType ? css.containerType !== "normal" : false) || !webkit && (css.backdropFilter ? css.backdropFilter !== "none" : false) || !webkit && (css.filter ? css.filter !== "none" : false) || ["transform", "translate", "scale", "rotate", "perspective", "filter"].some((value3) => (css.willChange || "").includes(value3)) || ["paint", "layout", "strict", "content"].some((value3) => (css.contain || "").includes(value3)); + } + function getContainingBlock(element2) { + let currentNode = getParentNode(element2); + while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { + if (isContainingBlock(currentNode)) { + return currentNode; + } else if (isTopLayer(currentNode)) { + return null; + } + currentNode = getParentNode(currentNode); + } + return null; + } + function isWebKit() { + if (typeof CSS === "undefined" || !CSS.supports) return false; + return CSS.supports("-webkit-backdrop-filter", "none"); + } + function isLastTraversableNode(node) { + return ["html", "body", "#document"].includes(getNodeName(node)); + } + function getComputedStyle2(element2) { + return getWindow(element2).getComputedStyle(element2); + } + function getNodeScroll(element2) { + if (isElement(element2)) { + return { + scrollLeft: element2.scrollLeft, + scrollTop: element2.scrollTop + }; + } + return { + scrollLeft: element2.scrollX, + scrollTop: element2.scrollY + }; + } + function getParentNode(node) { + if (getNodeName(node) === "html") { + return node; + } + const result = ( + // Step into the shadow DOM of the parent of a slotted node. + node.assignedSlot || // DOM Element detected. + node.parentNode || // ShadowRoot detected. + isShadowRoot(node) && node.host || // Fallback. + getDocumentElement(node) + ); + return isShadowRoot(result) ? result.host : result; + } + function getNearestOverflowAncestor(node) { + const parentNode = getParentNode(node); + if (isLastTraversableNode(parentNode)) { + return node.ownerDocument ? node.ownerDocument.body : node.body; + } + if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { + return parentNode; + } + return getNearestOverflowAncestor(parentNode); + } + function getOverflowAncestors(node, list, traverseIframes) { + var _node$ownerDocument2; + if (list === void 0) { + list = []; + } + if (traverseIframes === void 0) { + traverseIframes = true; + } + const scrollableAncestor = getNearestOverflowAncestor(node); + const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); + const win = getWindow(scrollableAncestor); + if (isBody) { + const frameElement = getFrameElement(win); + return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []); + } + return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); + } + function getFrameElement(win) { + return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; + } + + // node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs + function getCssDimensions(element2) { + const css = getComputedStyle2(element2); + let width = parseFloat(css.width) || 0; + let height = parseFloat(css.height) || 0; + const hasOffset = isHTMLElement(element2); + const offsetWidth = hasOffset ? element2.offsetWidth : width; + const offsetHeight = hasOffset ? element2.offsetHeight : height; + const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; + if (shouldFallback) { + width = offsetWidth; + height = offsetHeight; + } + return { + width, + height, + $: shouldFallback + }; + } + function unwrapElement(element2) { + return !isElement(element2) ? element2.contextElement : element2; + } + function getScale(element2) { + const domElement = unwrapElement(element2); + if (!isHTMLElement(domElement)) { + return createCoords(1); + } + const rect = domElement.getBoundingClientRect(); + const { + width, + height, + $ + } = getCssDimensions(domElement); + let x = ($ ? round(rect.width) : rect.width) / width; + let y = ($ ? round(rect.height) : rect.height) / height; + if (!x || !Number.isFinite(x)) { + x = 1; + } + if (!y || !Number.isFinite(y)) { + y = 1; + } + return { + x, + y + }; + } + var noOffsets = /* @__PURE__ */ createCoords(0); + function getVisualOffsets(element2) { + const win = getWindow(element2); + if (!isWebKit() || !win.visualViewport) { + return noOffsets; + } + return { + x: win.visualViewport.offsetLeft, + y: win.visualViewport.offsetTop + }; + } + function shouldAddVisualOffsets(element2, isFixed, floatingOffsetParent) { + if (isFixed === void 0) { + isFixed = false; + } + if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element2)) { + return false; + } + return isFixed; + } + function getBoundingClientRect(element2, includeScale, isFixedStrategy, offsetParent) { + if (includeScale === void 0) { + includeScale = false; + } + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + const clientRect = element2.getBoundingClientRect(); + const domElement = unwrapElement(element2); + let scale = createCoords(1); + if (includeScale) { + if (offsetParent) { + if (isElement(offsetParent)) { + scale = getScale(offsetParent); + } + } else { + scale = getScale(element2); + } + } + const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); + let x = (clientRect.left + visualOffsets.x) / scale.x; + let y = (clientRect.top + visualOffsets.y) / scale.y; + let width = clientRect.width / scale.x; + let height = clientRect.height / scale.y; + if (domElement) { + const win = getWindow(domElement); + const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; + let currentWin = win; + let currentIFrame = getFrameElement(currentWin); + while (currentIFrame && offsetParent && offsetWin !== currentWin) { + const iframeScale = getScale(currentIFrame); + const iframeRect = currentIFrame.getBoundingClientRect(); + const css = getComputedStyle2(currentIFrame); + const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; + const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; + x *= iframeScale.x; + y *= iframeScale.y; + width *= iframeScale.x; + height *= iframeScale.y; + x += left; + y += top; + currentWin = getWindow(currentIFrame); + currentIFrame = getFrameElement(currentWin); + } + } + return rectToClientRect({ + width, + height, + x, + y + }); + } + function getWindowScrollBarX(element2, rect) { + const leftScroll = getNodeScroll(element2).scrollLeft; + if (!rect) { + return getBoundingClientRect(getDocumentElement(element2)).left + leftScroll; + } + return rect.left + leftScroll; + } + function getHTMLOffset(documentElement, scroll, ignoreScrollbarX) { + if (ignoreScrollbarX === void 0) { + ignoreScrollbarX = false; + } + const htmlRect = documentElement.getBoundingClientRect(); + const x = htmlRect.left + scroll.scrollLeft - (ignoreScrollbarX ? 0 : ( + // RTL scrollbar. + getWindowScrollBarX(documentElement, htmlRect) + )); + const y = htmlRect.top + scroll.scrollTop; + return { + x, + y + }; + } + function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { + let { + elements, + rect, + offsetParent, + strategy + } = _ref; + const isFixed = strategy === "fixed"; + const documentElement = getDocumentElement(offsetParent); + const topLayer = elements ? isTopLayer(elements.floating) : false; + if (offsetParent === documentElement || topLayer && isFixed) { + return rect; + } + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + let scale = createCoords(1); + const offsets = createCoords(0); + const isOffsetParentAnElement = isHTMLElement(offsetParent); + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isHTMLElement(offsetParent)) { + const offsetRect = getBoundingClientRect(offsetParent); + scale = getScale(offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll, true) : createCoords(0); + return { + width: rect.width * scale.x, + height: rect.height * scale.y, + x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, + y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y + }; + } + function getClientRects(element2) { + return Array.from(element2.getClientRects()); + } + function getDocumentRect(element2) { + const html = getDocumentElement(element2); + const scroll = getNodeScroll(element2); + const body = element2.ownerDocument.body; + const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); + const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); + let x = -scroll.scrollLeft + getWindowScrollBarX(element2); + const y = -scroll.scrollTop; + if (getComputedStyle2(body).direction === "rtl") { + x += max(html.clientWidth, body.clientWidth) - width; + } + return { + width, + height, + x, + y + }; + } + function getViewportRect(element2, strategy) { + const win = getWindow(element2); + const html = getDocumentElement(element2); + const visualViewport = win.visualViewport; + let width = html.clientWidth; + let height = html.clientHeight; + let x = 0; + let y = 0; + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + const visualViewportBased = isWebKit(); + if (!visualViewportBased || visualViewportBased && strategy === "fixed") { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + return { + width, + height, + x, + y + }; + } + function getInnerBoundingClientRect(element2, strategy) { + const clientRect = getBoundingClientRect(element2, true, strategy === "fixed"); + const top = clientRect.top + element2.clientTop; + const left = clientRect.left + element2.clientLeft; + const scale = isHTMLElement(element2) ? getScale(element2) : createCoords(1); + const width = element2.clientWidth * scale.x; + const height = element2.clientHeight * scale.y; + const x = left * scale.x; + const y = top * scale.y; + return { + width, + height, + x, + y + }; + } + function getClientRectFromClippingAncestor(element2, clippingAncestor, strategy) { + let rect; + if (clippingAncestor === "viewport") { + rect = getViewportRect(element2, strategy); + } else if (clippingAncestor === "document") { + rect = getDocumentRect(getDocumentElement(element2)); + } else if (isElement(clippingAncestor)) { + rect = getInnerBoundingClientRect(clippingAncestor, strategy); + } else { + const visualOffsets = getVisualOffsets(element2); + rect = { + x: clippingAncestor.x - visualOffsets.x, + y: clippingAncestor.y - visualOffsets.y, + width: clippingAncestor.width, + height: clippingAncestor.height + }; + } + return rectToClientRect(rect); + } + function hasFixedPositionAncestor(element2, stopNode) { + const parentNode = getParentNode(element2); + if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { + return false; + } + return getComputedStyle2(parentNode).position === "fixed" || hasFixedPositionAncestor(parentNode, stopNode); + } + function getClippingElementAncestors(element2, cache) { + const cachedResult = cache.get(element2); + if (cachedResult) { + return cachedResult; + } + let result = getOverflowAncestors(element2, [], false).filter((el) => isElement(el) && getNodeName(el) !== "body"); + let currentContainingBlockComputedStyle = null; + const elementIsFixed = getComputedStyle2(element2).position === "fixed"; + let currentNode = elementIsFixed ? getParentNode(element2) : element2; + while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { + const computedStyle = getComputedStyle2(currentNode); + const currentNodeIsContaining = isContainingBlock(currentNode); + if (!currentNodeIsContaining && computedStyle.position === "fixed") { + currentContainingBlockComputedStyle = null; + } + const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === "static" && !!currentContainingBlockComputedStyle && ["absolute", "fixed"].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element2, currentNode); + if (shouldDropCurrentNode) { + result = result.filter((ancestor) => ancestor !== currentNode); + } else { + currentContainingBlockComputedStyle = computedStyle; + } + currentNode = getParentNode(currentNode); + } + cache.set(element2, result); + return result; + } + function getClippingRect(_ref) { + let { + element: element2, + boundary, + rootBoundary, + strategy + } = _ref; + const elementClippingAncestors = boundary === "clippingAncestors" ? isTopLayer(element2) ? [] : getClippingElementAncestors(element2, this._c) : [].concat(boundary); + const clippingAncestors = [...elementClippingAncestors, rootBoundary]; + const firstClippingAncestor = clippingAncestors[0]; + const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { + const rect = getClientRectFromClippingAncestor(element2, clippingAncestor, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromClippingAncestor(element2, firstClippingAncestor, strategy)); + return { + width: clippingRect.right - clippingRect.left, + height: clippingRect.bottom - clippingRect.top, + x: clippingRect.left, + y: clippingRect.top + }; + } + function getDimensions(element2) { + const { + width, + height + } = getCssDimensions(element2); + return { + width, + height + }; + } + function getRectRelativeToOffsetParent(element2, offsetParent, strategy) { + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + const isFixed = strategy === "fixed"; + const rect = getBoundingClientRect(element2, true, isFixed, offsetParent); + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + const offsets = createCoords(0); + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isOffsetParentAnElement) { + const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); + const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; + const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; + return { + x, + y, + width: rect.width, + height: rect.height + }; + } + function isStaticPositioned(element2) { + return getComputedStyle2(element2).position === "static"; + } + function getTrueOffsetParent(element2, polyfill) { + if (!isHTMLElement(element2) || getComputedStyle2(element2).position === "fixed") { + return null; + } + if (polyfill) { + return polyfill(element2); + } + let rawOffsetParent = element2.offsetParent; + if (getDocumentElement(element2) === rawOffsetParent) { + rawOffsetParent = rawOffsetParent.ownerDocument.body; + } + return rawOffsetParent; + } + function getOffsetParent(element2, polyfill) { + const win = getWindow(element2); + if (isTopLayer(element2)) { + return win; + } + if (!isHTMLElement(element2)) { + let svgOffsetParent = getParentNode(element2); + while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { + if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { + return svgOffsetParent; + } + svgOffsetParent = getParentNode(svgOffsetParent); + } + return win; + } + let offsetParent = getTrueOffsetParent(element2, polyfill); + while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) { + offsetParent = getTrueOffsetParent(offsetParent, polyfill); + } + if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { + return win; + } + return offsetParent || getContainingBlock(element2) || win; + } + var getElementRects = async function(data) { + const getOffsetParentFn = this.getOffsetParent || getOffsetParent; + const getDimensionsFn = this.getDimensions; + const floatingDimensions = await getDimensionsFn(data.floating); + return { + reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), + floating: { + x: 0, + y: 0, + width: floatingDimensions.width, + height: floatingDimensions.height + } + }; + }; + function isRTL2(element2) { + return getComputedStyle2(element2).direction === "rtl"; + } + var platform = { + convertOffsetParentRelativeRectToViewportRelativeRect, + getDocumentElement, + getClippingRect, + getOffsetParent, + getElementRects, + getClientRects, + getDimensions, + getScale, + isElement, + isRTL: isRTL2 + }; + function rectsAreEqual(a, b) { + return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; + } + function observeMove(element2, onMove) { + let io = null; + let timeoutId; + const root = getDocumentElement(element2); + function cleanup() { + var _io; + clearTimeout(timeoutId); + (_io = io) == null || _io.disconnect(); + io = null; + } + function refresh(skip, threshold) { + if (skip === void 0) { + skip = false; + } + if (threshold === void 0) { + threshold = 1; + } + cleanup(); + const elementRectForRootMargin = element2.getBoundingClientRect(); + const { + left, + top, + width, + height + } = elementRectForRootMargin; + if (!skip) { + onMove(); + } + if (!width || !height) { + return; + } + const insetTop = floor(top); + const insetRight = floor(root.clientWidth - (left + width)); + const insetBottom = floor(root.clientHeight - (top + height)); + const insetLeft = floor(left); + const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; + const options = { + rootMargin, + threshold: max(0, min(1, threshold)) || 1 + }; + let isFirstUpdate = true; + function handleObserve(entries) { + const ratio = entries[0].intersectionRatio; + if (ratio !== threshold) { + if (!isFirstUpdate) { + return refresh(); + } + if (!ratio) { + timeoutId = setTimeout(() => { + refresh(false, 1e-7); + }, 1e3); + } else { + refresh(false, ratio); + } + } + if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element2.getBoundingClientRect())) { + refresh(); + } + isFirstUpdate = false; + } + try { + io = new IntersectionObserver(handleObserve, { + ...options, + // Handle