Full Code of masmerise/livewire-toaster for AI

master 694affbd0af0 cached
65 files
85.4 KB
23.0k tokens
199 symbols
1 requests
Download .txt
Repository: masmerise/livewire-toaster
Branch: master
Commit: 694affbd0af0
Files: 65
Total size: 85.4 KB

Directory structure:
gitextract_4ngx_v20/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .stubs.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADING.md
├── composer.json
├── config/
│   └── toaster.php
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
├── resources/
│   ├── js/
│   │   ├── config.js
│   │   ├── hub.js
│   │   ├── index.js
│   │   ├── toast.js
│   │   ├── toaster.js
│   │   └── uuid41.js
│   └── views/
│       ├── close-button.blade.php
│       └── hub.blade.php
├── src/
│   ├── AccessibleCollector.php
│   ├── Alignment.php
│   ├── Assertable.php
│   ├── Collector.php
│   ├── Duration.php
│   ├── LivewireRelay.php
│   ├── Message.php
│   ├── PendingToast.php
│   ├── Position.php
│   ├── QueuingCollector.php
│   ├── SessionRelay.php
│   ├── TestableCollector.php
│   ├── Toast.php
│   ├── ToastBuilder.php
│   ├── ToastType.php
│   ├── Toastable.php
│   ├── ToastableMacros.php
│   ├── Toaster.php
│   ├── ToasterConfig.php
│   ├── ToasterHub.php
│   ├── ToasterServiceProvider.php
│   └── TranslatingCollector.php
└── tests/
    ├── AccessibleCollectorTest.php
    ├── CollectorFactoryMethods.php
    ├── ControllerTest.php
    ├── DurationTest.php
    ├── LivewireRelayTest.php
    ├── LivewireTest.php
    ├── MessageTest.php
    ├── PendingToastTest.php
    ├── QueuingCollectorTest.php
    ├── SessionRelayTest.php
    ├── TestCase.php
    ├── TestableCollectorTest.php
    ├── ToastBuilderTest.php
    ├── ToastFactoryMethods.php
    ├── ToastTest.php
    ├── ToastableMacrosTest.php
    ├── ToastableTest.php
    ├── ToasterConfigTest.php
    ├── ToasterHubTest.php
    ├── ToasterServiceProviderTest.php
    └── TranslatingCollectorTest.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[*.php]
ij_continuation_indent_size = 4
ij_php_align_assignments = false
ij_php_align_class_constants = false
ij_php_align_group_field_declarations = false
ij_php_align_inline_comments = false
ij_php_align_key_value_pairs = false
ij_php_align_multiline_array_initializer_expression = false
ij_php_align_multiline_binary_operation = false
ij_php_align_multiline_chained_methods = false
ij_php_align_multiline_extends_list = true
ij_php_align_multiline_for = true
ij_php_align_multiline_parameters = false
ij_php_align_multiline_parameters_in_calls = false
ij_php_align_multiline_ternary_operation = false
ij_php_align_phpdoc_comments = true
ij_php_align_phpdoc_param_names = true
ij_php_anonymous_brace_style = end_of_line
ij_php_api_weight = 28
ij_php_array_initializer_new_line_after_left_brace = true
ij_php_array_initializer_right_brace_on_new_line = true
ij_php_array_initializer_wrap = on_every_item
ij_php_assignment_wrap = off
ij_php_attributes_wrap = split_into_lines
ij_php_author_weight = 28
ij_php_binary_operation_sign_on_next_line = false
ij_php_binary_operation_wrap = off
ij_php_blank_lines_after_class_header = 0
ij_php_blank_lines_after_function = 1
ij_php_blank_lines_after_imports = 1
ij_php_blank_lines_after_opening_tag = 1
ij_php_blank_lines_after_package = 1
ij_php_blank_lines_around_class = 1
ij_php_blank_lines_around_constants = 0
ij_php_blank_lines_around_field = 1
ij_php_blank_lines_around_method = 1
ij_php_blank_lines_before_class_end = 0
ij_php_blank_lines_before_imports = 1
ij_php_blank_lines_before_method_body = 0
ij_php_blank_lines_before_package = 1
ij_php_blank_lines_before_return_statement = 1
ij_php_blank_lines_between_imports = 1
ij_php_block_brace_style = end_of_line
ij_php_call_parameters_new_line_after_left_paren = false
ij_php_call_parameters_right_paren_on_new_line = false
ij_php_call_parameters_wrap = normal
ij_php_catch_on_new_line = false
ij_php_category_weight = 28
ij_php_class_brace_style = next_line
ij_php_comma_after_last_array_element = true
ij_php_concat_spaces = false
ij_php_copyright_weight = 28
ij_php_deprecated_weight = 28
ij_php_do_while_brace_force = always
ij_php_else_if_style = combine
ij_php_else_on_new_line = false
ij_php_example_weight = 28
ij_php_extends_keyword_wrap = off
ij_php_extends_list_wrap = on_every_item
ij_php_fields_default_visibility = private
ij_php_filesource_weight = 28
ij_php_finally_on_new_line = false
ij_php_for_brace_force = always
ij_php_for_statement_new_line_after_left_paren = true
ij_php_for_statement_right_paren_on_new_line = true
ij_php_for_statement_wrap = off
ij_php_force_short_declaration_array_style = true
ij_php_getters_setters_naming_style = camel_case
ij_php_getters_setters_order_style = getters_first
ij_php_global_weight = 28
ij_php_group_use_wrap = on_every_item
ij_php_if_brace_force = always
ij_php_if_lparen_on_next_line = false
ij_php_if_rparen_on_next_line = false
ij_php_ignore_weight = 28
ij_php_import_sorting = alphabetic
ij_php_indent_break_from_case = true
ij_php_indent_case_from_switch = true
ij_php_indent_code_in_php_tags = false
ij_php_internal_weight = 28
ij_php_keep_blank_lines_after_lbrace = 0
ij_php_keep_blank_lines_before_right_brace = 0
ij_php_keep_blank_lines_in_code = 1
ij_php_keep_blank_lines_in_declarations = 0
ij_php_keep_control_statement_in_one_line = true
ij_php_keep_first_column_comment = true
ij_php_keep_indents_on_empty_lines = false
ij_php_keep_line_breaks = true
ij_php_keep_rparen_and_lbrace_on_one_line = true
ij_php_keep_simple_classes_in_one_line = false
ij_php_keep_simple_methods_in_one_line = false
ij_php_lambda_brace_style = end_of_line
ij_php_license_weight = 28
ij_php_line_comment_add_space = false
ij_php_line_comment_at_first_column = true
ij_php_link_weight = 28
ij_php_lower_case_boolean_const = true
ij_php_lower_case_keywords = true
ij_php_lower_case_null_const = true
ij_php_method_brace_style = next_line
ij_php_method_call_chain_wrap = off
ij_php_method_parameters_new_line_after_left_paren = true
ij_php_method_parameters_right_paren_on_new_line = true
ij_php_method_parameters_wrap = on_every_item
ij_php_method_weight = 28
ij_php_modifier_list_wrap = false
ij_php_multiline_chained_calls_semicolon_on_new_line = false
ij_php_namespace_brace_style = 1
ij_php_new_line_after_php_opening_tag = true
ij_php_null_type_position = in_the_end
ij_php_package_weight = 28
ij_php_param_weight = 0
ij_php_parameters_attributes_wrap = split_into_lines
ij_php_parentheses_expression_new_line_after_left_paren = false
ij_php_parentheses_expression_right_paren_on_new_line = false
ij_php_phpdoc_blank_line_before_tags = true
ij_php_phpdoc_blank_lines_around_parameters = true
ij_php_phpdoc_keep_blank_lines = true
ij_php_phpdoc_param_spaces_between_name_and_description = 1
ij_php_phpdoc_param_spaces_between_tag_and_type = 1
ij_php_phpdoc_param_spaces_between_type_and_name = 1
ij_php_phpdoc_use_fqcn = false
ij_php_phpdoc_wrap_long_lines = false
ij_php_place_assignment_sign_on_next_line = false
ij_php_place_parens_for_constructor = 0
ij_php_property_read_weight = 28
ij_php_property_weight = 28
ij_php_property_write_weight = 28
ij_php_return_type_on_new_line = false
ij_php_return_weight = 1
ij_php_see_weight = 28
ij_php_since_weight = 28
ij_php_sort_phpdoc_elements = true
ij_php_space_after_colon = true
ij_php_space_after_colon_in_named_argument = true
ij_php_space_after_colon_in_return_type = true
ij_php_space_after_comma = true
ij_php_space_after_for_semicolon = true
ij_php_space_after_quest = true
ij_php_space_after_type_cast = true
ij_php_space_after_unary_not = true
ij_php_space_before_array_initializer_left_brace = false
ij_php_space_before_catch_keyword = true
ij_php_space_before_catch_left_brace = true
ij_php_space_before_catch_parentheses = true
ij_php_space_before_class_left_brace = true
ij_php_space_before_closure_left_parenthesis = true
ij_php_space_before_colon = true
ij_php_space_before_colon_in_named_argument = false
ij_php_space_before_colon_in_return_type = false
ij_php_space_before_comma = false
ij_php_space_before_do_left_brace = true
ij_php_space_before_else_keyword = true
ij_php_space_before_else_left_brace = true
ij_php_space_before_finally_keyword = true
ij_php_space_before_finally_left_brace = true
ij_php_space_before_for_left_brace = true
ij_php_space_before_for_parentheses = true
ij_php_space_before_for_semicolon = false
ij_php_space_before_if_left_brace = true
ij_php_space_before_if_parentheses = true
ij_php_space_before_method_call_parentheses = false
ij_php_space_before_method_left_brace = true
ij_php_space_before_method_parentheses = false
ij_php_space_before_quest = true
ij_php_space_before_short_closure_left_parenthesis = true
ij_php_space_before_switch_left_brace = true
ij_php_space_before_switch_parentheses = true
ij_php_space_before_try_left_brace = true
ij_php_space_before_unary_not = false
ij_php_space_before_while_keyword = true
ij_php_space_before_while_left_brace = true
ij_php_space_before_while_parentheses = true
ij_php_space_between_ternary_quest_and_colon = false
ij_php_spaces_around_additive_operators = true
ij_php_spaces_around_arrow = false
ij_php_spaces_around_assignment_in_declare = false
ij_php_spaces_around_assignment_operators = true
ij_php_spaces_around_bitwise_operators = true
ij_php_spaces_around_equality_operators = true
ij_php_spaces_around_logical_operators = true
ij_php_spaces_around_multiplicative_operators = true
ij_php_spaces_around_null_coalesce_operator = true
ij_php_spaces_around_relational_operators = true
ij_php_spaces_around_shift_operators = true
ij_php_spaces_around_unary_operator = false
ij_php_spaces_around_var_within_brackets = false
ij_php_spaces_within_array_initializer_braces = false
ij_php_spaces_within_brackets = false
ij_php_spaces_within_catch_parentheses = false
ij_php_spaces_within_for_parentheses = false
ij_php_spaces_within_if_parentheses = false
ij_php_spaces_within_method_call_parentheses = false
ij_php_spaces_within_method_parentheses = false
ij_php_spaces_within_parentheses = false
ij_php_spaces_within_short_echo_tags = true
ij_php_spaces_within_switch_parentheses = false
ij_php_spaces_within_while_parentheses = false
ij_php_special_else_if_treatment = false
ij_php_subpackage_weight = 28
ij_php_ternary_operation_signs_on_next_line = true
ij_php_ternary_operation_wrap = normal
ij_php_throws_weight = 2
ij_php_todo_weight = 28
ij_php_unknown_tag_weight = 28
ij_php_upper_case_boolean_const = false
ij_php_upper_case_null_const = false
ij_php_uses_weight = 28
ij_php_var_weight = 28
ij_php_variable_naming_style = camel_case
ij_php_version_weight = 28
ij_php_while_brace_force = always
ij_php_while_on_new_line = false


================================================
FILE: .gitattributes
================================================
/.editorconfig           export-ignore
/.gitattributes          export-ignore
/.github                 export-ignore
/.gitignore              export-ignore
/art                     export-ignore
/phpstan.neon.dist       export-ignore
/phpunit.xml.dist        export-ignore
/pint.json               export-ignore
/tests                   export-ignore
/UPGRADING.md            export-ignore


================================================
FILE: .github/workflows/test.yml
================================================
name: Automated testing

on:
  - push
  - pull_request

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: true
      matrix:
        php: [8.2, 8.3, 8.4, 8.5]
        laravel: [11, 12, 13]
        include:
          - laravel: 11
            testbench: 9
          - laravel: 12
            testbench: 10
          - laravel: '13'
            testbench: '11'
        exclude:
          - laravel: '13'
            php: 8.2

    name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          ini-values: error_reporting=E_ALL
          tools: composer:v2
          coverage: none

      - name: Install dependencies
        run: |
          composer require "illuminate/contracts:^${{ matrix.laravel }}" "orchestra/testbench:^${{ matrix.testbench }}" --no-interaction --no-update
          composer update --prefer-dist --no-interaction --no-progress

      - name: Execute tests
        run: composer test


================================================
FILE: .gitignore
================================================
.idea
.php_cs
.php_cs.cache
.phpunit.cache
.phpunit.result.cache
composer.lock
phpunit.xml
phpstan.neon
vendor
node_modules
.php-cs-fixer.cache


================================================
FILE: .stubs.php
================================================
<?php declare(strict_types=1);

