Replace native confirm() dialogs with styled confirm modals.
Deploy Ladill Servers / deploy (push) Successful in 29s

Add shared confirm-dialog component, Alpine ladillConfirm store, and
swap browser confirms for consistent bottom-sheet modals across user,
admin, and hosting flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 21:11:10 +00:00
co-authored by Cursor
parent b5513283da
commit 29cdfe67b8
21 changed files with 499 additions and 85 deletions
@@ -42,10 +42,18 @@
{{ $token->last_used_at ? 'last used '.$token->last_used_at->diffForHumans() : 'never used' }}
</p>
</div>
<form method="POST" action="{{ route('account.developers.destroy', $token->id) }}" onsubmit="return confirm('Revoke {{ $token->name }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</form>
<x-confirm-dialog
:name="'revoke-token-'.$token->id"
title="Revoke API token?"
:message="'Revoke '.$token->name.'? Apps using this token will stop working.'"
:action="route('account.developers.destroy', $token->id)"
method="DELETE"
confirm-label="Revoke"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@empty
<p class="px-5 py-8 text-center text-sm text-slate-400">No tokens yet.</p>
+12 -4
View File
@@ -62,10 +62,18 @@
<option value="admin" @selected($member->role === 'admin')>Admin</option>
</select>
</form>
<form method="POST" action="{{ route('account.team.destroy', $member) }}" onsubmit="return confirm('Remove {{ $member->email }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</form>
<x-confirm-dialog
:name="'remove-member-'.$member->id"
title="Remove team member?"
:message="'Remove '.$member->email.' from this team?'"
:action="route('account.team.destroy', $member)"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $member->role }}</span>
@endif
+15 -8
View File
@@ -359,14 +359,21 @@
<span x-text="showCredentials ? 'Hide' : 'Show'"></span> Credentials
</button>
@endif
<form action="{{ route('hosting.panel.apps.delete', [$account, $site]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this {{ ucfirst($site->installed_app) }} installation? This will remove all application files and cannot be undone.');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-app-'.$site->id"
:title="'Delete '.ucfirst($site->installed_app).'?'"
message="This will remove all application files and cannot be undone."
:action="route('hosting.panel.apps.delete', [$account, $site])"
method="DELETE"
confirm-label="Delete"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
</div>
+18 -9
View File
@@ -111,15 +111,24 @@
<code class="text-sm font-mono text-slate-600 break-all">{{ $job['command'] }}</code>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<form action="{{ route('hosting.panel.cron.delete', $account) }}" method="POST" onsubmit="return confirm('Delete this cron job?')">
@csrf
@method('DELETE')
<input type="hidden" name="line_number" value="{{ $job['line_number'] }}">
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-cron-'.$job['line_number']"
title="Delete cron job?"
message="Delete this cron job?"
:action="route('hosting.panel.cron.delete', $account)"
method="DELETE"
confirm-label="Delete"
>
<x-slot:fields>
<input type="hidden" name="line_number" value="{{ $job['line_number'] }}">
</x-slot:fields>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</td>
</tr>
@endforeach
@@ -71,14 +71,21 @@
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
Reset Password
</button>
<form action="{{ route('hosting.panel.databases.delete', [$account, $database]) }}" method="POST" onsubmit="return confirm('Are you sure? This will permanently delete the database and all its data.')">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-db-'.$database->id"
title="Delete database?"
message="This will permanently delete the database and all its data."
:action="route('hosting.panel.databases.delete', [$account, $database])"
method="DELETE"
confirm-label="Delete"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
</td>
</tr>
@@ -126,8 +133,8 @@
@push('scripts')
<script>
async function resetPassword(databaseId) {
if (!confirm('Reset the database password? You will need to update your application configuration.')) return;
async async function resetPassword(databaseId) {
if (!await window.ladillConfirm({ title: 'Reset password?', message: 'Reset the database password? You will need to update your application configuration.', confirmLabel: 'Reset', variant: 'primary' })) return;
try {
const res = await fetch(`{{ url('hosting/panel/' . $account->id . '/databases') }}/${databaseId}/reset-password`, {
method: 'POST',
@@ -178,14 +178,21 @@
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
@if((int) $account->user_id === (int) auth()->id())
<form action="{{ route('hosting.panel.domains.remove', [$account, $site]) }}" method="POST" onsubmit="return confirm('Remove this domain? The files will not be deleted.')">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Remove
</button>
</form>
<x-confirm-dialog
:name="'remove-domain-'.$site->id"
title="Remove domain?"
message="Remove this domain? The files will not be deleted."
:action="route('hosting.panel.domains.remove', [$account, $site])"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Remove
</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="text-xs text-slate-400">Owner only</span>
@endif
@@ -418,9 +418,9 @@
this.selected = event.target.checked ? [...this.allPaths] : [];
},
bulkAction(action) {
async async async bulkAction(action) {
if (action === 'delete') {
if (!confirm('Delete ' + this.selected.length + ' item(s)? This cannot be undone.')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Delete items?', message: 'Delete ' + this.selected.length + ' item(s)? This cannot be undone.' })) return;
this.submitBulkDelete();
} else if (action === 'move' || action === 'copy') {
this.moveAction = action;
@@ -567,7 +567,7 @@
},
async deleteFile(path) {
if (!confirm('Are you sure you want to delete this?')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Delete item?', message: 'Are you sure you want to delete this?' })) return;
this.loading = true;
try {
const res = await fetch('{{ route("hosting.panel.files.delete", $account) }}', {
@@ -583,7 +583,7 @@
},
async extractFile(path, name) {
if (!confirm('Extract "' + name + '" in the current directory?\n\nThis may take a while for large archives.')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Extract archive?', message: 'Extract "' + name + '" in the current directory?\n\nThis may take a while for large archives.', confirmLabel: 'Extract', variant: 'primary' })) return;
this.loading = true;
this.extracting = true;
this.extractFileName = name;
+19 -8
View File
@@ -37,14 +37,25 @@
</select>
</form>
<form action="{{ route('hosting.panel.logs.clear', $account) }}" method="POST" onsubmit="return confirm('Clear this log file?')" class="ml-auto">
@csrf
<input type="hidden" name="type" value="{{ $logType }}">
<button type="submit" class="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Clear Log
</button>
</form>
<x-confirm-dialog
name="clear-log"
title="Clear log file?"
message="Clear this log file? This cannot be undone."
:action="route('hosting.panel.logs.clear', $account)"
method="POST"
confirm-label="Clear log"
class="ml-auto"
>
<x-slot:fields>
<input type="hidden" name="type" value="{{ $logType }}">
</x-slot:fields>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Clear Log
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
@@ -80,13 +80,21 @@
</form>
@if ($teamMembership->hasSshAccessKey())
<form method="POST" action="{{ route('hosting.panel.settings.ssh-key.destroy', $account) }}" class="mt-3" onsubmit="return confirm('Remove your SFTP key access for this hosting account?');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center rounded-lg border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-400 hover:text-slate-900">
Remove SFTP Key
</button>
</form>
<x-confirm-dialog
name="remove-sftp-key"
title="Remove SFTP key?"
message="Remove your SFTP key access for this hosting account?"
:action="route('hosting.panel.settings.ssh-key.destroy', $account)"
method="DELETE"
confirm-label="Remove key"
class="mt-3"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center rounded-lg border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-400 hover:text-slate-900">
Remove SFTP Key
</button>
</x-slot:trigger>
</x-confirm-dialog>
@endif
@else
<div class="mt-5 rounded-xl bg-slate-50 px-4 py-4 text-sm text-slate-600">