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
parent
e211770312
commit
436d80a097
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; },
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
<x-ui.button wire:click="generate" action="generate">
|
||||
<x-heroicon-o-sparkles class="w-4 h-4" />
|
||||
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>
|
||||
</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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue