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
parent
665ca4e40b
commit
da90b39085
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
@import "tailwindcss";
|
@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).
|
CluPilot design tokens — sourced from templates/akutell/* (light glass).
|
||||||
──────────────────────────────────────────────────────────────────────── */
|
──────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
@ -661,8 +666,9 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hamburger button in topbar (mobile only) */
|
/* Hamburger button in topbar (mobile only).
|
||||||
.clu-burger { display: none; }
|
Chained selector to outrank .clu-icon-btn {display:grid}. */
|
||||||
|
.clu-burger.clu-icon-btn { display: none; }
|
||||||
|
|
||||||
@media (max-width: 920px) {
|
@media (max-width: 920px) {
|
||||||
.clu-app-grid { grid-template-columns: 1fr; }
|
.clu-app-grid { grid-template-columns: 1fr; }
|
||||||
|
|
@ -678,7 +684,7 @@
|
||||||
}
|
}
|
||||||
.clu-sidebar.open { transform: translateX(0); }
|
.clu-sidebar.open { transform: translateX(0); }
|
||||||
.clu-sidebar-backdrop.open { display: block; }
|
.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-row { display: flex; align-items: center; gap: 10px; padding: 2px 4px; }
|
||||||
.clu-brand-name { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }
|
.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-level.error { color: #f05252; }
|
||||||
.clu-log-msg { color: #cccddd; flex: 1; word-break: break-all; }
|
.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 */
|
/* File tree */
|
||||||
.clu-file-tree {
|
.clu-file-tree {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
sm:max-w-md
|
||||||
|
md:max-w-xl
|
||||||
|
lg:max-w-3xl
|
||||||
|
xl:max-w-5xl
|
||||||
|
2xl:max-w-7xl
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -125,40 +125,34 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
</div>
|
</div>
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
(function initTrafficChart() {
|
(function () {
|
||||||
|
function initTrafficChart() {
|
||||||
const el = document.getElementById('clu-traffic-chart');
|
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();
|
if (el._chart) el._chart.destroy();
|
||||||
const ctx = el.getContext('2d');
|
const ctx = el.getContext('2d');
|
||||||
|
|
||||||
// 24 buckets, hour labels
|
const labels = Array.from({length: 24}, (_, i) =>
|
||||||
const labels = Array.from({length: 24}, (_, i) => ((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00');
|
((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00'
|
||||||
// mock 24h traffic curve (peak midday)
|
);
|
||||||
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 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);
|
const grad = ctx.createLinearGradient(0, 0, 0, 240);
|
||||||
grad.addColorStop(0, 'rgba(59,111,242,0.35)');
|
grad.addColorStop(0, 'rgba(59,111,242,0.35)');
|
||||||
grad.addColorStop(1, 'rgba(59,111,242,0.00)');
|
grad.addColorStop(1, 'rgba(59,111,242,0.00)');
|
||||||
|
|
||||||
el._chart = new Chart(ctx, {
|
el._chart = new window.Chart(ctx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: { labels, datasets: [{
|
||||||
labels,
|
data, borderColor: '#3b6ff2', borderWidth: 2,
|
||||||
datasets: [{
|
backgroundColor: grad, fill: true, tension: 0.35,
|
||||||
data,
|
pointRadius: 0, pointHoverRadius: 4,
|
||||||
borderColor: '#3b6ff2',
|
|
||||||
borderWidth: 2,
|
|
||||||
backgroundColor: grad,
|
|
||||||
fill: true,
|
|
||||||
tension: 0.35,
|
|
||||||
pointRadius: 0,
|
|
||||||
pointHoverRadius: 4,
|
|
||||||
pointHoverBackgroundColor: '#3b6ff2',
|
pointHoverBackgroundColor: '#3b6ff2',
|
||||||
}],
|
}] },
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true, maintainAspectRatio: false,
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: { legend: { display: false } },
|
plugins: { legend: { display: false } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { grid: { color: 'rgba(30,35,60,0.06)' }, ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: '#9596a6' } },
|
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>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-head"><h3 style="color:var(--color-danger);">Danger Zone</h3></div>
|
||||||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue