Fix: chart.js race, burger desktop visibility, modal glass-themed

User-reported issues + browser-verified:

1. **Chart.js race condition** — inline @push('scripts') ran before
   the @vite type=module script that imports Chart. Wrapped Chart init
   in a poll loop that retries every 30ms until window.Chart resolves.
   Re-fires on livewire:navigated for SPA navigation.

2. **Hamburger always visible on desktop** — .clu-icon-btn defined
   display:grid AFTER .clu-burger {display:none}. Chained selector
   '.clu-burger.clu-icon-btn' (specificity 0,2,0) now wins both at
   default and inside the <920px media query.

3. **Modal in CluPilot glass style** —
   - app/Livewire/Modals/ConfirmDelete.php (class extends ModalComponent)
     with title/message/confirmLabel/confirmEvent/payload props,
     confirm() dispatches the named event + closes the modal.
   - resources/views/livewire/modals/confirm-delete.blade.php
     renders clu-modal-card with icon + title/message head + foot
     containing cancel (clu-ghost-btn) + delete (clu-btn-primary
     clu-btn-danger with red gradient).
   - CSS overrides for the wire-elements default Tailwind v2 markup:
     bg-gray-500 backdrop dimmer, transparent modal-container, z-index
     bumped to 100 so it sits above sticky topbar + sidebar.
   - @source hints in app.css so Tailwind v4 scans
     vendor/wire-elements/modal blade templates and generates the
     sm:inline-block / sm:h-screen utilities the modal needs to center.
   - Demo: Settings → Danger Zone → 'Workspace löschen' opens modal.

4. **AppServiceProvider Vite::usePreloadTagAttributes(false)** kept.

5. Browser console clean across dashboard / sites / sites/{id} /
   settings + open modal (verified via Chrome MCP).
master
Boban Blaskovic 2026-05-31 23:33:07 +02:00
parent 665ca4e40b
commit da90b39085
6 changed files with 195 additions and 41 deletions

View File

@ -0,0 +1,49 @@
<?php
namespace App\Livewire\Modals;
use LivewireUI\Modal\ModalComponent;
/**
* Reusable confirm-delete modal.
*
* Open from anywhere:
* <button wire:click="$dispatch('openModal', {
* component: 'modals.confirm-delete',
* arguments: {
* title: 'Site löschen?',
* message: 'Diese Aktion ist nicht umkehrbar.',
* confirmEvent: 'site-delete-confirmed',
* payload: {{ $site->id }}
* }
* })">Löschen</button>
*
* Catch the result in the parent Livewire component:
* #[On('site-delete-confirmed')]
* public function handleDelete(int $id): void { ... }
*/
class ConfirmDelete extends ModalComponent
{
public string $title = 'Wirklich löschen?';
public string $message = 'Diese Aktion kann nicht rückgängig gemacht werden.';
public string $confirmLabel = 'Löschen';
public string $cancelLabel = 'Abbrechen';
public string $confirmEvent = 'confirmed';
public mixed $payload = null;
public function confirm(): void
{
$this->dispatch($this->confirmEvent, payload: $this->payload);
$this->closeModal();
}
public static function modalMaxWidth(): string
{
return 'md'; // sm | md | lg | xl | 2xl | 3xl | ...
}
public function render()
{
return view('livewire.modals.confirm-delete');
}
}

View File

