Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
nyumbani-modal
nyumbani-modal
nyumbani/ui
claude-code
ui
laravel
nyumbani

Invoke when adding, changing or debugging any modal, dialog, slide-over, bottom sheet or popup in a Nyumbani app: the <x-nyumbani::modal> component, opening it by event, reopening it after a failed save, loading its body remotely, or confirming a destructive action. Skip for toasts and for pages that are not overlays.

Install
php artisan boost:add-skill nyumbani/ui

Save this content to: .claude/skills/nyumbani-modal/SKILL.md

# Nyumbani Modal

Every Nyumbani app uses the package modal, `<x-nyumbani::modal>`. Never write a one-off Alpine modal and never copy one from another app.

## Setup (already done in a Nyumbani app)
- `resources/js/app.js` imports `vendor/nyumbani/ui/resources/dist/ui.js`, a pre-bundled file that includes the modal, its `@alpinejs/ui` plugin and `nyumbaniModal`. The app installs no npm package for it.
- Run `npm run build` after adding classes to a view.

## Props
| Prop | Default | Use |
| --- | --- | --- |
| `title` (attribute) | none | Heading in the title bar |
| `size` | `md` | `sm`, `md`, `lg`, `xl`, `full` |
| `position` | `center` | `right` makes a slide-over |
| `name` | none | Lets events open and close it |
| `error-bag` | none | Reopens when that bag has errors |
| `dismissable` | `true` | `false` is a static backdrop: Escape and outside clicks do nothing |
| `src` | none | URL of an HTML fragment loaded on first open |
| `confirm` | `false` | Two-step review before the form posts (see Two-step confirmation) |
| `continue-label`, `back-label` | `Continue`, `Back` | Text of the confirm buttons |
| `close-button`, `show-title` | `true` | Hide the X or the title bar |

Slots: `trigger` (opens on click), the default slot (body), `action` (footer pinned below the scrolling body), `review` (step two of a `confirm` modal).

## Patterns
Trigger and body:
```blade
<x-nyumbani::modal title="Add record">
    <x-slot:trigger><button type="button" class="btn btn-primary">Add</button></x-slot:trigger>
    <form>...</form>
    <x-slot:action>...</x-slot:action>
</x-nyumbani::modal>
```

Open or close by name from anywhere inside an Alpine scope (`x-data`):
```blade
<button type="button" x-on:click="$dispatch('open-modal', 'edit-record')">Edit</button>
```
`$dispatch` only works inside an element with `x-data`. Add one to the wrapper when the button sits outside any Alpine scope. Close with `close-modal`.

Reopen after a failed save, in four steps (see the nyumbani-error-bags skill):
1. Give the modal a `name` and an `error-bag` (use the same word for both).
2. Post the bag with `@bag('same-name')` inside the form.
3. Validate as usual: a Form Request or `$request->validate()` needs no trait and no override.
4. In the modal, read errors with `@error('field', 'same-name')` and refill with `old('field')` only when `$errors->hasBag('same-name')`.

It does not reopen when the modal has no `error-bag`, when the name differs from `@bag`, or when the view reads `@error('field')` without the bag. A fixed `protected $errorBag` on the Form Request also works. Bags apply to the redirect after a failed validation, not to JSON or Livewire answers.

Remote content: pass `src` and return a Blade fragment, not a full page, from that route. The loader shows while it loads and a retry link shows on failure. It loads once per page, and again only after a failure.

## Two-step confirmation
Use `confirm` to review a form before it posts. The default slot is step one (the form, with an `id`), step two is a `<div data-review>` written inside that form, and the `action` slot holds the real submit button, pointed at the form with `form="id"`. Continue runs the browser's native validation (`reportValidity()`), so use normal `required`, `minlength` and `pattern` attributes. The review reads what was typed from `data`, for example `x-text="data.name"`. The Enter key acts as Continue, and the modal always opens on step one.
```blade
<x-nyumbani::modal confirm error-bag="new-customer" title="New customer">
    <form id="new-customer-form" method="POST" action="{{ route('customers.store') }}">
        @csrf
        @bag('new-customer')
        <input name="name" required minlength="3">

<div data-review>Name: <span x-text="data.name"></span></div>
    </form>
    <x-slot:action><button type="submit" form="new-customer-form" class="btn btn-primary">Save</button></x-slot:action>
</x-nyumbani::modal>
```
The review reads `data` keyed by each field's `name`, so every field needs a `name`. Fields with no name, values held only in Alpine state, fields removed with `x-if`, checkbox groups and `name[]` are not in `data`; a select gives its value, not its label. A submit during step one is intercepted before the form's own `x-on:submit` or `wire:submit` runs, and those handlers run only in step two. The steps animate with `x-transition`.

