fix(FIX-13): 8 bugs — sidebar flash, mobile hamburger, AI insights, button overlay, QR modal collapse, dashboard always-active, bio themes

- BUG 1: blocking script in <head> reads localStorage before first paint; CSS rule html[data-sidebar-collapsed] .skel-sidebar{width:64px} eliminates width flash
- BUG 2: sidebar store gains mobileOpen/toggleMobile()/closeMobile(); hamburger calls toggleMobile(); backdrop and sidebar :style use mobileOpen; SPA navigated handler calls closeMobile()
- BUG 3: text-purple → text-accent in AI Insights view (text-purple is undefined)
- BUG 4: ui/button.blade.php — spinner is now absolute inset-0 overlay; content fades to opacity-40 via wire:loading.class instead of hiding
- BUG 5: insights generate button and QR modal save/cancel migrated to x-ui.button component
- BUG 6: QR modal Styling/Frame/Advanced sections replaced from <details> (destroyed by Livewire re-render) to Alpine x-data{open:false} divs with x-show — state survives wire:model.live updates
- BUG 7: updateActiveNav() now checks el.dataset.navExact; Dashboard <a> gets data-nav-exact="1" so it only activates on exact pathname match
- BUG 8: bio_pages.theme JSON applied in public show.blade.php via inline CSS vars (4 presets: dark/light/purple/blue); EditBioPage gains theme property with preset selector in modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 18:28:27 +02:00
parent e211770312
commit 436d80a097
10 changed files with 124 additions and 50 deletions

View File

@ -20,6 +20,8 @@ class EditBioPage extends ModalComponent
public bool $isPublished = false;
public string $theme = 'dark';
public function mount(string $bioUlid): void
{
$bio = BioPage::where('ulid', $bioUlid)->firstOrFail();
@ -31,6 +33,7 @@ class EditBioPage extends ModalComponent
$this->title = $bio->getTranslation('title', 'en') ?? '';
$this->slug = $bio->slug;
$this->isPublished = $bio->is_published;
$this->theme = is_array($bio->theme) ? ($bio->theme['preset'] ?? 'dark') : 'dark';
}
/** @return array<string, string> */
@ -40,6 +43,7 @@ class EditBioPage extends ModalComponent
'title' => 'required|max:200',
'slug' => 'nullable|alpha_dash|max:80',
'isPublished' => 'boolean',
'theme' => 'in:dark,light,purple,blue',
];
}
@ -55,6 +59,7 @@ class EditBioPage extends ModalComponent
'title' => ['en' => $this->title, 'de' => $this->title],
'slug' => $this->slug ?: $bio->slug,
'is_published' => $this->isPublished,
'theme' => ['preset' => $this->theme],
]);
$this->dispatch('bio-updated')->to(Index::class);

View File

@ -60,12 +60,23 @@ document.addEventListener('alpine:init', () => {
});
Alpine.store('sidebar', {
open: false,
mobileOpen: false,
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
init() { this._syncDataset(); },
_syncDataset() {
if (this.collapsed) {
document.documentElement.dataset.sidebarCollapsed = '1';
} else {
delete document.documentElement.dataset.sidebarCollapsed;
}
},
toggle() {
this.collapsed = ! this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
this._syncDataset();
window.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { collapsed: this.collapsed } }));
},
toggleMobile() { this.mobileOpen = ! this.mobileOpen; },
closeMobile() { this.mobileOpen = false; },
});
});

View File

