corepine/modal

Reusable Alpine + Livewire modal stack for Corepine packages.

Maintainers

Package info

github.com/corepine/modal

pkg:composer/corepine/modal

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.8 2026-05-02 15:20 UTC

This package is auto-updated.

Last update: 2026-05-02 15:21:01 UTC


README

Corepine Modal is a stack-based modal system for Laravel with two runtime modes:

  • standalone Alpine + Blade modals

  • Livewire stack-based modals

  • modal (dialog)

  • drawer (left or right panel)

  • sheet (bottom sheet)

It supports:

  • modal stacks (open child modals on top of parent modals)
  • declarative shell actions
  • strongly typed modal classes (extends Corepine\Modal\Modal)
  • configurable event names for package-safe integrations
  • standalone Blade-only modals (no Livewire modal class required)

Requirements

  • PHP ^8.2|^8.3|^8.4
  • Laravel ^11.0|^12.0|^13.0
  • Livewire ^3.7|^4.0

Livewire is required by the package because stack mode uses a Livewire host. Standalone Alpine + Blade usage is still fully supported.

Installation

composer require corepine/modal

Publish config:

php artisan vendor:publish --tag=corepine-modal-config

Setup

Livewire Stack Mode

Render the host once in your layout:

<x-corepine.modal.assets />

Standalone Alpine + Blade Mode

You can use <x-corepine.modal /> directly with browser events and no Livewire modal class. The host is not required for standalone-only usage.

Tailwind Setup

Add the package stylesheet to your main CSS entry:

@import "../../vendor/corepine/modal/resources/css/app.css";

The package CSS already includes Tailwind @source paths for its own views and PHP classes.

Quick Start (Livewire Stack Mode)

<?php

namespace App\Livewire\Modals;

use App\Models\User;
use Corepine\Modal\Actions\Action;
use Corepine\Modal\Enums\ModalType;
use Corepine\Modal\Modal;
use Corepine\Support\Enums\Placement;

class EditUser extends Modal
{
    public User $user;

    public static function modalAttributes(): array
    {
        return [
            'type' => ModalType::Modal,
            'placement' => Placement::Center,
            'origin' => Placement::Center,
            'shell' => true,
            'heading' => 'Edit User',
            'description' => 'Update account details',
            'showClose' => true,
            'dismissible' => true,
            'closeOnEscape' => true,
            'actions' => [
                Action::make('cancel')->label('Cancel')->close(),
                Action::make('save')->label('Save')->primary()->action('save'),
            ],
        ];
    }
}

Open it from Blade:

<button
    type="button"
    onclick="Livewire.dispatch('modal.open', { component: 'modals.edit-user', arguments: { user: {{ $user->id }} } })"
>
    Edit User
</button>

Open / Close APIs

From a modal class:

$this->openModal('modals.edit-user', ['user' => 5]);
$this->openBottomSheet('modals.user-sheet', ['user' => 5]);

$this->closeModal();
$this->closeModal(
    destroy: false,
    dispatch: ['users-refreshed' => ['user' => 5]],
    dispatchTo: ['orders.table' => ['sync-user' => ['user' => 5]]],
);
$this->closeTopModal(
    layers: 2,
    dispatch: ['users-refreshed' => ['user' => 5]],
);
$this->closeAll(
    dispatchTo: ['orders.table' => ['sync-user' => ['user' => 5]]],
);

From Blade helpers:

<x-corepine.modal.actions.open component="modals.edit-user" :arguments="['user' => $user->id]">
    <button type="button">Edit</button>
</x-corepine.modal.actions.open>

<x-corepine.modal.actions.open modal-id="user-sheet">
    <button type="button">Open Sheet</button>
</x-corepine.modal.actions.open>

<x-corepine.modal.actions.close
    layers="1"
    :destroy="true"
    :dispatch="['users-refreshed' => ['user' => $user->id]]"
    :dispatch-to="['orders.table' => ['sync-user' => ['user' => $user->id]]]"
>
    Close
</x-corepine.modal.actions.close>

dispatch fires regular Livewire/browser events after the close finishes.

dispatchTo fires Livewire targeted events after the close finishes.

Use modal-id on open/close helpers when you want to target a standalone <x-corepine.modal id="..." /> by id.

Quick Start (Standalone Alpine + Blade Mode)

Use this when you do not need a Livewire modal class:

<button
    type="button"
    onclick="window.dispatchEvent(new CustomEvent('modal.open', { detail: { id: 'user-sheet' } }))"
>
    Open Sheet
</button>