The review always lives in the form, in a `<div data-review>`. When the reader presses Continue, the modal copies that block into the review step, so one form partial serves a modal on the page and a form loaded with `src`. Never write `<x-slot:review>` in new code: the slot is retired and only still works so older code does not break. The block is hidden in the form and reads `data`. On a `confirm` modal the remote body hides itself during the review, so the form needs no `x-show` of its own.

```blade
<form id="customer-form" method="POST" action="{{ $action }}" class="p-4">
    @csrf
    <input name="name" required>

    <div data-review>Name: <span x-text="data.name"></span></div>
</form>
```

Do not hand-roll `confirm == true` toggles or a `type="button"` Continue that skips validation.

## Confirmations
Use native `confirm()`, for example `onclick="return confirm('Delete this record?')"` or `x-on:click="confirm('...') || $event.preventDefault()"`. Do not build a confirm modal for destructive actions.

## Rules
- The body is not padded by the component, so the content must be: wrap the default slot in an element with at least `p-4`, or put `class="p-4"` on the form. A fragment loaded with `src` pads its own root. Nothing may sit on the edge of the panel, except a deliberate full-bleed image or table. The `action` footer is padded by the component.
- Buttons inside use `btn btn-primary`. Every button is primary.
- Do not put `@php` in the view. Logic belongs in the `Nyumbani\Ui\Views\Components\Modal` class or a controller.
- The panel wrapper uses `overflow-clip`, not `overflow-hidden`, because the focus trap otherwise scrolls the slide-over sideways while it animates. Keep that when changing the component.
- After changing the component, run `php artisan view:clear` if the compiled view reports an undefined variable.
- The modal keeps all of its state and methods under one Alpine key, `modal` (`modal.open`, `modal.loading`, `modal.failed`, `modal.confirming`, `modal.show()`, `modal.advance()`), plus `data` (the review step reads `data.name`) and Alpine's `init`. Never add another top-level name to `nyumbaniModal()` in `resources/js/modal.js`: an app's own `x-data` and `x-model` sit on the same element, and Alpine resolves a bare name on that element's own data first, so a new name such as `show` silently shadows the app's state. Add members inside `modal`. `ModalTest` fails when a name appears at the top level. In an app, name the state you bind with `x-model` anything except `modal`, `data` and `init`.

Nyumbani Modal

Every Nyumbani app uses the package modal, <x-nyumbani::modal>. Never write a one-off Alpine modal and never copy one from another app.

Setup (already done in a Nyumbani app)

  • resources/js/app.js imports vendor/nyumbani/ui/resources/dist/ui.js, a pre-bundled file that includes the modal, its @alpinejs/ui plugin and nyumbaniModal. The app installs no npm package for it.
  • Run npm run build after adding classes to a view.

Props

Prop Default Use
title (attribute) none Heading in the title bar
size md sm, md, lg, xl, full
position center right makes a slide-over
name none Lets events open and close it
error-bag none Reopens when that bag has errors
dismissable true false is a static backdrop: Escape and outside clicks do nothing
src none URL of an HTML fragment loaded on first open
confirm false Two-step review before the form posts (see Two-step confirmation)
continue-label, back-label Continue, Back Text of the confirm buttons
close-button, show-title true Hide the X or the title bar

Slots: trigger (opens on click), the default slot (body), action (footer pinned below the scrolling body), review (step two of a confirm modal).

Patterns

Trigger and body:

<x-nyumbani::modal title="Add record">
    <x-slot:trigger><button type="button" class="btn btn-primary">Add</button></x-slot:trigger>
    <form>...</form>
    <x-slot:action>...</x-slot:action>
</x-nyumbani::modal>

Open or close by name from anywhere inside an Alpine scope (x-data):