namespace Illuminate\Http
{
    class RedirectResponse
    {
        public function error(string $message, array $replace = []): RedirectResponse {}

        public function info(string $message, array $replace = []): RedirectResponse {}

        public function success(string $message, array $replace = []): RedirectResponse {}

        public function warning(string $message, array $replace = []): RedirectResponse {}
    }
}

namespace Illuminate\Routing
{
    class Redirector
    {
        public function error(string $message, array $replace = []): Redirector {}

        public function info(string $message, array $replace = []): Redirector {}

        public function success(string $message, array $replace = []): Redirector {}

        public function warning(string $message, array $replace = []): Redirector {}
    }
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to `livewire-toaster` will be documented in this file.

## 2.9.0 - 2026-01-17

### Added

- Livewire 4 support

## 2.8.0 - 2025-05-02

### Added

- Tailwind 4 support

## 2.7.0 - 2025-02-25

### Added

- Laravel 12 support

## 2.6.0 - 2024-12-01

### Added

- PHP 8.4 support

## 2.5.0 - 2024-11-25

### Added

- Replacement of similar toasts

## 2.4.0 - 2024-11-24

### Added

- Suppression of duplicate toasts

## 2.3.2 - 2024-11-06

### Fixed

- Use the Livewire relay if redirecting using navigate

## 2.3.1 - 2024-07-21

### Fixed

- Removed lingering #[Override] attribute

## 2.3.0 - 2024-07-20

### Added

- PHP 8.2 support

## 2.2.1 - 2024-04-09

### Fixed

- Prevent error in toast disposal when `$el` is `null`

## 2.2.0 - 2024-03-11

### Added

- Laravel 11 support

### Removed

- Laravel 10 support

## 2.1.0 - 2023-12-13

### Added

- PHP 8.3 support

### Removed

- PHP 8.2 support

## 2.0.3 - 2023-09-28

- Prevent unfinalize usage

## 2.0.2 - 2023-09-23

### Fixed

- Add Octane support by @yehorherasymchuk in [#23](https://github.com/masmerise/livewire-toaster/pull/23)

## 2.0.1 - 2023-09-04

### Fixed

- Dispatch events on the `document` node instead of `window`

## 2.0.0 - 2023-08-24

### Added

- Livewire 3 support

### Removed

- Livewire 2 support

## 1.3.0 - 2023-08-05

### Added

- RTL support

## 1.2.1 - 2023-07-03

### Fixed

- `scope` the `toaster` service in order to support Laravel Octane

## 1.2.0 - 2023-06-13

### Added

- Middle alignment by @aldozumaran in [#9](https://github.com/masmerise/livewire-toaster/pull/9)

## 1.1.2 - 2023-05-08

### Fixed

- Register `ToastableMacros` with the `Redirector`

## 1.1.1 - 2023-04-28

- (Performance) improvements

## 1.1.0 - 2023-04-21

### Added

- Vertically alignable toast container

## 1.0.0 - 2023-02-25

- Stable release


================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)

Copyright (c) Muhammed Sari <support@muhammedsari.me>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center"><img src="https://github.com/masmerise/livewire-toaster/raw/master/art/banner.png" alt="Toaster Banner"></p>

# Beautiful toast notifications for Livewire

[![Latest Version on Packagist](https://img.shields.io/packagist/v/masmerise/livewire-toaster.svg?style=flat-square)](https://packagist.org/packages/masmerise/livewire-toaster)
[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/masmerise/livewire-toaster/test.yml?branch=master)](https://github.com/masmerise/livewire-toaster/actions?query=workflow%3A%22Automated+testing%22+branch%3Amaster)
[![Total Downloads](https://img.shields.io/packagist/dt/masmerise/livewire-toaster.svg?style=flat-square)](https://packagist.org/packages/masmerise/livewire-toaster)

**Toaster** provides a seamless experience to display toast notifications in your Livewire powered Laravel apps.

Unlike many other toast implementations that are available, Toaster makes it effortless to dispatch a toast notification
from either a standard `Controller` or a Livewire `Component`. You don't have to think about "flashing" things to the
session or "dispatching browser events" from your Livewire components. Just dispatch your toast and Toaster will route the message accordingly.

## Showcase

<p align="center"><img src="https://github.com/masmerise/livewire-toaster/raw/master/art/showcase.gif" alt="Toaster Demo"></p>

## Compatibility

<table>
<tr><th>Livewire</th><th>PHP</th><th>Laravel</th></tr>
<tr><td>

| | [2](https://laravel-livewire.com/docs/2.x) | [3 & 4](https://livewire.laravel.com/docs) |
|-|-|-|
| [1.x](https://github.com/masmerise/livewire-toaster/tree/1.3.0) | ✅ | ❌                                          |
| 2.x | ❌ | ✅ |


</td><td>

| | 8.2 | 8.3 | 8.4 | 8.5
|-|-|-|-|-|
| 1.0 - ∞ | ✅ | ✅ | ✅ | ✅ |

</td><td>

| | 10 | 11 | 12 | 13
|-|-|-|-|-|
| 1.0 - 2.1 * | ✅ | ❌ | ❌ | ❌
| 2.2 - ∞ | ❌ | ✅ | ✅ | ✅

</tr> </table>

_* feature complete_

## Contents

**Looking for v1 docs?** [Click here](https://github.com/masmerise/livewire-toaster/tree/1.3.0).

- [Installation](#installation)
  - [Preparing your template](#preparing-your-template)
  - [Configuring scripts](#configuring-scripts)
  - [Tailwind styles](#tailwind-styles)
  - [RTL support](#rtl-support)
- [Usage](#usage)
  - [Sending toasts from the back-end](#sending-toasts-from-the-back-end)
  - [Sending toasts from the front-end](#sending-toasts-from-the-front-end)
  - [Automatic translation of messages](#automatic-translation-of-messages)
  - [Accessibility](#accessibility)
  - [Replacing similar toasts](#replacing-similar-toasts)
  - [Suppressing duplicate toasts](#suppressing-duplicate-toasts)
  - [Unit testing](#unit-testing)
  - [Extending behavior](#extending-behavior)
- [View customization](#view-customization)
- [Testing](#testing)
- [Changelog](#changelog)
- [Security](#security)
- [Credits](#credits)
- [License](#license)

## Installation

You can install the package via [composer](https://getcomposer.org):

```bash
composer require masmerise/livewire-toaster
```

You can publish the package's config file:

```bash
php artisan vendor:publish --tag=toaster-config
```

This is the contents of the `toaster.php` config file:

```php
return [

    /**
     * Add an additional second for every 100th word of the toast messages.
     *
     * Supported: true | false
     */
    'accessibility' => true,

    /**
     * The vertical alignment of the toast container.
     *
     * Supported: "bottom", "middle" or "top"
     */
    'alignment' => 'bottom',

    /**
     * Allow users to close toast messages prematurely.
     *
     * Supported: true | false
     */
    'closeable' => true,

    /**
     * The on-screen duration of each toast.
     *
     * Minimum: 3000 (in milliseconds)
     */
    'duration' => 3000,

    /**
     * The horizontal position of each toast.
     *
     * Supported: "center", "left" or "right"
     */
    'position' => 'right',

    /**
     * New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.
     * Takes precedence over the "suppress" option.
     *
     * Supported: true | false
     */
    'replace' => false,

    /**
     * Prevent the display of duplicate toast messages.
     *
     * Supported: true | false
     */
    'suppress' => false,

    /**
     * Whether messages passed as translation keys should be translated automatically.
     *
     * Supported: true | false
     */
    'translate' => true,
];
```

### Preparing your template

Next, you'll need to use the `<x-toaster-hub />` component in your master template:

```html
<!DOCTYPE html>
<html>
<head>
    <!-- ... -->
</head>

<body>
    <!-- Application content -->

    <x-toaster-hub /> <!-- 👈 -->
</body>
</html>
```

### Configuring scripts

After that, you'll need to import `Toaster` at the top of your `resources/js/app.js` bundle to start listening to incoming toasts:

```js
import './bootstrap';
import '../../vendor/masmerise/livewire-toaster/resources/js'; // 👈

// other app stuff...
```

### Tailwind styles

> [!NOTE]
> Skip this step if you're going to customize Toaster's default view.

Toaster provides a minimal view that utilizes Tailwind CSS defaults.

If the default toast appearances suffice your needs, you'll need to register it with Tailwind's purge list:

For Tailwind CSS Version < 4.x
```js
module.exports = {
    content: [
        './resources/**/*.blade.php',
        './vendor/masmerise/livewire-toaster/resources/views/*.blade.php', // 👈
    ],
}
```

For Tailwind CSS V >= 4+ you'll need to add this to your `app.css` file:

> [!NOTE]
> Tailwind CSS v4.0 introduced a major change where you define your source content files directly in the main CSS entry point using the `@source` directive. This is the **most modern and recommended approach** for v4+.

```css
@import "tailwindcss";
...
@source '../../vendor/masmerise/livewire-toaster/resources/views/*.blade.php'; /* 👈 */
```
or into the file `tailwind.config.js`:

> [!NOTE]
> The `content` array still exists and functions in Tailwind CSS 4+. For many existing projects or frameworks, defining the paths here is a fallback or continued method. The config file itself is correctly updated to use the modern ESM `export default` syntax.

```js
/** @type {import('tailwindcss').Config} */
export default {
    content: [
        "./resources/**/*.blade.php",
        "./resources/**/*.js",
        "./resources/**/*.vue",
        "./vendor/masmerise/livewire-toaster/resources/views/*.blade.php", // 👈
    ],
    plugins: [],
};
```

Otherwise, please refer to [View customization](#view-customization).

### RTL support

> [!NOTE]
> **LTR** will be assumed regardless of whether you apply the `ltr` attribute or not.

If your app makes use of an **RTL** language such as Arabic and Hebrew, don't forget to add the `rtl` attribute to the document root:

```html
<!DOCTYPE html>
<html dir="rtl"> <!-- 👈 -->
    ...
</html>
```

This will make sure the UI elements (such as the close button) are flipped and the text is properly aligned.

## Usage

### Sending toasts from the back-end

> [!NOTE]
> Toaster supports the dispatch of multiple toasts at once, you are not limited to dispatching a single toast.

#### Toaster

The standard recommended way for dispatching toast messages is through the `Toaster` facade.

```php
use Masmerise\Toaster\Toaster;

final class RegistrationForm extends Component
{
    public function submit(): void
    {
        $this->validate();

        User::create($this->form);

        Toaster::success('User created!'); // 👈
    }
}
```

If you need fine-grained control, you can always use the `PendingToast` class directly to which `Toaster` proxies its calls:

```php
use Masmerise\Toaster\PendingToast;

final class RegistrationForm extends Component
{
    public function submit(): void
    {
        $this->validate();

        $user = User::create($this->form);

        // 👇
        PendingToast::create()
            ->when($user->isAdmin(),
                fn (PendingToast $toast) => $toast->message('Admin created')
            )
            ->unless($user->isAdmin(),
                fn (PendingToast $toast) => $toast->message('User created')
            )
            ->success();
    }
}
```

#### Toastable

You can make any class `Toastable` to dispatch toasts from:

```php
use Masmerise\Toaster\Toastable;

final class ProductListing extends Component
{
    use Toastable; // 👈

    public function check(): void
    {
        $result = Product::query()
            ->tap(new Available())
            ->count();

        if ($result < 5) {
            $this->warning('The quantity on hand is critically low.'); // 👈
        }
    }
}
```

#### Redirects

Whenever you return a `RedirectResponse` from anywhere in your app, you can chain any of the `Toaster` methods
to dispatch a toast message:

```php
final class CompanyController extends Controller
{
    /** @throws ValidationException */
    public function store(Request $request): RedirectResponse
    {
        $validator = Validator::make($request->all(), [...]);

        if ($validator->fails()) {
            return Redirect::back()
                ->error('The form contains several errors'); // 👈
        }

        Company::create($validator->validate());

        return Redirect::route('dashboard')
            ->info('Company created!'); // 👈
    }
}
```

This is, of course, **not** limited to `Controller`s as you can also redirect in Livewire `Component`s.

#### Dependency injection

If you'd like to keep things "pure", you can also inject the `Collector` contract
and use the `ToastBuilder` to dispatch your toasts:

```php
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\ToasterConfig;
use Masmerise\Toaster\ToastBuilder;

final readonly class SendEmailVerifiedNotification
{
    public function __construct(
        private ToasterConfig $config,
        private Collector $toasts,
    ) {}

    public function handle(Verified $event): void
    {
        $toast = ToastBuilder::create()
            ->duration($this->config->duration)
            ->success()
            ->message("Thank you, {$event->user->name}!")
            ->get();

        $this->toasts->collect($toast);
    }
}
```

### Sending toasts from the front-end

You can invoke the globally available `Toaster` instance to dispatch any toast message from anywhere:

```html
<button @click="Toaster.success('Form submitted!')">
    Submit
</button>
```

Available methods: `error`, `info`, `warning` & `success`

### Automatic translation of messages

> [!NOTE]
> The `translate` configuration value must be set to `true`.

Instead of doing this:

```php
Toaster::success(
    Lang::get('path.to.translation', ['replacement' => 'value'])
);
```

Toaster makes it possible to do this:

```php
Toaster::success('path.to.translation', ['replacement' => 'value']);
```

You can mix and match without any problems:

```php
Toaster::info('user.created', ['name' => $user->full_name]);
Toaster::info('You now have full access!');
```

You can do whatever you want, whenever you want.

### Accessibility

> [!NOTE]
> The `accessibility` configuration value must be set to `true`.

Toaster will add an additional second to a toast's on-screen duration for every 100th word.
This way, your users will have enough time to read toasts that are a tad larger than usual.

So, if your base duration value is `3 seconds` and your toast contains 223 words,
the total on-screen duration of the toast will be `3 + 2 = 5 seconds`

### Replacing similar toasts

> [!NOTE]
> The `replace` configuration value must be set to `true`.

> [!WARNING]
> Takes precedence over `suppress`.

Toaster will dispose of any toast that is similar to the one being dispatched prior to displaying the new toast.
A toast is considered similar if it has the same `duration`, `message`, and `type`.

### Suppressing duplicate toasts

> [!NOTE]
> The `suppress` configuration value must be set to `true`.

Toaster will prevent the display of duplicate toast messages while another toast with the same message is still on-screen.
A toast is considered a duplicate if it has the same `duration`, `message`, and `type`.

### Unit testing

> [!NOTE]
> If you make use of [automatic translation of messages](#automatic-translation-of-messages), you should assert whether the **translation keys** are passed along correctly instead of the human readable messages that are replaced by Laravel's translator.
> Otherwise, your tests are going to fail as the messages are not translated during unit testing.

Toaster provides a couple of testing capabilities in order for you to build a robust application:

```php
use Masmerise\Toaster\Toaster;

final class RegisterUserControllerTest extends TestCase
{
    #[Test]
    public function users_can_register(): void
    {
        // Arrange
        Toaster::fake();
        Toaster::assertNothingDispatched();

        // Act
        $response = $this->post('users', [ ... ]);

        // Assert
        $response->assertRedirect('profile');
        Toaster::assertDispatched('Welcome!');
    }
}
```

### Extending behavior

Imagine that you'd like to keep track of how many toasts are dispatched daily to display on an admin dashboard.
First, create a new class that encapsulates this logic:

```php
final readonly class DailyCountingCollector implements Collector
{
    public function __construct(private Collector $next) {}

    public function collect(Toast $toast): void
    {
        // increment the counter on durable storage

        $this->next->collect($toast);
    }

    public function release(): array
    {
        return $this->next->release();
    }
}
```

After that, extend the behavior in your `AppServiceProvider`:

```php
public function register(): void
{
    $this->app->extend(Collector::class,
        static fn (Collector $next) => new DailyCountingCollector($next)
    );
}
```

That's it!

## View customization

Even though the default toasts are pretty, they might not fit your design and you may want to customize them.

You can do so by publishing Toaster's views:

```php
php artisan vendor:publish --tag=toaster-views
```

The `hub.blade.php` view will be published to your application's `resources/views/vendor/toaster` directory.
Feel free to modify anything to your liking.

### Available `viewData`

- `$alignment` - can be used to align the toast container vertically depending on the configuration
- `$closeable` - whether the close button should be rendered by the Blade component
- `$config` - default configuration values, used by the Alpine component
- `$position` - can be used to position the toasts depending on the configuration
- `$toasts` - toasts that were flashed to the session by Toaster, used by the Alpine component

> [!WARNING]
> You **must** keep the `x-data` and `x-init` directives and you **must** keep using the `x-for` loop.
> Otherwise, the Alpine component that powers Toaster will start malfunctioning.


## Testing

```bash
composer test
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Security

If you discover any security related issues, please email support@muhammedsari.me instead of using the issue tracker.

## Credits

- [Muhammed Sari](https://github.com/masmerise)
- [Greg Korba](https://github.com/wirone)
- [All Contributors](../../contributors)

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.


================================================
FILE: UPGRADING.md
================================================
# Upgrade Guide

## v1 → v2

### Minimum versions

The following dependency versions have been updated:

- The minimum Livewire version is now v3.0

### Adjusting the `Toaster` import

`Alpine` is now registered by Livewire automatically. Therefore, any direct `Alpine.plugin` or `Alpine.start` call will no longer work.
You should adjust your `app.js` JavaScript entry point to account for this change. An example:

```diff
import './bootstrap';
+ import '../../vendor/masmerise/livewire-toaster/resources/js';

- import Alpine from 'alpinejs';
- import Toaster from '../../vendor/masmerise/livewire-toaster/resources/js';

- Alpine.plugin(Toaster);

- window.Alpine = Alpine;
- Alpine.start();
```


================================================
FILE: composer.json
================================================
{
    "name": "masmerise/livewire-toaster",
    "description": "Beautiful toast notifications for Laravel / Livewire.",
    "license": "MIT",
    "keywords": [
        "alert",
        "laravel",
        "livewire",
        "toast",
        "toaster"
    ],
    "authors": [
        {
            "name": "Muhammed Sari",
            "email": "support@muhammedsari.me",
            "role": "Developer"
        }
    ],
    "homepage": "https://github.com/masmerise/livewire-toaster",
    "require": {
        "php": "~8.2 || ~8.3 || ~8.4 || ~8.5",
        "illuminate/contracts": "^11.0 || ^12.0 || ^13.0",
        "illuminate/http": "^11.0 || ^12.0 || ^13.0",
        "illuminate/routing": "^11.0 || ^12.0 || ^13.0",
        "illuminate/support": "^11.0 || ^12.0 || ^13.0",
        "illuminate/view": "^11.0 || ^12.0 || ^13.0",
        "livewire/livewire": "^3.0 || ^4.0"
    },
    "require-dev": {
        "larastan/larastan": "^2.0 || ^3.1",
        "laravel/pint": "^1.0",
        "orchestra/testbench": "^9.0 || ^10.0 || ^11.0",
        "phpunit/phpunit": "^10.0 || ^11.5.3 || ^12.5.12"
    },
    "conflict": {
        "stevebauman/unfinalize": "*"
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "autoload": {
        "psr-4": {
            "Masmerise\\Toaster\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests"
        }
    },
    "config": {
        "sort-packages": true
    },
    "extra": {
        "laravel": {
            "aliases": {
                "Toaster": "Masmerise\\Toaster\\Toaster"
            },
            "providers": [
                "Masmerise\\Toaster\\ToasterServiceProvider"
            ]
        }
    },
    "scripts": {
        "format": "vendor/bin/pint",
        "larastan": "vendor/bin/phpstan analyse --memory-limit=2G",
        "test": "vendor/bin/phpunit",
        "verify": [
            "@larastan",
            "@test"
        ]
    }
}


================================================
FILE: config/toaster.php
================================================
<?php declare(strict_types=1);

return [

    /**
     * Add an additional second for every 100th word of the toast messages.
     *
     * Supported: true | false
     */
    'accessibility' => true,

    /**
     * The vertical alignment of the toast container.
     *
     * Supported: "bottom", "middle" or "top"
     */
    'alignment' => 'bottom',

    /**
     * Allow users to close toast messages prematurely.
     *
     * Supported: true | false
     */
    'closeable' => true,

    /**
     * The on-screen duration of each toast.
     *
     * Minimum: 3000 (in milliseconds)
     */
    'duration' => 3000,

    /**
     * The horizontal position of each toast.
     *
     * Supported: "center", "left" or "right"
     */
    'position' => 'right',

    /**
     * New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.
     * Takes precedence over the "suppress" option.
     *
     * Supported: true | false
     */
    'replace' => false,

    /**
     * Prevent the display of duplicate toast messages.
     *
     * Supported: true | false
     */
    'suppress' => false,

    /**
     * Whether messages passed as translation keys should be translated automatically.
     *
     * Supported: true | false
     */
    'translate' => true,
];


================================================
FILE: phpstan.neon.dist
================================================
includes:
    - ./vendor/larastan/larastan/extension.neon

parameters:
    paths:
        - src

    level: 9

    ignoreErrors:
        - identifier: missingType.generics
        - identifier: missingType.iterableValue
        - '#Cannot access offset .* on Illuminate\\Contracts\\Foundation\\Application#'
        - '#Parameter \#.* of class Masmerise\\Toaster\\ToasterConfig constructor expects .*, mixed given.#'
        - '#Trait Masmerise\\Toaster\\Toastable is used zero times and is not analysed.#'


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
    bootstrap="vendor/autoload.php"
    colors="true"
    executionOrder="random"
    failOnWarning="true"
    failOnRisky="true"
    failOnEmptyTestSuite="true"
    beStrictAboutOutputDuringTests="true"
    cacheDirectory=".phpunit.cache"
>
    <testsuites>
        <testsuite name="Toaster Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_KEY" value="base64:TaUN5M30kEy4U+fj7RDG0+ppdVWwI9apTf1a0inKdP0="/>
        <env name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>


================================================
FILE: pint.json
================================================
{
    "preset": "psr12",
    "rules": {
        "blank_line_after_opening_tag": false,
        "blank_line_before_statement": { "statements": ["return"] },
        "braces": { "allow_single_line_closure": true },
        "concat_space": { "spacing": "one" },
        "declare_strict_types": true,
        "method_argument_space": false,
        "no_extra_blank_lines": { "tokens":  ["curly_brace_block", "extra", "throw", "use"] },
        "no_useless_else": true,
        "not_operator_with_successor_space": true,
        "ordered_imports": { "imports_order": ["class", "function", "const"], "sort_algorithm": "alpha" },
        "single_line_empty_body": true,
        "trailing_comma_in_multiline": true
    }
}


================================================
FILE: resources/js/config.js
================================================
class Alignment {
    static Top = 'top';

    constructor(value) {
        this.value = value;
    }

    isTop() {
        return this.value === Alignment.Top;
    }
}

export class Config {
    constructor(alignment, duration, replace, suppress) {
        this.alignment = new Alignment(alignment);
        this.duration = duration;
        this.replace = replace;
        this.suppress = suppress;
    }

    static fromJson(data) {
        return new Config(data.alignment, data.duration, data.replace, data.suppress);
    }
}


================================================
FILE: resources/js/hub.js
================================================
import { Config } from './config';
import { Toast } from './toast';

export function Hub(Alpine) {
    Alpine.data('toasterHub', (initialToasts, config) => {
        config = Config.fromJson(config);

        return {
            _toasts: [],

            get toasts() {
                const toasts = this._toasts.filter(t => ! t.trashed);

                if (this._toasts.length && ! toasts.length) {
                    this.$nextTick(() => { this._toasts = []; });
                }

                return toasts;
            },

            init() {
                document.addEventListener('toaster:received', event => {
                    this.processToast(event);
                });

                initialToasts.map(Toast.fromJson).forEach(toast => this.show(toast));
            },
            
            processToast(data) {
                const toast = Toast.fromJson({ duration: config.duration, ...data.detail });
                
                if (config.replace) {
                    this.toasts.filter(t => t.equals(toast)).forEach(t => t.dispose());
                } else if (config.suppress && this.toasts.some(t => t.equals(toast))) {
                    return;
                }

                this.show(toast);
            },
            
            show(toast) {
                toast = Alpine.reactive(toast);
                toast.runAfterDuration(toast => toast.dispose());

                if (config.alignment.isTop()) {
                    this._toasts.unshift(toast);
                } else {
                    this._toasts.push(toast);
                }
            },
        }
    });
}


================================================
FILE: resources/js/index.js
================================================
import { Hub } from './hub';
import * as Toaster from './toaster';

window.Toaster = Toaster;

document.addEventListener('alpine:init', () => {
    window.Alpine.plugin(Hub);
});


================================================
FILE: resources/js/toast.js
================================================
import { uuid41 } from './uuid41';

export class Toast {
    constructor(duration, message, type) {
        this.$el = null;
        this.id = uuid41();
        this.isVisible = false;
        this.duration = duration;
        this.message = message;
        this.timeout = null;
        this.trashed = false;
        this.type = type;
    }

    static fromJson(data) {
        return new Toast(data.duration, data.message, data.type);
    }

    dispose() {
        if (this.timeout) {
            clearTimeout(this.timeout);
        }

        this.isVisible = false;

        if (this.$el) {
            this.$el.addEventListener('transitioncancel', () => { this.trashed = true; })
            this.$el.addEventListener('transitionend', () => { this.trashed = true; })
        }
    }

    equals(other) {
        return this.duration === other.duration
            && this.message === other.message
            && this.type === other.type;
    }

    runAfterDuration(callback) {
        this.timeout = setTimeout(() => callback(this), this.duration);
    }

    select(config) {
        return config[this.type];
    }

    show($el) {
        this.$el = $el;
        this.isVisible = true;
    }
}


================================================
FILE: resources/js/toaster.js
================================================
const event = (message, type) => {
    document.dispatchEvent(new CustomEvent('toaster:received', { detail: { message, type }}));
};

const error = message => event(message, 'error');
const info = message => event(message, 'info');
const success = message => event(message, 'success');
const warning = message => event(message, 'warning');

export { error, info, success, warning }


================================================
FILE: resources/js/uuid41.js
================================================
export function uuid41() {
    let d = '';

    while (d.length < 32) {
        d += Math.random().toString(16).substring(2);
    }

    const vr = ((Number.parseInt(d.substring(16, 1), 16) & 0x3) | 0x8).toString(16);

    return `${d.substring(0, 8)}-${d.substring(8, 4)}-4${d.substring(13, 3)}-${vr}${d.substring(17, 3)}-${d.substring(20, 12)}`;
}


================================================
FILE: resources/views/close-button.blade.php
================================================
<button @click="toast.dispose()" aria-label="@lang('close')" class="absolute right-0 p-2 focus:outline-none focus:outline-hidden rtl:right-auto rtl:left-0 {{ $alignment->is('bottom') ? 'top-3' : 'top-0' }}">
    <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
    </svg>
</button>

================================================
FILE: resources/views/hub.blade.php
================================================
<div role="status" id="toaster" x-data="toasterHub(@js($toasts), @js($config))" @class([
    'fixed z-50 p-4 w-full flex flex-col pointer-events-none sm:p-6',
    'bottom-0' => $alignment->is('bottom'),
    'top-1/2 -translate-y-1/2' => $alignment->is('middle'),
    'top-0' => $alignment->is('top'),
    'items-start rtl:items-end' => $position->is('left'),
    'items-center' => $position->is('center'),
    'items-end rtl:items-start' => $position->is('right'),
 ])>
    <template x-for="toast in toasts" :key="toast.id">
        <div x-show="toast.isVisible"
             x-init="$nextTick(() => toast.show($el))"
             @if($alignment->is('bottom'))
             x-transition:enter-start="translate-y-12 opacity-0"
             x-transition:enter-end="translate-y-0 opacity-100"
             @elseif($alignment->is('top'))
             x-transition:enter-start="-translate-y-12 opacity-0"
             x-transition:enter-end="translate-y-0 opacity-100"
             @else
             x-transition:enter-start="opacity-0 scale-90"
             x-transition:enter-end="opacity-100 scale-100"
             @endif
             x-transition:leave-end="opacity-0 scale-90"
             @class(['relative duration-300 transform transition ease-in-out max-w-xs w-full pointer-events-auto', 'text-center' => $position->is('center')])
             :class="toast.select({ error: 'text-white', info: 'text-black', success: 'text-white', warning: 'text-white' })"
        >
            <i x-text="toast.message"
               class="inline-block select-none not-italic px-6 py-3 rounded rounded-sm shadow-lg text-sm w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
               :class="toast.select({ error: 'bg-red-500', info: 'bg-gray-200', success: 'bg-green-600', warning: 'bg-orange-500' })"
            ></i>

            @includeWhen($closeable, 'toaster::close-button')
        </div>
    </template>
</div>


================================================
FILE: src/AccessibleCollector.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @internal */
final readonly class AccessibleCollector implements Collector
{
    private const AMOUNT_OF_WORDS = 100;
    private const ONE_SECOND = 1000;

    public function __construct(private Collector $next) {}

    public function collect(Toast $toast): void
    {
        $addend = (int) floor(str_word_count($toast->message->value) / self::AMOUNT_OF_WORDS);
        $addend = $addend * self::ONE_SECOND;

        if ($addend > 0) {
            $toast = ToastBuilder::proto($toast)->duration($toast->duration->value + $addend)->get();
        }

        $this->next->collect($toast);
    }

    public function release(): array
    {
        return $this->next->release();
    }
}


================================================
FILE: src/Alignment.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @internal */
enum Alignment: string
{
    use Assertable;

    case Bottom = 'bottom';
    case Middle = 'middle';
    case Top = 'top';
}


================================================
FILE: src/Assertable.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @interal */
trait Assertable
{
    public function is(string $value): bool
    {
        return $value === $this->value;
    }
}


================================================
FILE: src/Collector.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

interface Collector
{
    public function collect(Toast $toast): void;

    /** @internal */
    public function release(): array;
}


================================================
FILE: src/Duration.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use InvalidArgumentException;

/** @internal */
final readonly class Duration
{
    private const MINIMUM = 3000;

    public int $value;

    private function __construct(int $value)
    {
        if ($value < self::MINIMUM) {
            throw new InvalidArgumentException('The duration value must be at least 3000 ms.');
        }

        $this->value = $value;
    }

    public static function fromMillis(int $value): self
    {
        return new self($value);
    }
}


================================================
FILE: src/LivewireRelay.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Livewire\Component;
use Livewire\Features\SupportEvents\Event;
use Livewire\Livewire;
use Livewire\Mechanisms\HandleComponents\ComponentContext;

use function Livewire\store;

/** @internal */
final readonly class LivewireRelay
{
    public const EVENT = 'toaster:received';

    public function __invoke(Component $component, ComponentContext $ctx): void
    {
        if (! Livewire::isLivewireRequest()) {
            return;
        }

        $isRedirecting = store($component)->get('redirect');
        $isRedirectingUsingNavigate = store($component)->get('redirectUsingNavigate');

        if ($isRedirecting && ! $isRedirectingUsingNavigate) {
            return;
        }

        if ($toasts = Toaster::release()) {
            foreach ($toasts as $toast) {
                $event = new Event(self::EVENT, $toast->toArray());
                $ctx->pushEffect('dispatches', $event->serialize());
            }
        }
    }
}


================================================
FILE: src/Message.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use InvalidArgumentException;

/** @internal */
final readonly class Message
{
    public array $replace;

    public string $value;

    private function __construct(string $value, array $replace = [])
    {
        if (empty($value = trim($value))) {
            throw new InvalidArgumentException('The message value cannot be empty.');
        }

        $this->value = $value;
        $this->replace = $replace;
    }

    public static function fromString(string $value): self
    {
        return new self($value);
    }

    public static function fromTranslatable(string $value, array $replace = []): self
    {
        return new self($value, $replace);
    }

    public function equals(Message|string $other): bool
    {
        if ($other instanceof Message) {
            $other = $other->value;
        }

        return $other === $this->value;
    }
}


================================================
FILE: src/PendingToast.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Support\Traits\ForwardsCalls;

/**
 * @method PendingToast duration(int $milliseconds)
 * @method PendingToast error()
 * @method PendingToast info()
 * @method PendingToast message(string $message, array $replace = [])
 * @method PendingToast success()
 * @method PendingToast type(string $type)
 * @method PendingToast unless($value = null, callable $callback = null, callable $default = null)
 * @method PendingToast warning()
 * @method PendingToast when($value = null, callable $callback = null, callable $default = null)
 */
final class PendingToast
{
    use ForwardsCalls;

    private ToastBuilder $builder;

    private bool $dispatched = false;

    private function __construct(int $duration)
    {
        $this->builder = ToastBuilder::create()->duration($duration);
    }

    public static function create(): self
    {
        return new self(Toaster::config()->duration);
    }

    public function dispatch(): void
    {
        $toast = $this->builder->get();

        Toaster::collect($toast);

        $this->dispatched = true;
    }

    public function __call(string $name, array $arguments): mixed
    {
        $result = $this->forwardCallTo($this->builder, $name, $arguments);

        if ($result instanceof ToastBuilder) {
            $this->builder = $result;

            return $this;
        }

        return $result;
    }

    public function __destruct()
    {
        if (! $this->dispatched) {
            $this->dispatch();
        }
    }
}


================================================
FILE: src/Position.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @internal */
enum Position: string
{
    use Assertable;

    case Center = 'center';
    case Left = 'left';
    case Right = 'right';
}


================================================
FILE: src/QueuingCollector.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @internal */
final class QueuingCollector implements Collector
{
    private array $toasts = [];

    public function collect(Toast $toast): void
    {
        $this->toasts[] = $toast;
    }

    public function release(): array
    {
        $toasts = $this->toasts;
        $this->toasts = [];

        return $toasts;
    }
}


================================================
FILE: src/SessionRelay.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;

/** @internal */
final readonly class SessionRelay
{
    public const NAME = 'toasts';

    public function __construct(private Application $app) {}

    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        if (! $this->app->resolved(Collector::class)) {
            return $response;
        }

        if ($toasts = $this->app[Collector::class]->release()) {
            $this->app[Session::class]->put(self::NAME, $this->serialize($toasts));
        }

        return $response;
    }

    private function serialize(array $toasts): array
    {
        return array_map(static fn (Toast $toast) => $toast->toArray(), $toasts);
    }
}


================================================
FILE: src/TestableCollector.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use PHPUnit\Framework\Assert as PHPUnit;

final class TestableCollector implements Collector
{
    private array $toasts = [];

    public function collect(Toast $toast): void
    {
        $this->toasts[] = $toast;
    }

    public function release(): array
    {
        $toasts = $this->toasts;
        $this->toasts = [];

        return $toasts;
    }

    public function assertDispatched(string $message): void
    {
        $toasts = array_filter($this->toasts, static fn (Toast $toast) => $toast->message->equals($message));

        PHPUnit::assertNotEmpty($toasts, "A toast with the message `{$message}` was not dispatched.");
    }

    public function assertNothingDispatched(): void
    {
        $count = count($this->toasts);

        PHPUnit::assertSame(0, $count, "{$count} unexpected toasts were dispatched.");
    }
}


================================================
FILE: src/Toast.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Contracts\Support\Arrayable;

final readonly class Toast implements Arrayable
{
    public function __construct(
        public Message $message,
        public Duration $duration,
        public ToastType $type,
    ) {}

    public function toArray(): array
    {
        return [
            'duration' => $this->duration->value,
            'message' => $this->message->value,
            'type' => $this->type->value,
        ];
    }
}


================================================
FILE: src/ToastBuilder.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Support\Traits\Conditionable;
use UnexpectedValueException;

final class ToastBuilder
{
    use Conditionable;

    private ?Duration $duration = null;

    private ?Message $message = null;

    private ?ToastType $type = null;

    public static function create(): self
    {
        return new self();
    }

    public static function proto(Toast $toast): self
    {
        $builder = self::create();
        $builder->duration = $toast->duration;
        $builder->message = $toast->message;
        $builder->type = $toast->type;

        return $builder;
    }

    public function duration(int $milliseconds): self
    {
        return $this->modify('duration', Duration::fromMillis($milliseconds));
    }

    public function error(): self
    {
        return $this->modify('type', ToastType::Error);
    }

    public function info(): self
    {
        return $this->modify('type', ToastType::Info);
    }

    public function message(string $message, array $replace = []): self
    {
        return $this->modify('message', Message::fromTranslatable($message, $replace));
    }

    public function success(): self
    {
        return $this->modify('type', ToastType::Success);
    }

    public function type(string $type): self
    {
        return $this->modify('type', ToastType::from($type));
    }

    public function warning(): self
    {
        return $this->modify('type', ToastType::Warning);
    }

    public function get(): Toast
    {
        if (! $this->duration instanceof Duration) {
            throw new UnexpectedValueException('You must provide a valid duration.');
        }

        if (! $this->message instanceof Message) {
            throw new UnexpectedValueException('You must provide a valid message.');
        }

        if (! $this->type instanceof ToastType) {
            throw new UnexpectedValueException('You must choose a valid type.');
        }

        return new Toast($this->message, $this->duration, $this->type);
    }

    private function modify(string $property, mixed $value): self
    {
        $that = clone $this;
        $that->{$property} = $value;

        return $that;
    }
}


================================================
FILE: src/ToastType.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

/** @internal */
enum ToastType: string
{
    case Error = 'error';
    case Info = 'info';
    case Success = 'success';
    case Warning = 'warning';
}


================================================
FILE: src/Toastable.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

trait Toastable
{
    protected function error(string $message, array $replace = []): PendingToast
    {
        return Toaster::error($message, $replace);
    }

    protected function info(string $message, array $replace = []): PendingToast
    {
        return Toaster::info($message, $replace);
    }

    protected function success(string $message, array $replace = []): PendingToast
    {
        return Toaster::success($message, $replace);
    }

    protected function toast(): PendingToast
    {
        return Toaster::toast();
    }

    protected function warning(string $message, array $replace = []): PendingToast
    {
        return Toaster::warning($message, $replace);
    }
}


================================================
FILE: src/ToastableMacros.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Closure;

/** @internal */
final readonly class ToastableMacros
{
    protected function error(): Closure
    {
        return $this->macro('error');
    }

    protected function info(): Closure
    {
        return $this->macro('info');
    }

    protected function success(): Closure
    {
        return $this->macro('success');
    }

    protected function warning(): Closure
    {
        return $this->macro('warning');
    }

    private function macro(string $type): Closure
    {
        return function (string $message, array $replace = []) use ($type) {
            Toaster::toast()->type($type)->message($message, $replace);

            return $this;
        };
    }
}


================================================
FILE: src/Toaster.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Support\Facades\Facade;

/**
 * @method static void assertDispatched(string $message)
 * @method static void assertNothingDispatched()
 * @method static void collect(Toast $toast)
 * @method static array release()
 */
final class Toaster extends Facade
{
    public static function config(): ToasterConfig
    {
        return self::$app[ToasterServiceProvider::CONFIG];
    }

    public static function error(string $message, array $replace = []): PendingToast
    {
        return self::toast()->message($message, $replace)->error();
    }

    public static function fake(): TestableCollector
    {
        self::swap($fake = new TestableCollector());

        return $fake;
    }

    public static function info(string $message, array $replace = []): PendingToast
    {
        return self::toast()->message($message, $replace)->info();
    }

    public static function success(string $message, array $replace = []): PendingToast
    {
        return self::toast()->message($message, $replace)->success();
    }

    public static function toast(): PendingToast
    {
        return PendingToast::create();
    }

    public static function warning(string $message, array $replace = []): PendingToast
    {
        return self::toast()->message($message, $replace)->warning();
    }

    protected static function getFacadeAccessor(): string
    {
        return ToasterServiceProvider::NAME;
    }

    protected static function getMockableClass(): string
    {
        return Collector::class;
    }
}


================================================
FILE: src/ToasterConfig.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Support\Arr;

/** @internal */
final readonly class ToasterConfig
{
    private function __construct(
        public string $alignment,
        public int $duration,
        public string $position,
        public bool $wantsAccessibility,
        public bool $wantsCloseableToasts,
        public bool $wantsReplacement,
        public bool $wantsSuppression,
        public bool $wantsTranslation,
    ) {}

    /**
     * @param array{
     *     alignment?: "bottom" | "middle" | "top",
     *     duration?: int,
     *     position?: "center" | "left" | "right",
     *     accessibility?: bool,
     *     closeable?: bool,
     *     replace?: bool,
     *     suppress?: bool,
     *     translate?: bool,
     * } $config
     */
    public static function fromArray(array $config): self
    {
        return new self(
            Arr::get($config, 'alignment', 'bottom'),
            Arr::get($config, 'duration', 3000),
            Arr::get($config, 'position', 'right'),
            Arr::get($config, 'accessibility', true),
            Arr::get($config, 'closeable', true),
            Arr::get($config, 'replace', false),
            Arr::get($config, 'suppress', false),
            Arr::get($config, 'translate', true),
        );
    }

    public function alignment(): Alignment
    {
        return Alignment::from($this->alignment);
    }

    public function position(): Position
    {
        return Position::from($this->position);
    }

    public function toJavaScript(): array
    {
        return [
            'alignment' => $this->alignment,
            'duration' => $this->duration,
            'replace' => $this->wantsReplacement,
            'suppress' => $this->wantsSuppression,
        ];
    }
}


================================================
FILE: src/ToasterHub.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Contracts\Session\Session;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

/** @internal */
final class ToasterHub extends Component
{
    public const NAME = 'toaster-hub';

    public function __construct(
        private readonly ToasterConfig $config,
        private readonly Session $session,
        private readonly string $view = 'toaster::hub',
    ) {}

    public function render(): View
    {
        return $this->view($this->view, [
            'alignment' => $this->config->alignment(),
            'closeable' => $this->config->wantsCloseableToasts,
            'config' => $this->config->toJavaScript(),
            'position' => $this->config->position(),
            'toasts' => $this->session->pull(SessionRelay::NAME, []),
        ]);
    }
}


================================================
FILE: src/ToasterServiceProvider.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Routing\Router;
use Illuminate\Support\AggregateServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
use Livewire\LivewireManager;
use Livewire\LivewireServiceProvider;

final class ToasterServiceProvider extends AggregateServiceProvider
{
    public const CONFIG = 'toaster.config';
    public const NAME = 'toaster';

    protected $providers = [LivewireServiceProvider::class];

    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__ . '/../resources/views', self::NAME);

        if ($this->app->runningInConsole()) {
            $this->registerPublishing();
        }

        $this->callAfterResolving(BladeCompiler::class, $this->aliasToasterHub(...));
        $this->callAfterResolving(Collector::class, $this->relayToLivewire(...));
        $this->callAfterResolving(Router::class, $this->relayToSession(...));

        Redirector::mixin($macros = new ToastableMacros());
        RedirectResponse::mixin($macros);
    }

    public function register(): void
    {
        $config = $this->configureService();

        parent::register();

        $this->app->scoped(Collector::class, QueuingCollector::class);
        $this->app->alias(Collector::class, self::NAME);

        if ($config->wantsAccessibility) {
            $this->app->extend(Collector::class, static fn (Collector $next) => new AccessibleCollector($next));
        }

        if ($config->wantsTranslation) {
            $this->app->extend(Collector::class, fn (Collector $next) => new TranslatingCollector($next, $this->app['translator']));
        }
    }

    private function aliasToasterHub(BladeCompiler $blade): void
    {
        $blade->component(ToasterHub::NAME, ToasterHub::class);
    }

    private function configureService(): ToasterConfig
    {
        $this->mergeConfigFrom(__DIR__ . '/../config/toaster.php', self::NAME);

        $config = ToasterConfig::fromArray($this->app['config'][self::NAME] ?? []);
        $this->app->instance(ToasterConfig::class, $config);
        $this->app->alias(ToasterConfig::class, self::CONFIG);

        return $config;
    }

    private function registerPublishing(): void
    {
        $this->publishes([
            __DIR__ . '/../config/toaster.php' => $this->app->configPath('toaster.php'),
        ], 'toaster-config');

        $this->publishes([
            __DIR__ . '/../resources/views' => $this->app->resourcePath('views/vendor/toaster'),
        ], 'toaster-views');
    }

    private function relayToLivewire(): void
    {
        $this->app[LivewireManager::class]->listen('dehydrate', new LivewireRelay());
    }

    private function relayToSession(Router $router): void
    {
        $router->aliasMiddleware(SessionRelay::NAME, SessionRelay::class);
        $router->pushMiddlewareToGroup('web', SessionRelay::NAME);
    }
}


================================================
FILE: src/TranslatingCollector.php
================================================
<?php declare(strict_types=1);

namespace Masmerise\Toaster;

use Illuminate\Contracts\Translation\Translator;

/** @internal */
final readonly class TranslatingCollector implements Collector
{
    public function __construct(
        private Collector $next,
        private Translator $translator,
    ) {}

    public function collect(Toast $toast): void
    {
        $replacement = $this->translator->get($original = $toast->message->value, $toast->message->replace);

        if (is_string($replacement) && $replacement !== $original) {
            $toast = ToastBuilder::proto($toast)->message($replacement)->get();
        }

        $this->next->collect($toast);
    }

    public function release(): array
    {
        return $this->next->release();
    }
}


================================================
FILE: tests/AccessibleCollectorTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\AccessibleCollector;
use Masmerise\Toaster\Message;
use PHPUnit\Framework\Attributes\Test;

final class AccessibleCollectorTest extends TestCase
{
    use CollectorFactoryMethods;
    use ToastFactoryMethods;

    #[Test]
    public function it_adds_a_second_for_every_one_hundredth_word_floored(): void
    {
        $collector = new AccessibleCollector($this->aCollector());
        $message = Message::fromTranslatable(str_repeat('word ', 223));

        $collector->collect($this->aToast(message: $message));
        [$toast] = $collector->release();

        $this->assertSame(5000, $toast->duration->value);
    }
}


================================================
FILE: tests/CollectorFactoryMethods.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\QueuingCollector;

trait CollectorFactoryMethods
{
    private function aCollector(): QueuingCollector
    {
        return new QueuingCollector();
    }
}


================================================
FILE: tests/ControllerTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\Collector;
use Masmerise\Toaster\Toastable;
use Masmerise\Toaster\ToastBuilder;
use Masmerise\Toaster\Toaster;
use PHPUnit\Framework\Attributes\Test;

final class ControllerTest extends TestCase
{
    protected function defineRoutes($router): void
    {
        $router->get('inject', [ToastController::class, 'inject'])->middleware('web');
        $router->get('multiple', [ToastController::class, 'multiple'])->middleware('web');
    }

    #[Test]
    public function multiple_toasts_can_be_dispatched(): void
    {
        $this->get('multiple')->assertOk()->assertSessionHas('toasts', [
            [
                'duration' => 3000,
                'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',
                'type' => 'warning',
            ],
            [
                'duration' => 3333,
                'message' => 'Life is available only in the present moment. - Thich Nhat Hanh',
                'type' => 'error',
            ],
        ]);
    }

    #[Test]
    public function toast_is_flashed_to_the_session_using_dependency_injection(): void
    {
        $this->get('inject')->assertOk()->assertSessionHas('toasts', [[
            'duration' => 4000,
            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',
            'type' => 'success',
        ]]);
    }
}

final class ToastController
{
    use Toastable;

    public function inject(Collector $toasts): array
    {
        $toast = ToastBuilder::create()
            ->success()
            ->duration(4000)
            ->message('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk')
            ->get();

        $toasts->collect($toast);

        return ['message' => 'ok'];
    }

    public function multiple(): array
    {
        $this->warning('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk');

        Toaster::error('Life is available only in the present moment. - Thich Nhat Hanh')
            ->duration(3333)
            ->error()
            ->dispatch();

        return ['message' => 'ok'];
    }
}


================================================
FILE: tests/DurationTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use InvalidArgumentException;
use Masmerise\Toaster\Duration;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class DurationTest extends TestCase
{
    #[Test]
    public function it_requires_a_duration_to_be_at_least_1s(): void
    {
        $duration = Duration::fromMillis(3000);
        $this->assertSame(3000, $duration->value);

        $this->expectExceptionMessage(InvalidArgumentException::class);
        $this->expectExceptionMessage('The duration value must be at least 3000 ms.');

        Duration::fromMillis(999);
    }
}


================================================
FILE: tests/LivewireRelayTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Livewire\Component;
use Livewire\Livewire;
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\LivewireRelay;
use PHPUnit\Framework\Attributes\Test;

final class LivewireRelayTest extends TestCase
{
    #[Test]
    public function it_dispatches_events(): void
    {
        // mount => skip
        $component = Livewire::test(TestComponent::class);
        $component->assertNotDispatched(LivewireRelay::EVENT);

        // redirect => skip
        $component->call('redirectingAction');
        $component->assertNotDispatched(LivewireRelay::EVENT);

        // redirect using navigate => dispatch
        $component->call('redirectingActionUsingNavigate');
        $component->assertDispatched(LivewireRelay::EVENT,
            duration: 3000,
            message: 'Crispy toasts',
            type: 'success',
        );

        // normal action => dispatch
        $component->call('normalAction');
        $component->assertDispatched(LivewireRelay::EVENT,
            duration: 3000,
            message: 'Crispy toasts',
            type: 'success',
        );
    }
}

final class TestComponent extends Component
{
    use ToastFactoryMethods;

    public function normalAction(): void
    {
        // noop
    }

    public function redirectingAction(): void
    {
        $this->redirect('https://localhost');
    }

    public function redirectingActionUsingNavigate(): void
    {
        $this->redirect('https://localhost', true);
    }

    public function render(Collector $toasts): string
    {
        $toasts->collect($this->aToast()); // trigger relay registration

        return '<div></div>';
    }
}


================================================
FILE: tests/LivewireTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Livewire\Component;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\Toastable;
use Masmerise\Toaster\ToastBuilder;
use Masmerise\Toaster\Toaster;
use PHPUnit\Framework\Attributes\Test;

final class LivewireTest extends TestCase
{
    private Testable $component;

    protected function setUp(): void
    {
        parent::setUp();

        $this->component = Livewire::test(ToastComponent::class);
    }

    #[Test]
    public function multiple_toasts_can_be_dispatched(): void
    {
        $this->component->call('multiple');

        $this->assertCount(2, $events = $this->component->effects['dispatches']);

        [$eventA, $eventB] = $events;
        $this->assertSame('toaster:received', $eventA['name']);
        $this->assertSame('toaster:received', $eventB['name']);
        $this->assertSame([
            'duration' => 3000,
            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',
            'type' => 'warning',
        ], $eventA['params']);
        $this->assertSame([
            'duration' => 3333,
            'message' => 'Life is available only in the present moment. - Thich Nhat Hanh',
            'type' => 'error',
        ], $eventB['params']);
    }

    #[Test]
    public function toast_is_dispatched_to_the_browser_using_dependency_injection(): void
    {
        $this->component->call('inject');

        $this->assertCount(1, $events = $this->component->effects['dispatches']);

        [$event] = $events;
        $this->assertSame([
            'duration' => 4000,
            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',
            'type' => 'success',
        ], $event['params']);
    }
}

final class ToastComponent extends Component
{
    use Toastable;

    public function inject(Collector $toasts): void
    {
        $toast = ToastBuilder::create()
            ->success()
            ->duration(4000)
            ->message('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk')
            ->get();

        $toasts->collect($toast);
    }

    public function multiple(): void
    {
        $this->warning('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk');

        Toaster::error('Life is available only in the present moment. - Thich Nhat Hanh')
            ->duration(3333)
            ->dispatch();
    }

    public function render(): string
    {
        return '<div></div>';
    }
}


================================================
FILE: tests/MessageTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use InvalidArgumentException;
use Masmerise\Toaster\Message;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class MessageTest extends TestCase
{
    #[Test]
    public function it_requires_a_message_not_to_be_empty(): void
    {
        $message = Message::fromTranslatable('Clementine :name', ['name' => 'Tangerine']);
        $this->assertSame('Clementine :name', $message->value);
        $this->assertSame(['name' => 'Tangerine'], $message->replace);

        $this->expectExceptionMessage(InvalidArgumentException::class);
        $this->expectExceptionMessage('The message value cannot be empty.');

        Message::fromString('     ');
    }
}


================================================
FILE: tests/PendingToastTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\PendingToast;
use Masmerise\Toaster\Toaster;
use PHPUnit\Framework\Attributes\Test;

final class PendingToastTest extends TestCase
{
    #[Test]
    public function it_can_be_instantiated_through_static_factory_on_toaster(): void
    {
        $error = Toaster::error('validation.accepted', ['attribute' => 'terms'])->get();
        $this->assertSame([
            'duration' => 3000,
            'message' => 'validation.accepted',
            'type' => 'error',
        ], $error->toArray());

        $info = Toaster::info('Informational')->duration(3500)->get();
        $this->assertSame([
            'duration' => 3500,
            'message' => 'Informational',
            'type' => 'info',
        ], $info->toArray());

        $success = Toaster::success('Successful')->get();
        $this->assertSame([
            'duration' => 3000,
            'message' => 'Successful',
            'type' => 'success',
        ], $success->toArray());

        $warning = Toaster::warning('passwords.reset')->get();
        $this->assertSame([
            'duration' => 3000,
            'message' => 'passwords.reset',
            'type' => 'warning',
        ], $warning->toArray());
    }

    #[Test]
    public function it_can_be_instantiated_with_defaults(): void
    {
        $toast = PendingToast::create()->message('test')->success()->get();

        $this->assertSame([
            'duration' => 3000, // config default
            'message' => 'test',
            'type' => 'success',
        ], $toast->toArray());
    }

    #[Test]
    public function it_will_automatically_dispatch_the_toast_upon_destruction(): void
    {
        Toaster::shouldReceive('collect')->once();

        PendingToast::create()
            ->duration(4000)
            ->error()
            ->message('Uvuvuvwevwe Onyetenyevwe Ughemuhwem Osas');
    }

    #[Test]
    public function it_will_only_dispatch_once(): void
    {
        Toaster::shouldReceive('collect')->once();

        PendingToast::create()
            ->duration(4000)
            ->success()
            ->message('Uvuvuvwevwe Onyetenyevwe Ughemuhwem Osas')
            ->dispatch();
    }
}


================================================
FILE: tests/QueuingCollectorTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class QueuingCollectorTest extends TestCase
{
    use CollectorFactoryMethods;
    use ToastFactoryMethods;

    #[Test]
    public function it_can_add_and_flush_toasts(): void
    {
        $collector = $this->aCollector();
        $collector->collect($toastA = $this->aToast());
        $collector->collect($toastB = $this->aToast());

        $toasts = $collector->release();

        $this->assertCount(2, $toasts);
        $this->assertSame($toastA, $toasts[0]);
        $this->assertSame($toastB, $toasts[1]);
        $this->assertEmpty($collector->release());
    }
}


================================================
FILE: tests/SessionRelayTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Illuminate\Http\Request;
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\SessionRelay;
use PHPUnit\Framework\Attributes\Test;

final class SessionRelayTest extends TestCase
{
    use CollectorFactoryMethods;
    use ToastFactoryMethods;

    #[Test]
    public function it_relays_toasts_to_the_session(): void
    {
        $session = $this->app['session.store'];
        $relay = new SessionRelay($this->app);

        $relay->handle(new Request(), function () {});

        $this->assertFalse($session->exists(SessionRelay::NAME));

        $collector = $this->app[Collector::class];
        $collector->collect($this->aToast());
        $collector->collect($this->aToast());

        $relay->handle(new Request(), function () {});

        $this->assertTrue($session->exists(SessionRelay::NAME));
        $this->assertCount(2, $toasts = $session->get(SessionRelay::NAME));
        $this->assertEmpty($collector->release());
        $this->assertIsArray($toast = $toasts[0]);
        $this->assertArrayHasKey('duration', $toast);
        $this->assertArrayHasKey('message', $toast);
        $this->assertArrayHasKey('type', $toast);
    }
}


================================================
FILE: tests/TestCase.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\ToasterServiceProvider;
use Orchestra\Testbench\TestCase as TestCaseBase;

abstract class TestCase extends TestCaseBase
{
    protected function getPackageProviders($app): array
    {
        return [ToasterServiceProvider::class];
    }
}


================================================
FILE: tests/TestableCollectorTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\TestableCollector;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class TestableCollectorTest extends TestCase
{
    use ToastFactoryMethods;

    #[Test]
    public function it_can_assert_if_toasts_were_dispatched(): void
    {
        $instance = new TestableCollector();

        $instance->assertNothingDispatched();

        $instance->collect($this->aToast());

        $instance->assertDispatched('Crispy toasts');
    }
}


================================================
FILE: tests/ToastBuilderTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\ToastBuilder;
use Masmerise\Toaster\ToastType;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use UnexpectedValueException;

final class ToastBuilderTest extends TestCase
{
    #[Test]
    public function it_can_fluently_build_and_return_a_toast(): void
    {
        $builderA = ToastBuilder::create();
        $builderB = $builderA->message('Rice cooker');
        $builderC = $builderB->success();
        $builderD = $builderC->duration(4000);

        $toast = $builderD->get();

        $this->assertNotSame($builderA, $builderB);
        $this->assertNotSame($builderB, $builderC);
        $this->assertNotSame($builderC, $builderD);
        $this->assertSame('Rice cooker', $toast->message->value);
        $this->assertSame(ToastType::Success, $toast->type);
        $this->assertSame(4000, $toast->duration->value);
    }

    #[Test]
    public function it_throws_if_the_builder_data_is_incomplete_to_build_a_toast(): void
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('You must provide a valid duration.');

        ToastBuilder::create()->get();
    }
}


================================================
FILE: tests/ToastFactoryMethods.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\Duration;
use Masmerise\Toaster\Message;
use Masmerise\Toaster\Toast;
use Masmerise\Toaster\ToastType;

trait ToastFactoryMethods
{
    private function aToast(...$values): Toast
    {
        return new Toast(...[
            'message' => Message::fromString('Crispy toasts'),
            'duration' => Duration::fromMillis(3000),
            'type' => ToastType::Success,
            ...$values,
        ]);
    }
}


================================================
FILE: tests/ToastTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ToastTest extends TestCase
{
    use ToastFactoryMethods;

    #[Test]
    public function it_can_be_serialized_to_array(): void
    {
        $toast = $this->aToast();

        $result = $toast->toArray();

        $this->assertSame([
            'duration' => 3000,
            'message' => 'Crispy toasts',
            'type' => 'success',
        ], $result);
    }
}


================================================
FILE: tests/ToastableMacrosTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Illuminate\Http\RedirectResponse;
use PHPUnit\Framework\Attributes\Test;

final class ToastableMacrosTest extends TestCase
{
    protected function defineRoutes($router): void
    {
        $router->get('redirect', [ToastableMacroController::class, 'redirect'])->middleware('web');
    }

    #[Test]
    public function redirect(): void
    {
        $this->get('redirect')->assertSessionHas('toasts');
    }
}

final class ToastableMacroController
{
    public function redirect(): RedirectResponse
    {
        return redirect()
            ->away('https://www.google.com')
            ->success('Success Macro!');
    }
}


================================================
FILE: tests/ToastableTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Livewire\Component;
use Livewire\Livewire;
use Masmerise\Toaster\LivewireRelay;
use Masmerise\Toaster\SessionRelay;
use Masmerise\Toaster\Toastable;
use PHPUnit\Framework\Attributes\Test;

final class ToastableTest extends TestCase
{
    #[Test]
    public function it_can_be_invoked_from_controllers(): void
    {
        $this->app['router']->get('test', [ToastableController::class, 'index'])->middleware('web');

        $response = $this->get('test');

        $response->assertOk()->assertSessionHas(SessionRelay::NAME, [[
            'duration' => 3000,
            'message' => 'I am a crispy toast, yummy!',
            'type' => 'info',
        ]]);
    }

    #[Test]
    public function it_can_be_invoked_from_livewire_components(): void
    {
        $component = Livewire::test(ToastableComponent::class);

        $component->call('bake');

        $component->assertDispatched(LivewireRelay::EVENT,
            duration: 3000,
            message: 'I became a crispy toast, yummy!',
            type: 'success',
        );
    }
}

final class ToastableComponent extends Component
{
    use Toastable;

    public function bake(): void
    {
        $this->success('I became a crispy toast, yummy!');
    }

    public function render(): string
    {
        return '<div></div>';
    }
}

final class ToastableController
{
    use Toastable;

    public function index(): array
    {
        $this->info('I am a crispy toast, yummy!');

        return ['message' => 'success'];
    }
}


================================================
FILE: tests/ToasterConfigTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\ToasterConfig;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ToasterConfigTest extends TestCase
{
    #[Test]
    public function it_can_be_serialized_for_the_frontend(): void
    {
        $config = ToasterConfig::fromArray(require __DIR__ . '/../config/toaster.php');

        $array = $config->toJavaScript();

        $this->assertSame([
            'alignment' => 'bottom',
            'duration' => 3000,
            'replace' => false,
            'suppress' => false,
        ], $array);
    }
}


================================================
FILE: tests/ToasterHubTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
use Masmerise\Toaster\Alignment;
use Masmerise\Toaster\Position;
use Masmerise\Toaster\ToasterConfig;
use Masmerise\Toaster\ToasterHub;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

final class ToasterHubTest extends TestCase
{
    use InteractsWithViews;

    public static function configurations(): iterable
    {
        yield [['alignment' => Alignment::Bottom->value, 'closeable' => true, 'position' => Position::Right->value]];
        yield [['alignment' => Alignment::Middle->value, 'closeable' => true, 'position' => Position::Right->value]];
        yield [['alignment' => Alignment::Top->value, 'closeable' => true, 'position' => Position::Right->value]];
        yield [['alignment' => Alignment::Bottom->value, 'closeable' => false, 'position' => Position::Left->value]];
        yield [['alignment' => Alignment::Middle->value, 'closeable' => false, 'position' => Position::Left->value]];
        yield [['alignment' => Alignment::Top->value, 'closeable' => false, 'position' => Position::Left->value]];
        yield [['alignment' => Alignment::Bottom->value, 'closeable' => false, 'position' => Position::Center->value]];
        yield [['alignment' => Alignment::Middle->value, 'closeable' => false, 'position' => Position::Center->value]];
        yield [['alignment' => Alignment::Top->value, 'closeable' => false, 'position' => Position::Center->value]];
    }

    #[DataProvider('configurations')]
    #[Test]
    public function it_can_be_rendered(array $config): void
    {
        $component = $this->component(ToasterHub::class, ['config' => ToasterConfig::fromArray($config)]);

        $component->assertSee('toaster');
    }
}


================================================
FILE: tests/ToasterServiceProviderTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Masmerise\Toaster\AccessibleCollector;
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\ToasterConfig;
use Masmerise\Toaster\ToasterServiceProvider;
use Masmerise\Toaster\TranslatingCollector;
use Orchestra\Testbench\TestCase;
use PHPUnit\Framework\Attributes\Test;

final class ToasterServiceProviderTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->app->register(ToasterServiceProvider::class);
    }

    #[Test]
    public function it_binds_the_service_as_a_singleton(): void
    {
        $this->assertTrue($this->app->isShared(Collector::class));
        $this->assertTrue($this->app->isAlias(ToasterServiceProvider::NAME));
    }

    #[Test]
    public function it_registers_the_translating_behaviour_only_if_enabled_in_the_config(): void
    {
        $this->assertInstanceOf(TranslatingCollector::class, $this->app[ToasterServiceProvider::NAME]);

        $this->refreshApplication();
        $this->app['config']->set('toaster.translate', false);
        $this->app->register(ToasterServiceProvider::class);

        $this->assertInstanceOf(AccessibleCollector::class, $this->app[ToasterServiceProvider::NAME]);
    }

    #[Test]
    public function it_registers_macros(): void
    {
        $this->assertTrue(Redirector::hasMacro('error'));
        $this->assertTrue(Redirector::hasMacro('info'));
        $this->assertTrue(Redirector::hasMacro('success'));
        $this->assertTrue(Redirector::hasMacro('warning'));

        $this->assertTrue(RedirectResponse::hasMacro('error'));
        $this->assertTrue(RedirectResponse::hasMacro('info'));
        $this->assertTrue(RedirectResponse::hasMacro('success'));
        $this->assertTrue(RedirectResponse::hasMacro('warning'));
    }

    #[Test]
    public function it_registers_custom_config_object(): void
    {
        $this->assertTrue($this->app->bound(ToasterConfig::class));

        $config = $this->app[ToasterConfig::class];

        $this->assertSame(3000, $config->duration);
        $this->assertTrue($config->wantsAccessibility);
        $this->assertTrue($config->wantsCloseableToasts);
        $this->assertTrue($config->wantsTranslation);
        $this->assertSame('right', $config->position);
    }
}


================================================
FILE: tests/TranslatingCollectorTest.php
================================================
<?php declare(strict_types=1);

namespace Tests;

use Masmerise\Toaster\Message;
use Masmerise\Toaster\TranslatingCollector;
use PHPUnit\Framework\Attributes\Test;

final class TranslatingCollectorTest extends TestCase
{
    use CollectorFactoryMethods;
    use ToastFactoryMethods;

    #[Test]
    public function it_can_translate_the_messages(): void
    {
        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);
        $message = Message::fromTranslatable('auth.throttle', ['seconds' => 1337]);

        $collector->collect($this->aToast(message: $message));
        [$toast] = $collector->release();

        $this->assertSame('Too many login attempts. Please try again in 1337 seconds.', $toast->message->value);
    }

    #[Test]
    public function it_doesnt_replace_array_resolved_translations(): void
    {
        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);
        $message = Message::fromTranslatable('validation.size');

        $collector->collect($this->aToast(message: $message));
        [$toast] = $collector->release();

        $this->assertSame('validation.size', $toast->message->value);
    }

    #[Test]
    public function it_doesnt_modify_regular_strings(): void
    {
        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);

        $collector->collect($this->aToast());
        [$toast] = $collector->release();

        $this->assertSame('Crispy toasts', $toast->message->value);
    }
}
Download .txt
gitextract_4ngx_v20/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .stubs.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADING.md
├── composer.json
├── config/
│   └── toaster.php
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
├── resources/
│   ├── js/
│   │   ├── config.js
│   │   ├── hub.js
│   │   ├── index.js
│   │   ├── toast.js
│   │   ├── toaster.js
│   │   └── uuid41.js
│   └── views/
│       ├── close-button.blade.php
│       └── hub.blade.php
├── src/
│   ├── AccessibleCollector.php
│   ├── Alignment.php
│   ├── Assertable.php
│   ├── Collector.php
│   ├── Duration.php
│   ├── LivewireRelay.php
│   ├── Message.php
│   ├── PendingToast.php
│   ├── Position.php
│   ├── QueuingCollector.php
│   ├── SessionRelay.php
│   ├── TestableCollector.php
│   ├── Toast.php
│   ├── ToastBuilder.php
│   ├── ToastType.php
│   ├── Toastable.php
│   ├── ToastableMacros.php
│   ├── Toaster.php
│   ├── ToasterConfig.php
│   ├── ToasterHub.php
│   ├── ToasterServiceProvider.php
│   └── TranslatingCollector.php
└── tests/
    ├── AccessibleCollectorTest.php
    ├── CollectorFactoryMethods.php
    ├── ControllerTest.php
    ├── DurationTest.php
    ├── LivewireRelayTest.php
    ├── LivewireTest.php
    ├── MessageTest.php
    ├── PendingToastTest.php
    ├── QueuingCollectorTest.php
    ├── SessionRelayTest.php
    ├── TestCase.php
    ├── TestableCollectorTest.php
    ├── ToastBuilderTest.php
    ├── ToastFactoryMethods.php
    ├── ToastTest.php
    ├── ToastableMacrosTest.php
    ├── ToastableTest.php
    ├── ToasterConfigTest.php
    ├── ToasterHubTest.php
    ├── ToasterServiceProviderTest.php
    └── TranslatingCollectorTest.php
Download .txt
SYMBOL INDEX (199 symbols across 45 files)

FILE: .stubs.php
  class RedirectResponse (line 5) | class RedirectResponse
    method error (line 7) | public function error(string $message, array $replace = []): RedirectR...
    method info (line 9) | public function info(string $message, array $replace = []): RedirectRe...
    method success (line 11) | public function success(string $message, array $replace = []): Redirec...
    method warning (line 13) | public function warning(string $message, array $replace = []): Redirec...
  class Redirector (line 19) | class Redirector
    method error (line 21) | public function error(string $message, array $replace = []): Redirecto...
    method info (line 23) | public function info(string $message, array $replace = []): Redirector {}
    method success (line 25) | public function success(string $message, array $replace = []): Redirec...
    method warning (line 27) | public function warning(string $message, array $replace = []): Redirec...

FILE: resources/js/config.js
  class Alignment (line 1) | class Alignment {
    method constructor (line 4) | constructor(value) {
    method isTop (line 8) | isTop() {
  class Config (line 13) | class Config {
    method constructor (line 14) | constructor(alignment, duration, replace, suppress) {
    method fromJson (line 21) | static fromJson(data) {

FILE: resources/js/hub.js
  function Hub (line 4) | function Hub(Alpine) {

FILE: resources/js/toast.js
  class Toast (line 3) | class Toast {
    method constructor (line 4) | constructor(duration, message, type) {
    method fromJson (line 15) | static fromJson(data) {
    method dispose (line 19) | dispose() {
    method equals (line 32) | equals(other) {
    method runAfterDuration (line 38) | runAfterDuration(callback) {
    method select (line 42) | select(config) {
    method show (line 46) | show($el) {

FILE: resources/js/uuid41.js
  function uuid41 (line 1) | function uuid41() {

FILE: src/AccessibleCollector.php
  class AccessibleCollector (line 6) | final readonly class AccessibleCollector implements Collector
    method __construct (line 11) | public function __construct(private Collector $next) {}
    method collect (line 13) | public function collect(Toast $toast): void
    method release (line 25) | public function release(): array

FILE: src/Assertable.php
  type Assertable (line 6) | trait Assertable
    method is (line 8) | public function is(string $value): bool

FILE: src/Collector.php
  type Collector (line 5) | interface Collector
    method collect (line 7) | public function collect(Toast $toast): void;
    method release (line 10) | public function release(): array;

FILE: src/Duration.php
  class Duration (line 8) | final readonly class Duration
    method __construct (line 14) | private function __construct(int $value)
    method fromMillis (line 23) | public static function fromMillis(int $value): self

FILE: src/LivewireRelay.php
  class LivewireRelay (line 13) | final readonly class LivewireRelay
    method __invoke (line 17) | public function __invoke(Component $component, ComponentContext $ctx):...

FILE: src/Message.php
  class Message (line 8) | final readonly class Message
    method __construct (line 14) | private function __construct(string $value, array $replace = [])
    method fromString (line 24) | public static function fromString(string $value): self
    method fromTranslatable (line 29) | public static function fromTranslatable(string $value, array $replace ...
    method equals (line 34) | public function equals(Message|string $other): bool

FILE: src/PendingToast.php
  class PendingToast (line 18) | final class PendingToast
    method __construct (line 26) | private function __construct(int $duration)
    method create (line 31) | public static function create(): self
    method dispatch (line 36) | public function dispatch(): void
    method __call (line 45) | public function __call(string $name, array $arguments): mixed
    method __destruct (line 58) | public function __destruct()

FILE: src/QueuingCollector.php
  class QueuingCollector (line 6) | final class QueuingCollector implements Collector
    method collect (line 10) | public function collect(Toast $toast): void
    method release (line 15) | public function release(): array

FILE: src/SessionRelay.php
  class SessionRelay (line 11) | final readonly class SessionRelay
    method __construct (line 15) | public function __construct(private Application $app) {}
    method handle (line 17) | public function handle(Request $request, Closure $next): mixed
    method serialize (line 32) | private function serialize(array $toasts): array

FILE: src/TestableCollector.php
  class TestableCollector (line 7) | final class TestableCollector implements Collector
    method collect (line 11) | public function collect(Toast $toast): void
    method release (line 16) | public function release(): array
    method assertDispatched (line 24) | public function assertDispatched(string $message): void
    method assertNothingDispatched (line 31) | public function assertNothingDispatched(): void

FILE: src/Toast.php
  class Toast (line 7) | final readonly class Toast implements Arrayable
    method __construct (line 9) | public function __construct(
    method toArray (line 15) | public function toArray(): array

FILE: src/ToastBuilder.php
  class ToastBuilder (line 8) | final class ToastBuilder
    method create (line 18) | public static function create(): self
    method proto (line 23) | public static function proto(Toast $toast): self
    method duration (line 33) | public function duration(int $milliseconds): self
    method error (line 38) | public function error(): self
    method info (line 43) | public function info(): self
    method message (line 48) | public function message(string $message, array $replace = []): self
    method success (line 53) | public function success(): self
    method type (line 58) | public function type(string $type): self
    method warning (line 63) | public function warning(): self
    method get (line 68) | public function get(): Toast
    method modify (line 85) | private function modify(string $property, mixed $value): self

FILE: src/Toastable.php
  type Toastable (line 5) | trait Toastable
    method error (line 7) | protected function error(string $message, array $replace = []): Pendin...
    method info (line 12) | protected function info(string $message, array $replace = []): Pending...
    method success (line 17) | protected function success(string $message, array $replace = []): Pend...
    method toast (line 22) | protected function toast(): PendingToast
    method warning (line 27) | protected function warning(string $message, array $replace = []): Pend...

FILE: src/ToastableMacros.php
  class ToastableMacros (line 8) | final readonly class ToastableMacros
    method error (line 10) | protected function error(): Closure
    method info (line 15) | protected function info(): Closure
    method success (line 20) | protected function success(): Closure
    method warning (line 25) | protected function warning(): Closure
    method macro (line 30) | private function macro(string $type): Closure

FILE: src/Toaster.php
  class Toaster (line 13) | final class Toaster extends Facade
    method config (line 15) | public static function config(): ToasterConfig
    method error (line 20) | public static function error(string $message, array $replace = []): Pe...
    method fake (line 25) | public static function fake(): TestableCollector
    method info (line 32) | public static function info(string $message, array $replace = []): Pen...
    method success (line 37) | public static function success(string $message, array $replace = []): ...
    method toast (line 42) | public static function toast(): PendingToast
    method warning (line 47) | public static function warning(string $message, array $replace = []): ...
    method getFacadeAccessor (line 52) | protected static function getFacadeAccessor(): string
    method getMockableClass (line 57) | protected static function getMockableClass(): string

FILE: src/ToasterConfig.php
  class ToasterConfig (line 8) | final readonly class ToasterConfig
    method __construct (line 10) | private function __construct(
    method fromArray (line 33) | public static function fromArray(array $config): self
    method alignment (line 47) | public function alignment(): Alignment
    method position (line 52) | public function position(): Position
    method toJavaScript (line 57) | public function toJavaScript(): array

FILE: src/ToasterHub.php
  class ToasterHub (line 10) | final class ToasterHub extends Component
    method __construct (line 14) | public function __construct(
    method render (line 20) | public function render(): View

FILE: src/ToasterServiceProvider.php
  class ToasterServiceProvider (line 13) | final class ToasterServiceProvider extends AggregateServiceProvider
    method boot (line 20) | public function boot(): void
    method register (line 36) | public function register(): void
    method aliasToasterHub (line 54) | private function aliasToasterHub(BladeCompiler $blade): void
    method configureService (line 59) | private function configureService(): ToasterConfig
    method registerPublishing (line 70) | private function registerPublishing(): void
    method relayToLivewire (line 81) | private function relayToLivewire(): void
    method relayToSession (line 86) | private function relayToSession(Router $router): void

FILE: src/TranslatingCollector.php
  class TranslatingCollector (line 8) | final readonly class TranslatingCollector implements Collector
    method __construct (line 10) | public function __construct(
    method collect (line 15) | public function collect(Toast $toast): void
    method release (line 26) | public function release(): array

FILE: tests/AccessibleCollectorTest.php
  class AccessibleCollectorTest (line 9) | final class AccessibleCollectorTest extends TestCase
    method it_adds_a_second_for_every_one_hundredth_word_floored (line 14) | #[Test]

FILE: tests/CollectorFactoryMethods.php
  type CollectorFactoryMethods (line 7) | trait CollectorFactoryMethods
    method aCollector (line 9) | private function aCollector(): QueuingCollector

FILE: tests/ControllerTest.php
  class ControllerTest (line 11) | final class ControllerTest extends TestCase
    method defineRoutes (line 13) | protected function defineRoutes($router): void
    method multiple_toasts_can_be_dispatched (line 19) | #[Test]
    method toast_is_flashed_to_the_session_using_dependency_injection (line 36) | #[Test]
  class ToastController (line 47) | final class ToastController
    method inject (line 51) | public function inject(Collector $toasts): array
    method multiple (line 64) | public function multiple(): array

FILE: tests/DurationTest.php
  class DurationTest (line 10) | final class DurationTest extends TestCase
    method it_requires_a_duration_to_be_at_least_1s (line 12) | #[Test]

FILE: tests/LivewireRelayTest.php
  class LivewireRelayTest (line 11) | final class LivewireRelayTest extends TestCase
    method it_dispatches_events (line 13) | #[Test]
  class TestComponent (line 42) | final class TestComponent extends Component
    method normalAction (line 46) | public function normalAction(): void
    method redirectingAction (line 51) | public function redirectingAction(): void
    method redirectingActionUsingNavigate (line 56) | public function redirectingActionUsingNavigate(): void
    method render (line 61) | public function render(Collector $toasts): string

FILE: tests/LivewireTest.php
  class LivewireTest (line 14) | final class LivewireTest extends TestCase
    method setUp (line 18) | protected function setUp(): void
    method multiple_toasts_can_be_dispatched (line 25) | #[Test]
    method toast_is_dispatched_to_the_browser_using_dependency_injection (line 47) | #[Test]
  class ToastComponent (line 63) | final class ToastComponent extends Component
    method inject (line 67) | public function inject(Collector $toasts): void
    method multiple (line 78) | public function multiple(): void
    method render (line 87) | public function render(): string

FILE: tests/MessageTest.php
  class MessageTest (line 10) | final class MessageTest extends TestCase
    method it_requires_a_message_not_to_be_empty (line 12) | #[Test]

FILE: tests/PendingToastTest.php
  class PendingToastTest (line 9) | final class PendingToastTest extends TestCase
    method it_can_be_instantiated_through_static_factory_on_toaster (line 11) | #[Test]
    method it_can_be_instantiated_with_defaults (line 43) | #[Test]
    method it_will_automatically_dispatch_the_toast_upon_destruction (line 55) | #[Test]
    method it_will_only_dispatch_once (line 66) | #[Test]

FILE: tests/QueuingCollectorTest.php
  class QueuingCollectorTest (line 8) | final class QueuingCollectorTest extends TestCase
    method it_can_add_and_flush_toasts (line 13) | #[Test]

FILE: tests/SessionRelayTest.php
  class SessionRelayTest (line 10) | final class SessionRelayTest extends TestCase
    method it_relays_toasts_to_the_session (line 15) | #[Test]

FILE: tests/TestCase.php
  class TestCase (line 8) | abstract class TestCase extends TestCaseBase
    method getPackageProviders (line 10) | protected function getPackageProviders($app): array

FILE: tests/TestableCollectorTest.php
  class TestableCollectorTest (line 9) | final class TestableCollectorTest extends TestCase
    method it_can_assert_if_toasts_were_dispatched (line 13) | #[Test]

FILE: tests/ToastBuilderTest.php
  class ToastBuilderTest (line 11) | final class ToastBuilderTest extends TestCase
    method it_can_fluently_build_and_return_a_toast (line 13) | #[Test]
    method it_throws_if_the_builder_data_is_incomplete_to_build_a_toast (line 31) | #[Test]

FILE: tests/ToastFactoryMethods.php
  type ToastFactoryMethods (line 10) | trait ToastFactoryMethods
    method aToast (line 12) | private function aToast(...$values): Toast

FILE: tests/ToastTest.php
  class ToastTest (line 8) | final class ToastTest extends TestCase
    method it_can_be_serialized_to_array (line 12) | #[Test]

FILE: tests/ToastableMacrosTest.php
  class ToastableMacrosTest (line 8) | final class ToastableMacrosTest extends TestCase
    method defineRoutes (line 10) | protected function defineRoutes($router): void
    method redirect (line 15) | #[Test]
  class ToastableMacroController (line 22) | final class ToastableMacroController
    method redirect (line 24) | public function redirect(): RedirectResponse

FILE: tests/ToastableTest.php
  class ToastableTest (line 12) | final class ToastableTest extends TestCase
    method it_can_be_invoked_from_controllers (line 14) | #[Test]
    method it_can_be_invoked_from_livewire_components (line 28) | #[Test]
  class ToastableComponent (line 43) | final class ToastableComponent extends Component
    method bake (line 47) | public function bake(): void
    method render (line 52) | public function render(): string
  class ToastableController (line 58) | final class ToastableController
    method index (line 62) | public function index(): array

FILE: tests/ToasterConfigTest.php
  class ToasterConfigTest (line 9) | final class ToasterConfigTest extends TestCase
    method it_can_be_serialized_for_the_frontend (line 11) | #[Test]

FILE: tests/ToasterHubTest.php
  class ToasterHubTest (line 13) | final class ToasterHubTest extends TestCase
    method configurations (line 17) | public static function configurations(): iterable
    method it_can_be_rendered (line 30) | #[DataProvider('configurations')]

FILE: tests/ToasterServiceProviderTest.php
  class ToasterServiceProviderTest (line 15) | final class ToasterServiceProviderTest extends TestCase
    method setUp (line 17) | protected function setUp(): void
    method it_binds_the_service_as_a_singleton (line 24) | #[Test]
    method it_registers_the_translating_behaviour_only_if_enabled_in_the_config (line 31) | #[Test]
    method it_registers_macros (line 43) | #[Test]
    method it_registers_custom_config_object (line 57) | #[Test]

FILE: tests/TranslatingCollectorTest.php
  class TranslatingCollectorTest (line 9) | final class TranslatingCollectorTest extends TestCase
    method it_can_translate_the_messages (line 14) | #[Test]
    method it_doesnt_replace_array_resolved_translations (line 26) | #[Test]
    method it_doesnt_modify_regular_strings (line 38) | #[Test]
Condensed preview — 65 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (95K chars).
[
  {
    "path": ".editorconfig",
    "chars": 8864,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 390,
    "preview": "/.editorconfig           export-ignore\n/.gitattributes          export-ignore\n/.github                 export-ignore\n/.g"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1130,
    "preview": "name: Automated testing\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fa"
  },
  {
    "path": ".gitignore",
    "chars": 144,
    "preview": ".idea\n.php_cs\n.php_cs.cache\n.phpunit.cache\n.phpunit.result.cache\ncomposer.lock\nphpunit.xml\nphpstan.neon\nvendor\nnode_modu"
  },
  {
    "path": ".stubs.php",
    "chars": 868,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Illuminate\\Http\n{\n    class RedirectResponse\n    {\n        public function err"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1847,
    "preview": "# Changelog\n\nAll notable changes to `livewire-toaster` will be documented in this file.\n\n## 2.9.0 - 2026-01-17\n\n### Adde"
  },
  {
    "path": "LICENSE.md",
    "chars": 1101,
    "preview": "The MIT License (MIT)\n\nCopyright (c) Muhammed Sari <support@muhammedsari.me>\n\nPermission is hereby granted, free of char"
  },
  {
    "path": "README.md",
    "chars": 15540,
    "preview": "<p align=\"center\"><img src=\"https://github.com/masmerise/livewire-toaster/raw/master/art/banner.png\" alt=\"Toaster Banner"
  },
  {
    "path": "UPGRADING.md",
    "chars": 700,
    "preview": "# Upgrade Guide\n\n## v1 → v2\n\n### Minimum versions\n\nThe following dependency versions have been updated:\n\n- The minimum L"
  },
  {
    "path": "composer.json",
    "chars": 1955,
    "preview": "{\n    \"name\": \"masmerise/livewire-toaster\",\n    \"description\": \"Beautiful toast notifications for Laravel / Livewire.\",\n"
  },
  {
    "path": "config/toaster.php",
    "chars": 1312,
    "preview": "<?php declare(strict_types=1);\n\nreturn [\n\n    /**\n     * Add an additional second for every 100th word of the toast mess"
  },
  {
    "path": "phpstan.neon.dist",
    "chars": 507,
    "preview": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n\nparameters:\n    paths:\n        - src\n\n    level: 9\n\n    ignor"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 790,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noName"
  },
  {
    "path": "pint.json",
    "chars": 715,
    "preview": "{\n    \"preset\": \"psr12\",\n    \"rules\": {\n        \"blank_line_after_opening_tag\": false,\n        \"blank_line_before_statem"
  },
  {
    "path": "resources/js/config.js",
    "chars": 532,
    "preview": "class Alignment {\n    static Top = 'top';\n\n    constructor(value) {\n        this.value = value;\n    }\n\n    isTop() {\n   "
  },
  {
    "path": "resources/js/hub.js",
    "chars": 1639,
    "preview": "import { Config } from './config';\nimport { Toast } from './toast';\n\nexport function Hub(Alpine) {\n    Alpine.data('toas"
  },
  {
    "path": "resources/js/index.js",
    "chars": 179,
    "preview": "import { Hub } from './hub';\nimport * as Toaster from './toaster';\n\nwindow.Toaster = Toaster;\n\ndocument.addEventListener"
  },
  {
    "path": "resources/js/toast.js",
    "chars": 1205,
    "preview": "import { uuid41 } from './uuid41';\n\nexport class Toast {\n    constructor(duration, message, type) {\n        this.$el = n"
  },
  {
    "path": "resources/js/toaster.js",
    "chars": 382,
    "preview": "const event = (message, type) => {\n    document.dispatchEvent(new CustomEvent('toaster:received', { detail: { message, t"
  },
  {
    "path": "resources/js/uuid41.js",
    "chars": 350,
    "preview": "export function uuid41() {\n    let d = '';\n\n    while (d.length < 32) {\n        d += Math.random().toString(16).substrin"
  },
  {
    "path": "resources/views/close-button.blade.php",
    "chars": 613,
    "preview": "<button @click=\"toast.dispose()\" aria-label=\"@lang('close')\" class=\"absolute right-0 p-2 focus:outline-none focus:outlin"
  },
  {
    "path": "resources/views/hub.blade.php",
    "chars": 1926,
    "preview": "<div role=\"status\" id=\"toaster\" x-data=\"toasterHub(@js($toasts), @js($config))\" @class([\n    'fixed z-50 p-4 w-full flex"
  },
  {
    "path": "src/AccessibleCollector.php",
    "chars": 754,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nfinal readonly class AccessibleCollector "
  },
  {
    "path": "src/Alignment.php",
    "chars": 205,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum Alignment: string\n{\n    use Assertab"
  },
  {
    "path": "src/Assertable.php",
    "chars": 195,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @interal */\ntrait Assertable\n{\n    public function is("
  },
  {
    "path": "src/Collector.php",
    "chars": 195,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\ninterface Collector\n{\n    public function collect(Toast $t"
  },
  {
    "path": "src/Duration.php",
    "chars": 538,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse InvalidArgumentException;\n\n/** @internal */\nfinal read"
  },
  {
    "path": "src/LivewireRelay.php",
    "chars": 1004,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Livewire\\Component;\nuse Livewire\\Features\\SupportEvent"
  },
  {
    "path": "src/Message.php",
    "chars": 930,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse InvalidArgumentException;\n\n/** @internal */\nfinal read"
  },
  {
    "path": "src/PendingToast.php",
    "chars": 1559,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Traits\\ForwardsCalls;\n\n/**\n * @meth"
  },
  {
    "path": "src/Position.php",
    "chars": 204,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum Position: string\n{\n    use Assertabl"
  },
  {
    "path": "src/QueuingCollector.php",
    "chars": 396,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nfinal class QueuingCollector implements C"
  },
  {
    "path": "src/SessionRelay.php",
    "chars": 896,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Closure;\nuse Illuminate\\Contracts\\Foundation\\Applicati"
  },
  {
    "path": "src/TestableCollector.php",
    "chars": 901,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse PHPUnit\\Framework\\Assert as PHPUnit;\n\nfinal class Test"
  },
  {
    "path": "src/Toast.php",
    "chars": 519,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\n\nfinal readonl"
  },
  {
    "path": "src/ToastBuilder.php",
    "chars": 2230,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Traits\\Conditionable;\nuse Unexpecte"
  },
  {
    "path": "src/ToastType.php",
    "chars": 216,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum ToastType: string\n{\n    case Error ="
  },
  {
    "path": "src/Toastable.php",
    "chars": 758,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\ntrait Toastable\n{\n    protected function error(string $mes"
  },
  {
    "path": "src/ToastableMacros.php",
    "chars": 753,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Closure;\n\n/** @internal */\nfinal readonly class Toasta"
  },
  {
    "path": "src/Toaster.php",
    "chars": 1588,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Facades\\Facade;\n\n/**\n * @method sta"
  },
  {
    "path": "src/ToasterConfig.php",
    "chars": 1813,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Arr;\n\n/** @internal */\nfinal readon"
  },
  {
    "path": "src/ToasterHub.php",
    "chars": 862,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Session\\Session;\nuse Illuminate\\C"
  },
  {
    "path": "src/ToasterServiceProvider.php",
    "chars": 2958,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routi"
  },
  {
    "path": "src/TranslatingCollector.php",
    "chars": 769,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Translation\\Translator;\n\n/** @int"
  },
  {
    "path": "tests/AccessibleCollectorTest.php",
    "chars": 694,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\AccessibleCollector;\nuse Masmerise\\Toaster\\Messa"
  },
  {
    "path": "tests/CollectorFactoryMethods.php",
    "chars": 228,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\QueuingCollector;\n\ntrait CollectorFactoryMethods"
  },
  {
    "path": "tests/ControllerTest.php",
    "chars": 2208,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\Toastable;\nuse "
  },
  {
    "path": "tests/DurationTest.php",
    "chars": 618,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse InvalidArgumentException;\nuse Masmerise\\Toaster\\Duration;\nuse PHPU"
  },
  {
    "path": "tests/LivewireRelayTest.php",
    "chars": 1683,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Livewire;\nuse Masmerise\\Toaster\\C"
  },
  {
    "path": "tests/LivewireTest.php",
    "chars": 2608,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Features\\SupportTesting\\Testable;"
  },
  {
    "path": "tests/MessageTest.php",
    "chars": 733,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse InvalidArgumentException;\nuse Masmerise\\Toaster\\Message;\nuse PHPUn"
  },
  {
    "path": "tests/PendingToastTest.php",
    "chars": 2229,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\PendingToast;\nuse Masmerise\\Toaster\\Toaster;\nuse"
  },
  {
    "path": "tests/QueuingCollectorTest.php",
    "chars": 703,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;"
  },
  {
    "path": "tests/SessionRelayTest.php",
    "chars": 1204,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\Request;\nuse Masmerise\\Toaster\\Collector;\nuse Masm"
  },
  {
    "path": "tests/TestCase.php",
    "chars": 312,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToasterServiceProvider;\nuse Orchestra\\Testbench\\"
  },
  {
    "path": "tests/TestableCollectorTest.php",
    "chars": 533,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\TestableCollector;\nuse PHPUnit\\Framework\\Attribu"
  },
  {
    "path": "tests/ToastBuilderTest.php",
    "chars": 1226,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToastBuilder;\nuse Masmerise\\Toaster\\ToastType;\nu"
  },
  {
    "path": "tests/ToastFactoryMethods.php",
    "chars": 490,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Duration;\nuse Masmerise\\Toaster\\Message;\nuse Mas"
  },
  {
    "path": "tests/ToastTest.php",
    "chars": 506,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;"
  },
  {
    "path": "tests/ToastableMacrosTest.php",
    "chars": 681,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse PHPUnit\\Framework\\Attributes"
  },
  {
    "path": "tests/ToastableTest.php",
    "chars": 1557,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Livewire;\nuse Masmerise\\Toaster\\L"
  },
  {
    "path": "tests/ToasterConfigTest.php",
    "chars": 614,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToasterConfig;\nuse PHPUnit\\Framework\\Attributes\\"
  },
  {
    "path": "tests/ToasterHubTest.php",
    "chars": 1811,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Testing\\Concerns\\InteractsWithViews;\nuse Mas"
  },
  {
    "path": "tests/ToasterServiceProviderTest.php",
    "chars": 2374,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Redirecto"
  },
  {
    "path": "tests/TranslatingCollectorTest.php",
    "chars": 1539,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Message;\nuse Masmerise\\Toaster\\TranslatingCollec"
  }
]

About this extraction

This page contains the full source code of the masmerise/livewire-toaster GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 65 files (85.4 KB), approximately 23.0k tokens, and a symbol index with 199 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!