Feat: add confirmation modal component and Livewire support

- Introduced `Confirmation` trait for reusable confirmation modal logic in Livewire components.
- Added `ConfirmationModal` Livewire component for handling confirmation prompts with customizable title, subtitle, description, and styling.
- Updated `captainhook.json` to increase `subjectLength` limit.
- Integrated confirmation modal in the sidebar layout with `<livewire:confirmation-modal/>`.
This commit is contained in:
= 2026-05-22 11:37:31 +00:00
parent 2956d03001
commit 5eca6d3602
4 changed files with 164 additions and 1 deletions

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Concerns;
use Livewire\Attributes\On;
trait Confirmation
{
/**
* Shows a confirmation modal before executing the given method.
*
* @param string $method Method name that should be executed if the user confirms.
* @param mixed|null $params Any parameters that should be passed to the method.
* @param string $title Title of the confirmation modal.
* @param string $subtitle Subtitle of the confirmation modal.
* @param string|null $description Description of the confirmation modal.
* @param string $descriptionClass Classes to apply to the description element. (!, !important to overide)
* @param string $acceptBtnClass Classes to apply to the accept button. (!, !important to overide)
* @param string $icon Icon to display in the confirmation modal.
*/
public function requireConfirmation(
string $method,
mixed $params = null,
string $title = 'Are you sure?',
string $subtitle = 'This action cannot be undone.',
?string $description = null,
string $descriptionClass = '',
string $acceptBtnClass = '',
string $icon = 'lucide.triangle-alert'
): void {
$this->dispatch(
'open-confirmation',
componentId: $this->getId(),
action: $method,
params: $params,
title: $title,
subtitle: $subtitle,
description: $description,
descriptionClass: $descriptionClass,
acceptBtnClass: $acceptBtnClass,
icon: $icon
);
}
/**
* Listen globally for the acceptance, but only process if the ID matches.
*/
#[On('confirmation-accepted')]
public function onConfirmationAccepted(string $action, mixed $params, string $componentId): void
{
// Ignore the event if it wasn't requested by this specific component instance
if ($this->getId() !== $componentId) {
return;
}
// Verify the method actually exists on the component
if (method_exists($this, $action)) {
// Execute the requested method, passing any provided parameters
$this->{$action}($params);
}
}
}

View File

@ -5,7 +5,7 @@
{
"action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Beams",
"options": {
"subjectLength": 50,
"subjectLength": 100,
"bodyLineLength": 300
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\On;
use TailwindMerge\TailwindMerge;
new class extends Component {
public bool $open = false;
public string $title = 'Are you sure?';
public string $subtitle = 'This action cannot be undone.';
public ?string $description = null;
public string $icon = 'lucide.triangle-alert';
public string $descriptionClass = '';
public string $acceptBtnClass = '';
public string $action = '';
public mixed $params = null;
public string $componentId = '';
#[On('open-confirmation')]
public function openModal(
string $componentId,
string $action,
mixed $params = null,
string $title = 'Are you sure?',
string $subtitle = 'This action cannot be undone.',
?string $description = null,
string $descriptionClass = '',
string $acceptBtnClass = '',
string $icon = 'lucide.triangle-alert'
): void {
$this->action = $action;
$this->componentId = $componentId;
$this->title = $title;
$this->subtitle = $subtitle;
$this->description = $description;
$this->params = $params;
$this->descriptionClass = $descriptionClass;
$this->acceptBtnClass = $acceptBtnClass;
$this->icon = $icon;
$this->open = true;
}
public function confirm(): void
{
$this->open = false;
$this->dispatch('confirmation-accepted',
action: $this->action,
params: $this->params,
componentId: $this->componentId
);
}
public function getMergedAlertClass(): string
{
return TailwindMerge::factory()->make()->merge(
'alert-warning alert-soft text-red-600',
$this->descriptionClass
);
}
public function getMergedButtonClass(): string
{
return TailwindMerge::factory()->make()->merge(
'btn-warning',
$this->acceptBtnClass
);
}
}
?>
<div>
<x-mary-modal wire:model="open" :title="$title" :subtitle="$subtitle" class="backdrop-blur" persistent>
@isset($description)
<x-mary-alert
class="{{ $this->getMergedAlertClass() }}"
:icon="$icon">
{{ $description }}
</x-mary-alert>
@endisset
<x-slot:actions>
<x-mary-button label="Cancel" @click="$wire.open = false"/>
<x-mary-button
label="Confirm"
class="{{ $this->getMergedButtonClass() }}"
wire:click="confirm"
spinner="confirm"/>
</x-slot:actions>
</x-mary-modal>
</div>

View File

@ -15,6 +15,7 @@
</main>
</div>
<x-mary-toast/>
<livewire:confirmation-modal/>
</div>
@livewireScripts
</body>