<x-corepine.modal id="user-sheet" type="sheet" heading="User Details">
    <p class="text-sm text-zinc-600">Standalone modal body</p>

    <x-slot:footer>
        <button
            type="button"
            class="rounded-md border px-3 py-2 text-sm"
            onclick="window.dispatchEvent(new CustomEvent('modal.close', { detail: { id: 'user-sheet' } }))"
        >
            Close
        </button>
    </x-slot:footer>
</x-corepine.modal>

Standalone named slots:

  • header: custom header content. Slot attributes are merged onto the header wrapper classes.
  • When header slot is provided (even empty), it overrides built-in heading/description/close rendering.
  • footer: custom footer content.

Example:

<x-corepine.modal id="custom-header-modal" show-close="false">
    <x-slot:header class="font-bold text-lg" data-testid="custom-header">
        Custom Header
    </x-slot:header>

    <p>Body</p>
</x-corepine.modal>

Standalone browser events:

  • modal.open
  • modal.close
  • modal.toggle

Each event accepts { id?: string } in detail.

Modal Attributes

The canonical shell/action API uses actions (not legacy keys).

Key Type Default Notes
type modal | drawer | sheet modal Presentation type.
placement Placement | string by type modal: center/top/bottom/left/right, drawer: left/right, sheet: forced bottom.
origin Placement | string follows type/placement Transform origin; same value set as placement vocabulary.
size string default Width token from config sizes, or custom class string.
height string | number | null null Panel height (modal/drawer) and initial sheet height.
maxHeight string | number | null null Shared max-height cap for all types.
dismissible bool true Scrim click closes when true.
draggable bool type-aware Sheet drag/resize behavior.
showDragHandle bool type-aware Sheet handle visibility.
dragCloseThreshold float 0.3 Sheet drag-close ratio.
closeOnEscape bool true Escape closes top layer.
closeAllOnEscape bool false Escape closes full stack.
destroyOnClose bool true Remove closed layers from host state.
dispatch array [] Default events to dispatch after close.
dispatchTo array [] Default targeted Livewire events to dispatch after close.
dispatchCloseEvent bool false Emits the built-in modal.component-closed notification for that layer.
blur bool false Scrim blur effect.
shell bool true Enables built-in shell header/body/footer structure.
heading string | null null Shell heading text.
description string | null null Shell description text.
showClose bool | null auto Built-in shell close icon. Defaults to visible only when built-in heading or description is present.
footerActionsAlignment Alignment | string end start, center, end.
actions array [] Declarative shell actions (close / method).
class string '' Extra panel classes.

Type Behavior Rules

  • sheet: always renders from bottom and always uses placement=bottom, origin=bottom.
  • drawer: only left and right are valid.
  • modal: supports all five placement values and now fully respects both placement and origin.

Declarative Actions

Action payloads can be raw arrays or fluent Action::make(...) objects.

use Corepine\Modal\Actions\Action;

'actions' => [
    Action::make('cancel')
        ->label('Cancel')
        ->close(),

    Action::make('save')
        ->label('Save')
        ->primary()
        ->action('save'),
]

Supported fluent helpers include:

  • method() / action()
  • close(layers, destroy, closeAll)
  • dispatch() / dispatchTo() on close actions
  • disabled()
  • visible()
  • color() and shortcuts (primary, danger, success, warning, info, gray, dark)
  • accent()
  • outline()
  • attributes()

Event System

Default incoming events:

  • modal.open
  • modal.open-sheet
  • modal.close
  • modal.close-top
  • modal.close-all
  • modal.destroy
  • modal.reset
  • modal.toggle

Default outgoing events:

  • modal.opened
  • modal.closed
  • modal.changed
  • modal.all-closed
  • modal.component-closed

Event Customization

You can rename both incoming and outgoing events in config/corepine-modal.php:

'events' => [
    'listen' => [
        'open' => 'acme.modal.open',
        'close' => 'acme.modal.close',
    ],
    'dispatch' => [
        'opened' => 'acme.modal.opened',
    ],
],

For package integrations, do not hardcode event strings. Resolve them from the service:

use Corepine\Modal\Facades\Modal;

$openEvent = Modal::event()->openModal();
$closeEvent = Modal::event()->closeModal();

Configuration

Main sections in config/corepine-modal.php:

  • events.listen: incoming event names
  • events.dispatch: outgoing event names
  • defaults.attributes: global modal attribute defaults
  • sizes: modal width tokens

Example Size Override

'sizes' => [
    'default' => 'max-w-xl sm:max-w-full',
    'editor' => 'max-w-[960px]',
],

Blade Components (Canonical)

  • <x-corepine.modal.assets />
  • <x-corepine.modal />
  • <x-corepine.modal.layout />
  • <x-corepine.modal.footer />
  • <x-corepine.modal.actions.open />
  • <x-corepine.modal.actions.close />