@ -1,3 +1,13 @@
@php
$preset = is_array($page->theme) ? ($page->theme['preset'] ?? 'dark') : 'dark';
$themes = [
'dark' => ['bg' => '#0a0a0f', 'card' => '#111117', 'border' => 'rgba(255,255,255,0.08)', 'text' => '#f0f0f5', 'sub' => '#b8b8d0', 'accent' => '#7c3aed'],
'light' => ['bg' => '#ffffff', 'card' => '#f9fafb', 'border' => '#e5e7eb', 'text' => '#111827', 'sub' => '#6b7280', 'accent' => '#7c3aed'],
'purple' => ['bg' => '#13001a', 'card' => '#1e0028', 'border' => 'rgba(139,92,246,0.25)', 'text' => '#f0e8ff', 'sub' => '#c4b5fd', 'accent' => '#a855f7'],
'blue' => ['bg' => '#000d1a', 'card' => '#001c33', 'border' => 'rgba(59,130,246,0.25)', 'text' => '#e8f4ff', 'sub' => '#93c5fd', 'accent' => '#3b82f6'],
];
$t = $themes[$preset] ?? $themes['dark'];
@endphp
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
@ -5,21 +15,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $page->getTranslation('title', app()->getLocale()) }}</title>
@vite(['resources/css/app.css'])
<style>
body {
background: {{ $t['bg'] }};
color: {{ $t['text'] }};
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1rem;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
.bio-card {
background: {{ $t['card'] }};
border: 1px solid {{ $t['border'] }};
border-radius: 0.75rem;
padding: 1rem;
}
.bio-sub { color: {{ $t['sub'] }}; }
.bio-accent { color: {{ $t['accent'] }}; }
</style>
</head>
<body class="bg-gray-950 text-gray-100 min-h-screen flex flex-col items-center py-12">
<body>
<div class="w-full max-w-md">
<h1 class="text-2xl font-bold text-center mb-2">
{{ $page->getTranslation('title', app()->getLocale()) }}
</h1>
@if($page->getTranslation('description', app()->getLocale()))
<p class="text-gray-400 text-center mb-8">
<p class="bio-sub text-center mb-8">
{{ $page->getTranslation('description', app()->getLocale()) }}
</p>
@endif
<div class="space-y-3">
@foreach($page->blocks as $block)
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800">
<div class="text-sm text-gray-300">{{ $block->config['content'] ?? '' }}</div>
<div class="bio-card">
<div class="text-sm bio-sub">{{ $block->config['content'] ?? '' }}</div>
</div>
@endforeach
</div>

View File

@ -100,7 +100,7 @@
x-data="{ wsOpen: false }"
:class="$store.sidebar.collapsed ? 'w-16' : 'w-60'"
class="fixed inset-y-0 left-0 h-screen flex flex-col bg-s1 border-r border-white/[.06] z-40 -translate-x-full md:translate-x-0 transition-[width,transform] duration-200 ease-in-out"
:style="$store.sidebar?.open ? 'transform: translateX(0)' : null"
:style="$store.sidebar?.mobileOpen ? 'transform: translateX(0)' : null"
>
<!-- Header: Logo + Workspace Switcher -->
<div class="flex-shrink-0 relative" @click.outside="wsOpen = false">
@ -183,6 +183,7 @@
href="{{ $url }}"
wire:navigate
data-nav-link="{{ $item['route'] }}"
@if(in_array($item['route'], ['w.dashboard', 'dashboard'])) data-nav-exact="1" @endif
:title="$store.sidebar.collapsed ? '{{ $item['label'] }}' : ''"
:class="$store.sidebar.collapsed ? 'justify-center px-2 gap-0' : 'gap-3 px-3'"
class="flex items-center py-2 rounded-lg text-sm transition-colors group

View File

@ -13,7 +13,7 @@
<!-- Mobile hamburger -->
<button
class="md:hidden text-t2 hover:text-t1 transition-colors p-1 -ml-1"
@click="$store.sidebar.open = !$store.sidebar.open"
@click="$store.sidebar.toggleMobile()"
aria-label="Toggle sidebar"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5">

View File

@ -21,16 +21,15 @@ $cls = $base . ' ' . ($variants[$variant] ?? $variants['primary']);
{{ $attributes->merge(['type' => $type, 'class' => $cls]) }}
@if($action) wire:loading.attr="disabled" wire:target="{{ $action }}" @endif
>
<span @if($action) wire:loading.remove wire:target="{{ $action }}" @endif class="flex items-center gap-2">
<span @if($action) wire:loading.class="opacity-40" wire:target="{{ $action }}" @endif class="flex items-center gap-2">
{{ $slot->isNotEmpty() ? $slot : $label }}
</span>
@if($action)
<span wire:loading wire:target="{{ $action }}" class="flex items-center gap-2">
<span wire:loading wire:target="{{ $action }}" class="absolute inset-0 flex items-center justify-center">
<svg class="animate-spin h-4 w-4 flex-shrink-0" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
</svg>
{{ $loadingLabel }}
</span>
@endif
</button>

View File

@ -15,6 +15,17 @@
<meta name="theme-color" content="#0a0a0f" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<!-- Sidebar-init: set collapsed attribute before first paint eliminates width flash -->
<script>
(function () {
try {
if (localStorage.getItem('sidebarCollapsed') === 'true') {
document.documentElement.dataset.sidebarCollapsed = '1';
}
} catch (e) {}
})();
</script>
<!-- Theme-init: runs synchronously before any paint eliminates FOUC -->
<script>
(function () {
@ -76,6 +87,7 @@
animation: nim-spin 0.55s linear infinite;
z-index: 10;
}
html[data-sidebar-collapsed] .skel-sidebar { width: 64px; }
body.app-ready .app-spinner,
body.app-ready .skel-sidebar,
body.app-ready .skel-topbar { display: none; }
@ -95,14 +107,14 @@
<!-- Mobile Sidebar Overlay -->
<div
class="fixed inset-0 z-30 bg-black/60 md:hidden"
x-show="$store.sidebar?.open"
x-show="$store.sidebar?.mobileOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="$store.sidebar.open = false"
@click="$store.sidebar.closeMobile()"
aria-hidden="true"
x-cloak
></div>
@ -145,7 +157,7 @@
// Close mobile sidebar after SPA navigation
document.addEventListener('livewire:navigated', function () {
if (window.Alpine && Alpine.store('sidebar')) {
Alpine.store('sidebar').open = false;
Alpine.store('sidebar').closeMobile();
}
});
@ -154,7 +166,7 @@
var path = window.location.pathname;
document.querySelectorAll('[data-nav-link]').forEach(function (el) {
var href = new URL(el.href, location.origin).pathname;
var isActive = href && (path === href || path.startsWith(href + '/'));
var isActive = href && (path === href || (! el.dataset.navExact && path.startsWith(href + '/')));
el.classList.toggle('bg-blue/10', isActive);
el.classList.toggle('text-blue', isActive);
el.classList.toggle('text-t2', !isActive);

View File

@ -54,9 +54,15 @@
@endif
</div>
<details class="border border-white/[.06] rounded-lg mb-2">
<summary class="px-4 py-3 text-sm font-medium text-t1 cursor-pointer select-none">Styling</summary>
<div class="px-4 pb-4 space-y-3 mt-2">
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-2">
<button type="button" @click="open = !open"
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
<span>Styling</span>
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
</svg>
</button>
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-t2 mb-1">Foreground</label>
@ -86,11 +92,17 @@
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
</div>
</div>
</details>
</div>
<details class="border border-white/[.06] rounded-lg mb-2">
<summary class="px-4 py-3 text-sm font-medium text-t1 cursor-pointer select-none">Frame</summary>
<div class="px-4 pb-4 space-y-3 mt-2">
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-2">
<button type="button" @click="open = !open"
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
<span>Frame</span>
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
</svg>
</button>
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
<select wire:model.live="frame" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
<option value="none">No frame</option>
<option value="label-bottom">Label bottom</option>
@ -103,11 +115,17 @@
<input wire:model.live="frameColor" type="color" class="w-full h-9 rounded cursor-pointer border border-white/[.06]">
</div>
</div>
</details>
</div>
<details class="border border-white/[.06] rounded-lg mb-4">
<summary class="px-4 py-3 text-sm font-medium text-t1 cursor-pointer select-none">Advanced</summary>
<div class="px-4 pb-4 space-y-3 mt-2">
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-4">
<button type="button" @click="open = !open"
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
<span>Advanced</span>
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
</svg>
</button>
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
<label class="flex items-center gap-2 text-sm text-t2 cursor-pointer">
<input wire:model.live="isDynamic" type="checkbox"> Dynamic QR (target changeable later)
</label>
@ -122,18 +140,11 @@
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
</div>
</div>
</details>
</div>
<div class="flex gap-3">
<button wire:click="save" wire:loading.attr="disabled" wire:target="save"
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 disabled:opacity-60 transition-opacity">
<span wire:loading.remove wire:target="save">Create QR Code</span>
<span wire:loading wire:target="save">Creating...</span>
</button>
<button wire:click="closeModal" type="button"
class="px-4 py-2.5 bg-s2 text-t2 text-sm font-medium rounded-lg hover:text-t1 transition-colors border border-white/[.06]">
Cancel
</button>
<x-ui.button wire:click="save" action="save" label="Create QR Code" class="flex-1" />
<x-ui.button wire:click="closeModal" variant="secondary" label="Cancel" />
</div>
</div>

View File

@ -18,6 +18,20 @@
@error('slug')<p class="mt-1 text-xs text-red">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-xs font-medium text-t2 mb-2">Theme</label>
<div class="flex gap-2 flex-wrap">
@foreach(['dark' => ['#0a0a0f', '#f0f0f5'], 'light' => ['#ffffff', '#111827'], 'purple' => ['#13001a', '#f0e8ff'], 'blue' => ['#000d1a', '#e8f4ff']] as $preset => [$bg, $fg])
<button type="button" wire:click="$set('theme', '{{ $preset }}')"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg border text-xs font-medium transition-colors
{{ $theme === $preset ? 'border-blue text-t1' : 'border-white/[.06] text-t2 hover:border-white/20' }}">
<span class="w-4 h-4 rounded-full border border-white/20 flex-shrink-0" style="background: {{ $bg }}"></span>
{{ ucfirst($preset) }}
</button>
@endforeach
</div>
</div>
<div class="flex items-center gap-3">
<label class="relative inline-flex items-center cursor-pointer">
<input wire:model="isPublished" type="checkbox" class="sr-only peer">

View File

@ -4,25 +4,15 @@
<h1 class="text-2xl font-bold text-t1">AI Insights</h1>
<p class="text-sm text-t3 mt-1">Analyze your link performance with AI</p>
</div>
<button wire:click="generate" wire:loading.attr="disabled" wire:target="generate"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-60 flex items-center gap-2">
<span wire:loading.remove wire:target="generate">
<x-heroicon-o-sparkles class="w-4 h-4 inline mr-1" />
Generate Insights
</span>
<span wire:loading wire:target="generate" class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
Analyzing…
</span>
</button>
<x-ui.button wire:click="generate" action="generate">
<x-heroicon-o-sparkles class="w-4 h-4" />
Generate Insights
</x-ui.button>
</div>
@if($insights)
<div class="p-5 bg-s1 border border-white/[.06] rounded-xl">
<div class="flex items-center gap-2 mb-3 text-purple text-xs font-medium">
<div class="flex items-center gap-2 mb-3 text-accent text-xs font-medium">
<x-heroicon-o-sparkles class="w-3.5 h-3.5" />
AI Analysis
</div>