@ -1,5 +1,10 @@
@import "tailwindcss";
/* Pull Tailwind utilities used by 3rd-party Blade templates so Tailwind v4
generates them. Without these @source hints the scanner skips vendor/. */
@source "../../vendor/wire-elements/modal/resources/views/**/*.blade.php";
@source "../../vendor/livewire/livewire/src/**/*.blade.php";
/*
CluPilot design tokens sourced from templates/akutell/* (light glass).
*/
@ -661,8 +666,9 @@
display: none;
}
/* Hamburger button in topbar (mobile only) */
.clu-burger { display: none; }
/* Hamburger button in topbar (mobile only).
Chained selector to outrank .clu-icon-btn {display:grid}. */
.clu-burger.clu-icon-btn { display: none; }
@media (max-width: 920px) {
.clu-app-grid { grid-template-columns: 1fr; }
@ -678,7 +684,7 @@
}
.clu-sidebar.open { transform: translateX(0); }
.clu-sidebar-backdrop.open { display: block; }
.clu-burger { display: grid; }
.clu-burger.clu-icon-btn { display: grid; }
}
.clu-brand-row { display: flex; align-items: center; gap: 10px; padding: 2px 4px; }
.clu-brand-name { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }
@ -1137,6 +1143,66 @@
.clu-log-level.error { color: #f05252; }
.clu-log-msg { color: #cccddd; flex: 1; word-break: break-all; }
/* ───── Modal (wire-elements/modal v3) ───── */
/* The package's modal.blade.php uses Tailwind v2 classes (bg-gray-500,
bg-white, opacity-75) that don't render with our custom v4 theme.
Override the structural pieces so the modal sits on a dim backdrop
with our glass-themed card body. */
/* Backdrop dimmer (.fixed.inset-0.transition-all.transform > .absolute.inset-0) */
body > [wire\:id] .fixed.inset-0.transition-all.transform > .absolute.inset-0,
body > [wire\:id] .fixed.inset-0.transition-all.transform > .bg-gray-500 {
background-color: rgba(20,22,35,0.55) !important;
opacity: 1 !important;
}
/* Modal container chrome strip white bg/shadow, let clu-modal-card
drive its own glass look. */
#modal-container {
background-color: transparent !important;
box-shadow: none !important;
border-radius: var(--radius) !important;
overflow: visible !important;
}
/* Lift modal wrapper above sticky topbar (z:20) + sidebar (z:30) */
body > [wire\:id] > .fixed.inset-0.z-10 { z-index: 100 !important; }
/* Our actual styled card */
.clu-modal-card {
background: var(--color-glass-strong);
backdrop-filter: blur(28px) saturate(175%);
-webkit-backdrop-filter: blur(28px) saturate(175%);
border: 1px solid var(--color-glass-border);
border-radius: var(--radius);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
padding: 22px 24px 18px;
color: var(--color-fg);
display: flex; flex-direction: column; gap: 18px;
}
.clu-modal-head { display: flex; gap: 14px; align-items: flex-start; }
.clu-modal-ico {
width: 40px; height: 40px;
border-radius: 12px;
background: var(--color-danger-soft);
color: var(--color-danger);
display: grid; place-items: center;
flex: none;
border: 1px solid rgba(219,59,59,0.25);
}
.clu-modal-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.clu-modal-msg { margin: 4px 0 0; font-size: 13px; color: var(--color-muted); line-height: 1.5; }
.clu-modal-foot {
display: flex; gap: 8px; justify-content: flex-end;
padding-top: 14px;
border-top: 1px solid var(--color-hairline);
}
/* Danger variant of primary button */
.clu-btn-danger {
background: linear-gradient(180deg, #e25555, #db3b3b);
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset, 0 6px 16px -5px rgba(219,59,59,0.55);
}
.clu-btn-danger:hover { filter: brightness(1.05); }
/* File tree */
.clu-file-tree {
font-family: var(--font-mono);

View File

@ -0,0 +1,5 @@
sm:max-w-md
md:max-w-xl
lg:max-w-3xl
xl:max-w-5xl
2xl:max-w-7xl

View File

@ -0,0 +1,25 @@
{{-- Confirm-delete modal in CluPilot glass style. --}}
<div class="clu-modal-card">
<div class="clu-modal-head">
<div class="clu-modal-ico">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M10.3 3.86l-8.55 14.83A2 2 0 0 0 3.46 22h17.08a2 2 0 0 0 1.71-3.31L13.7 3.86a2 2 0 0 0-3.4 0z"/>
<path d="M12 9v4M12 17h.01"/>
</svg>
</div>
<div>
<h2 class="clu-modal-title">{{ $title }}</h2>
<p class="clu-modal-msg">{{ $message }}</p>
</div>
</div>
<div class="clu-modal-foot">
<button type="button" class="clu-ghost-btn" wire:click="closeModal">{{ $cancelLabel }}</button>
<button type="button" class="clu-btn-primary clu-btn-danger" wire:click="confirm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14"/>
</svg>
{{ $confirmLabel }}
</button>
</div>
</div>

View File

@ -125,40 +125,34 @@ new #[Layout('layouts.app')] class extends Component {
</div>
@push('scripts')
<script>
(function initTrafficChart() {
(function () {
function initTrafficChart() {
const el = document.getElementById('clu-traffic-chart');
if (!el || !window.Chart) return;
if (!el) return;
// Wait until ChartJS is registered on window (app.js loads after page-parse)
if (!window.Chart) { setTimeout(initTrafficChart, 30); return; }
if (el._chart) el._chart.destroy();
const ctx = el.getContext('2d');
// 24 buckets, hour labels
const labels = Array.from({length: 24}, (_, i) => ((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00');
// mock 24h traffic curve (peak midday)
const labels = Array.from({length: 24}, (_, i) =>
((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00'
);
const data = [42, 38, 32, 28, 22, 19, 25, 48, 92, 134, 168, 192, 210, 218, 204, 185, 162, 148, 135, 118, 96, 78, 64, 52];
const grad = ctx.createLinearGradient(0, 0, 0, 240);
grad.addColorStop(0, 'rgba(59,111,242,0.35)');
grad.addColorStop(1, 'rgba(59,111,242,0.00)');
el._chart = new Chart(ctx, {
el._chart = new window.Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
data,
borderColor: '#3b6ff2',
borderWidth: 2,
backgroundColor: grad,
fill: true,
tension: 0.35,
pointRadius: 0,
pointHoverRadius: 4,
data: { labels, datasets: [{
data, borderColor: '#3b6ff2', borderWidth: 2,
backgroundColor: grad, fill: true, tension: 0.35,
pointRadius: 0, pointHoverRadius: 4,
pointHoverBackgroundColor: '#3b6ff2',
}],
},
}] },
options: {
responsive: true,
maintainAspectRatio: false,
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: 'rgba(30,35,60,0.06)' }, ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: '#9596a6' } },
@ -166,8 +160,10 @@ new #[Layout('layouts.app')] class extends Component {
},
},
});
}
initTrafficChart();
document.addEventListener('livewire:navigated', initTrafficChart);
})();
document.addEventListener('livewire:navigated', () => initTrafficChart());
</script>
@endpush

View File

@ -165,7 +165,20 @@ new #[Layout('layouts.app')] class extends Component {
<div class="clu-card-head"><h3 style="color:var(--color-danger);">Danger Zone</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<p class="text-[13px]">Workspace löschen entfernt alle Sites, Server-Verbindungen und Backups unwiderruflich.</p>
<button class="clu-ghost-btn" style="color:var(--color-danger);border-color:rgba(219,59,59,0.3);align-self:flex-start;" type="button">Workspace löschen…</button>
<button class="clu-ghost-btn"
style="color:var(--color-danger);border-color:rgba(219,59,59,0.3);align-self:flex-start;"
type="button"
wire:click="$dispatch('openModal', {
component: 'modals.confirm-delete',
arguments: {
title: 'Workspace wirklich löschen?',
message: 'Alle Sites, Server-Verbindungen, Backups und Logs werden unwiderruflich entfernt. Diese Aktion kann nicht rückgängig gemacht werden.',
confirmLabel: 'Workspace löschen',
confirmEvent: 'workspace-delete-confirmed'
}
})">
Workspace löschen…
</button>
</div>
</div>
@endif