<button type="button" x-on:click="$dispatch('open-modal', 'edit-record')">Edit</button>

$dispatch only works inside an element with x-data. Add one to the wrapper when the button sits outside any Alpine scope. Close with close-modal.

Reopen after a failed save, in four steps (see the nyumbani-error-bags skill):

  1. Give the modal a name and an error-bag (use the same word for both).
  2. Post the bag with @bag('same-name') inside the form.
  3. Validate as usual: a Form Request or $request->validate() needs no trait and no override.
  4. In the modal, read errors with @error('field', 'same-name') and refill with old('field') only when $errors->hasBag('same-name').

It does not reopen when the modal has no error-bag, when the name differs from @bag, or when the view reads @error('field') without the bag. A fixed protected $errorBag on the Form Request also works. Bags apply to the redirect after a failed validation, not to JSON or Livewire answers.

Remote content: pass src and return a Blade fragment, not a full page, from that route. The loader shows while it loads and a retry link shows on failure. It loads once per page, and again only after a failure.

Two-step confirmation

Use confirm to review a form before it posts. The default slot is step one (the form, with an id), step two is a <div data-review> written inside that form, and the action slot holds the real submit button, pointed at the form with form="id". Continue runs the browser's native validation (reportValidity()), so use normal required, minlength and pattern attributes. The review reads what was typed from data, for example x-text="data.name". The Enter key acts as Continue, and the modal always opens on step one.

<x-nyumbani::modal confirm error-bag="new-customer" title="New customer">
    <form id="new-customer-form" method="POST" action="{{ route('customers.store') }}">
        @csrf
        @bag('new-customer')
        <input name="name" required minlength="3">

<div data-review>Name: <span x-text="data.name"></span></div>
    </form>
    <x-slot:action><button type="submit" form="new-customer-form" class="btn btn-primary">Save</button></x-slot:action>
</x-nyumbani::modal>

The review reads data keyed by each field's name, so every field needs a name. Fields with no name, values held only in Alpine state, fields removed with x-if, checkbox groups and name[] are not in data; a select gives its value, not its label. A submit during step one is intercepted before the form's own x-on:submit or wire:submit runs, and those handlers run only in step two. The steps animate with x-transition.

The review always lives in the form, in a <div data-review>. When the reader presses Continue, the modal copies that block into the review step, so one form partial serves a modal on the page and a form loaded with src. Never write <x-slot:review> in new code: the slot is retired and only still works so older code does not break. The block is hidden in the form and reads data. On a confirm modal the remote body hides itself during the review, so the form needs no x-show of its own.

<form id="customer-form" method="POST" action="{{ $action }}" class="p-4">
    @csrf
    <input name="name" required>

    <div data-review>Name: <span x-text="data.name"></span></div>
</form>

Do not hand-roll confirm == true toggles or a type="button" Continue that skips validation.

Confirmations

Use native confirm(), for example onclick="return confirm('Delete this record?')" or x-on:click="confirm('...') || $event.preventDefault()". Do not build a confirm modal for destructive actions.

Rules

  • The body is not padded by the component, so the content must be: wrap the default slot in an element with at least p-4, or put class="p-4" on the form. A fragment loaded with src pads its own root. Nothing may sit on the edge of the panel, except a deliberate full-bleed image or table. The action footer is padded by the component.
  • Buttons inside use btn btn-primary. Every button is primary.
  • Do not put @php in the view. Logic belongs in the Nyumbani\Ui\Views\Components\Modal class or a controller.
  • The panel wrapper uses overflow-clip, not overflow-hidden, because the focus trap otherwise scrolls the slide-over sideways while it animates. Keep that when changing the component.
  • After changing the component, run php artisan view:clear if the compiled view reports an undefined variable.
  • The modal keeps all of its state and methods under one Alpine key, modal (modal.open, modal.loading, modal.failed, modal.confirming, modal.show(), modal.advance()), plus data (the review step reads data.name) and Alpine's init. Never add another top-level name to nyumbaniModal() in resources/js/modal.js: an app's own x-data and x-model sit on the same element, and Alpine resolves a bare name on that element's own data first, so a new name such as show silently shadows the app's state. Add members inside modal. ModalTest fails when a name appears at the top level. In an app, name the state you bind with x-model anything except modal, data and init.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle