[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.{yml,yaml}]\nindent_size = 2\n\n[*.php]\nij_continuation_indent_size = 4\nij_php_align_assignments = false\nij_php_align_class_constants = false\nij_php_align_group_field_declarations = false\nij_php_align_inline_comments = false\nij_php_align_key_value_pairs = false\nij_php_align_multiline_array_initializer_expression = false\nij_php_align_multiline_binary_operation = false\nij_php_align_multiline_chained_methods = false\nij_php_align_multiline_extends_list = true\nij_php_align_multiline_for = true\nij_php_align_multiline_parameters = false\nij_php_align_multiline_parameters_in_calls = false\nij_php_align_multiline_ternary_operation = false\nij_php_align_phpdoc_comments = true\nij_php_align_phpdoc_param_names = true\nij_php_anonymous_brace_style = end_of_line\nij_php_api_weight = 28\nij_php_array_initializer_new_line_after_left_brace = true\nij_php_array_initializer_right_brace_on_new_line = true\nij_php_array_initializer_wrap = on_every_item\nij_php_assignment_wrap = off\nij_php_attributes_wrap = split_into_lines\nij_php_author_weight = 28\nij_php_binary_operation_sign_on_next_line = false\nij_php_binary_operation_wrap = off\nij_php_blank_lines_after_class_header = 0\nij_php_blank_lines_after_function = 1\nij_php_blank_lines_after_imports = 1\nij_php_blank_lines_after_opening_tag = 1\nij_php_blank_lines_after_package = 1\nij_php_blank_lines_around_class = 1\nij_php_blank_lines_around_constants = 0\nij_php_blank_lines_around_field = 1\nij_php_blank_lines_around_method = 1\nij_php_blank_lines_before_class_end = 0\nij_php_blank_lines_before_imports = 1\nij_php_blank_lines_before_method_body = 0\nij_php_blank_lines_before_package = 1\nij_php_blank_lines_before_return_statement = 1\nij_php_blank_lines_between_imports = 1\nij_php_block_brace_style = end_of_line\nij_php_call_parameters_new_line_after_left_paren = false\nij_php_call_parameters_right_paren_on_new_line = false\nij_php_call_parameters_wrap = normal\nij_php_catch_on_new_line = false\nij_php_category_weight = 28\nij_php_class_brace_style = next_line\nij_php_comma_after_last_array_element = true\nij_php_concat_spaces = false\nij_php_copyright_weight = 28\nij_php_deprecated_weight = 28\nij_php_do_while_brace_force = always\nij_php_else_if_style = combine\nij_php_else_on_new_line = false\nij_php_example_weight = 28\nij_php_extends_keyword_wrap = off\nij_php_extends_list_wrap = on_every_item\nij_php_fields_default_visibility = private\nij_php_filesource_weight = 28\nij_php_finally_on_new_line = false\nij_php_for_brace_force = always\nij_php_for_statement_new_line_after_left_paren = true\nij_php_for_statement_right_paren_on_new_line = true\nij_php_for_statement_wrap = off\nij_php_force_short_declaration_array_style = true\nij_php_getters_setters_naming_style = camel_case\nij_php_getters_setters_order_style = getters_first\nij_php_global_weight = 28\nij_php_group_use_wrap = on_every_item\nij_php_if_brace_force = always\nij_php_if_lparen_on_next_line = false\nij_php_if_rparen_on_next_line = false\nij_php_ignore_weight = 28\nij_php_import_sorting = alphabetic\nij_php_indent_break_from_case = true\nij_php_indent_case_from_switch = true\nij_php_indent_code_in_php_tags = false\nij_php_internal_weight = 28\nij_php_keep_blank_lines_after_lbrace = 0\nij_php_keep_blank_lines_before_right_brace = 0\nij_php_keep_blank_lines_in_code = 1\nij_php_keep_blank_lines_in_declarations = 0\nij_php_keep_control_statement_in_one_line = true\nij_php_keep_first_column_comment = true\nij_php_keep_indents_on_empty_lines = false\nij_php_keep_line_breaks = true\nij_php_keep_rparen_and_lbrace_on_one_line = true\nij_php_keep_simple_classes_in_one_line = false\nij_php_keep_simple_methods_in_one_line = false\nij_php_lambda_brace_style = end_of_line\nij_php_license_weight = 28\nij_php_line_comment_add_space = false\nij_php_line_comment_at_first_column = true\nij_php_link_weight = 28\nij_php_lower_case_boolean_const = true\nij_php_lower_case_keywords = true\nij_php_lower_case_null_const = true\nij_php_method_brace_style = next_line\nij_php_method_call_chain_wrap = off\nij_php_method_parameters_new_line_after_left_paren = true\nij_php_method_parameters_right_paren_on_new_line = true\nij_php_method_parameters_wrap = on_every_item\nij_php_method_weight = 28\nij_php_modifier_list_wrap = false\nij_php_multiline_chained_calls_semicolon_on_new_line = false\nij_php_namespace_brace_style = 1\nij_php_new_line_after_php_opening_tag = true\nij_php_null_type_position = in_the_end\nij_php_package_weight = 28\nij_php_param_weight = 0\nij_php_parameters_attributes_wrap = split_into_lines\nij_php_parentheses_expression_new_line_after_left_paren = false\nij_php_parentheses_expression_right_paren_on_new_line = false\nij_php_phpdoc_blank_line_before_tags = true\nij_php_phpdoc_blank_lines_around_parameters = true\nij_php_phpdoc_keep_blank_lines = true\nij_php_phpdoc_param_spaces_between_name_and_description = 1\nij_php_phpdoc_param_spaces_between_tag_and_type = 1\nij_php_phpdoc_param_spaces_between_type_and_name = 1\nij_php_phpdoc_use_fqcn = false\nij_php_phpdoc_wrap_long_lines = false\nij_php_place_assignment_sign_on_next_line = false\nij_php_place_parens_for_constructor = 0\nij_php_property_read_weight = 28\nij_php_property_weight = 28\nij_php_property_write_weight = 28\nij_php_return_type_on_new_line = false\nij_php_return_weight = 1\nij_php_see_weight = 28\nij_php_since_weight = 28\nij_php_sort_phpdoc_elements = true\nij_php_space_after_colon = true\nij_php_space_after_colon_in_named_argument = true\nij_php_space_after_colon_in_return_type = true\nij_php_space_after_comma = true\nij_php_space_after_for_semicolon = true\nij_php_space_after_quest = true\nij_php_space_after_type_cast = true\nij_php_space_after_unary_not = true\nij_php_space_before_array_initializer_left_brace = false\nij_php_space_before_catch_keyword = true\nij_php_space_before_catch_left_brace = true\nij_php_space_before_catch_parentheses = true\nij_php_space_before_class_left_brace = true\nij_php_space_before_closure_left_parenthesis = true\nij_php_space_before_colon = true\nij_php_space_before_colon_in_named_argument = false\nij_php_space_before_colon_in_return_type = false\nij_php_space_before_comma = false\nij_php_space_before_do_left_brace = true\nij_php_space_before_else_keyword = true\nij_php_space_before_else_left_brace = true\nij_php_space_before_finally_keyword = true\nij_php_space_before_finally_left_brace = true\nij_php_space_before_for_left_brace = true\nij_php_space_before_for_parentheses = true\nij_php_space_before_for_semicolon = false\nij_php_space_before_if_left_brace = true\nij_php_space_before_if_parentheses = true\nij_php_space_before_method_call_parentheses = false\nij_php_space_before_method_left_brace = true\nij_php_space_before_method_parentheses = false\nij_php_space_before_quest = true\nij_php_space_before_short_closure_left_parenthesis = true\nij_php_space_before_switch_left_brace = true\nij_php_space_before_switch_parentheses = true\nij_php_space_before_try_left_brace = true\nij_php_space_before_unary_not = false\nij_php_space_before_while_keyword = true\nij_php_space_before_while_left_brace = true\nij_php_space_before_while_parentheses = true\nij_php_space_between_ternary_quest_and_colon = false\nij_php_spaces_around_additive_operators = true\nij_php_spaces_around_arrow = false\nij_php_spaces_around_assignment_in_declare = false\nij_php_spaces_around_assignment_operators = true\nij_php_spaces_around_bitwise_operators = true\nij_php_spaces_around_equality_operators = true\nij_php_spaces_around_logical_operators = true\nij_php_spaces_around_multiplicative_operators = true\nij_php_spaces_around_null_coalesce_operator = true\nij_php_spaces_around_relational_operators = true\nij_php_spaces_around_shift_operators = true\nij_php_spaces_around_unary_operator = false\nij_php_spaces_around_var_within_brackets = false\nij_php_spaces_within_array_initializer_braces = false\nij_php_spaces_within_brackets = false\nij_php_spaces_within_catch_parentheses = false\nij_php_spaces_within_for_parentheses = false\nij_php_spaces_within_if_parentheses = false\nij_php_spaces_within_method_call_parentheses = false\nij_php_spaces_within_method_parentheses = false\nij_php_spaces_within_parentheses = false\nij_php_spaces_within_short_echo_tags = true\nij_php_spaces_within_switch_parentheses = false\nij_php_spaces_within_while_parentheses = false\nij_php_special_else_if_treatment = false\nij_php_subpackage_weight = 28\nij_php_ternary_operation_signs_on_next_line = true\nij_php_ternary_operation_wrap = normal\nij_php_throws_weight = 2\nij_php_todo_weight = 28\nij_php_unknown_tag_weight = 28\nij_php_upper_case_boolean_const = false\nij_php_upper_case_null_const = false\nij_php_uses_weight = 28\nij_php_var_weight = 28\nij_php_variable_naming_style = camel_case\nij_php_version_weight = 28\nij_php_while_brace_force = always\nij_php_while_on_new_line = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "/.editorconfig           export-ignore\n/.gitattributes          export-ignore\n/.github                 export-ignore\n/.gitignore              export-ignore\n/art                     export-ignore\n/phpstan.neon.dist       export-ignore\n/phpunit.xml.dist        export-ignore\n/pint.json               export-ignore\n/tests                   export-ignore\n/UPGRADING.md            export-ignore\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Automated testing\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: true\n      matrix:\n        php: [8.2, 8.3, 8.4, 8.5]\n        laravel: [11, 12, 13]\n        include:\n          - laravel: 11\n            testbench: 9\n          - laravel: 12\n            testbench: 10\n          - laravel: '13'\n            testbench: '11'\n        exclude:\n          - laravel: '13'\n            php: 8.2\n\n    name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          ini-values: error_reporting=E_ALL\n          tools: composer:v2\n          coverage: none\n\n      - name: Install dependencies\n        run: |\n          composer require \"illuminate/contracts:^${{ matrix.laravel }}\" \"orchestra/testbench:^${{ matrix.testbench }}\" --no-interaction --no-update\n          composer update --prefer-dist --no-interaction --no-progress\n\n      - name: Execute tests\n        run: composer test\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n.php_cs\n.php_cs.cache\n.phpunit.cache\n.phpunit.result.cache\ncomposer.lock\nphpunit.xml\nphpstan.neon\nvendor\nnode_modules\n.php-cs-fixer.cache\n"
  },
  {
    "path": ".stubs.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Illuminate\\Http\n{\n    class RedirectResponse\n    {\n        public function error(string $message, array $replace = []): RedirectResponse {}\n\n        public function info(string $message, array $replace = []): RedirectResponse {}\n\n        public function success(string $message, array $replace = []): RedirectResponse {}\n\n        public function warning(string $message, array $replace = []): RedirectResponse {}\n    }\n}\n\nnamespace Illuminate\\Routing\n{\n    class Redirector\n    {\n        public function error(string $message, array $replace = []): Redirector {}\n\n        public function info(string $message, array $replace = []): Redirector {}\n\n        public function success(string $message, array $replace = []): Redirector {}\n\n        public function warning(string $message, array $replace = []): Redirector {}\n    }\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to `livewire-toaster` will be documented in this file.\n\n## 2.9.0 - 2026-01-17\n\n### Added\n\n- Livewire 4 support\n\n## 2.8.0 - 2025-05-02\n\n### Added\n\n- Tailwind 4 support\n\n## 2.7.0 - 2025-02-25\n\n### Added\n\n- Laravel 12 support\n\n## 2.6.0 - 2024-12-01\n\n### Added\n\n- PHP 8.4 support\n\n## 2.5.0 - 2024-11-25\n\n### Added\n\n- Replacement of similar toasts\n\n## 2.4.0 - 2024-11-24\n\n### Added\n\n- Suppression of duplicate toasts\n\n## 2.3.2 - 2024-11-06\n\n### Fixed\n\n- Use the Livewire relay if redirecting using navigate\n\n## 2.3.1 - 2024-07-21\n\n### Fixed\n\n- Removed lingering #[Override] attribute\n\n## 2.3.0 - 2024-07-20\n\n### Added\n\n- PHP 8.2 support\n\n## 2.2.1 - 2024-04-09\n\n### Fixed\n\n- Prevent error in toast disposal when `$el` is `null`\n\n## 2.2.0 - 2024-03-11\n\n### Added\n\n- Laravel 11 support\n\n### Removed\n\n- Laravel 10 support\n\n## 2.1.0 - 2023-12-13\n\n### Added\n\n- PHP 8.3 support\n\n### Removed\n\n- PHP 8.2 support\n\n## 2.0.3 - 2023-09-28\n\n- Prevent unfinalize usage\n\n## 2.0.2 - 2023-09-23\n\n### Fixed\n\n- Add Octane support by @yehorherasymchuk in [#23](https://github.com/masmerise/livewire-toaster/pull/23)\n\n## 2.0.1 - 2023-09-04\n\n### Fixed\n\n- Dispatch events on the `document` node instead of `window`\n\n## 2.0.0 - 2023-08-24\n\n### Added\n\n- Livewire 3 support\n\n### Removed\n\n- Livewire 2 support\n\n## 1.3.0 - 2023-08-05\n\n### Added\n\n- RTL support\n\n## 1.2.1 - 2023-07-03\n\n### Fixed\n\n- `scope` the `toaster` service in order to support Laravel Octane\n\n## 1.2.0 - 2023-06-13\n\n### Added\n\n- Middle alignment by @aldozumaran in [#9](https://github.com/masmerise/livewire-toaster/pull/9)\n\n## 1.1.2 - 2023-05-08\n\n### Fixed\n\n- Register `ToastableMacros` with the `Redirector`\n\n## 1.1.1 - 2023-04-28\n\n- (Performance) improvements\n\n## 1.1.0 - 2023-04-21\n\n### Added\n\n- Vertically alignable toast container\n\n## 1.0.0 - 2023-02-25\n\n- Stable release\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) Muhammed Sari <support@muhammedsari.me>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"https://github.com/masmerise/livewire-toaster/raw/master/art/banner.png\" alt=\"Toaster Banner\"></p>\n\n# Beautiful toast notifications for Livewire\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/masmerise/livewire-toaster.svg?style=flat-square)](https://packagist.org/packages/masmerise/livewire-toaster)\n[![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)\n[![Total Downloads](https://img.shields.io/packagist/dt/masmerise/livewire-toaster.svg?style=flat-square)](https://packagist.org/packages/masmerise/livewire-toaster)\n\n**Toaster** provides a seamless experience to display toast notifications in your Livewire powered Laravel apps.\n\nUnlike many other toast implementations that are available, Toaster makes it effortless to dispatch a toast notification\nfrom either a standard `Controller` or a Livewire `Component`. You don't have to think about \"flashing\" things to the\nsession or \"dispatching browser events\" from your Livewire components. Just dispatch your toast and Toaster will route the message accordingly.\n\n## Showcase\n\n<p align=\"center\"><img src=\"https://github.com/masmerise/livewire-toaster/raw/master/art/showcase.gif\" alt=\"Toaster Demo\"></p>\n\n## Compatibility\n\n<table>\n<tr><th>Livewire</th><th>PHP</th><th>Laravel</th></tr>\n<tr><td>\n\n| | [2](https://laravel-livewire.com/docs/2.x) | [3 & 4](https://livewire.laravel.com/docs) |\n|-|-|-|\n| [1.x](https://github.com/masmerise/livewire-toaster/tree/1.3.0) | ✅ | ❌                                          |\n| 2.x | ❌ | ✅ |\n\n\n</td><td>\n\n| | 8.2 | 8.3 | 8.4 | 8.5\n|-|-|-|-|-|\n| 1.0 - ∞ | ✅ | ✅ | ✅ | ✅ |\n\n</td><td>\n\n| | 10 | 11 | 12 | 13\n|-|-|-|-|-|\n| 1.0 - 2.1 * | ✅ | ❌ | ❌ | ❌\n| 2.2 - ∞ | ❌ | ✅ | ✅ | ✅\n\n</tr> </table>\n\n_* feature complete_\n\n## Contents\n\n**Looking for v1 docs?** [Click here](https://github.com/masmerise/livewire-toaster/tree/1.3.0).\n\n- [Installation](#installation)\n  - [Preparing your template](#preparing-your-template)\n  - [Configuring scripts](#configuring-scripts)\n  - [Tailwind styles](#tailwind-styles)\n  - [RTL support](#rtl-support)\n- [Usage](#usage)\n  - [Sending toasts from the back-end](#sending-toasts-from-the-back-end)\n  - [Sending toasts from the front-end](#sending-toasts-from-the-front-end)\n  - [Automatic translation of messages](#automatic-translation-of-messages)\n  - [Accessibility](#accessibility)\n  - [Replacing similar toasts](#replacing-similar-toasts)\n  - [Suppressing duplicate toasts](#suppressing-duplicate-toasts)\n  - [Unit testing](#unit-testing)\n  - [Extending behavior](#extending-behavior)\n- [View customization](#view-customization)\n- [Testing](#testing)\n- [Changelog](#changelog)\n- [Security](#security)\n- [Credits](#credits)\n- [License](#license)\n\n## Installation\n\nYou can install the package via [composer](https://getcomposer.org):\n\n```bash\ncomposer require masmerise/livewire-toaster\n```\n\nYou can publish the package's config file:\n\n```bash\nphp artisan vendor:publish --tag=toaster-config\n```\n\nThis is the contents of the `toaster.php` config file:\n\n```php\nreturn [\n\n    /**\n     * Add an additional second for every 100th word of the toast messages.\n     *\n     * Supported: true | false\n     */\n    'accessibility' => true,\n\n    /**\n     * The vertical alignment of the toast container.\n     *\n     * Supported: \"bottom\", \"middle\" or \"top\"\n     */\n    'alignment' => 'bottom',\n\n    /**\n     * Allow users to close toast messages prematurely.\n     *\n     * Supported: true | false\n     */\n    'closeable' => true,\n\n    /**\n     * The on-screen duration of each toast.\n     *\n     * Minimum: 3000 (in milliseconds)\n     */\n    'duration' => 3000,\n\n    /**\n     * The horizontal position of each toast.\n     *\n     * Supported: \"center\", \"left\" or \"right\"\n     */\n    'position' => 'right',\n\n    /**\n     * New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.\n     * Takes precedence over the \"suppress\" option.\n     *\n     * Supported: true | false\n     */\n    'replace' => false,\n\n    /**\n     * Prevent the display of duplicate toast messages.\n     *\n     * Supported: true | false\n     */\n    'suppress' => false,\n\n    /**\n     * Whether messages passed as translation keys should be translated automatically.\n     *\n     * Supported: true | false\n     */\n    'translate' => true,\n];\n```\n\n### Preparing your template\n\nNext, you'll need to use the `<x-toaster-hub />` component in your master template:\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n    <!-- ... -->\n</head>\n\n<body>\n    <!-- Application content -->\n\n    <x-toaster-hub /> <!-- 👈 -->\n</body>\n</html>\n```\n\n### Configuring scripts\n\nAfter that, you'll need to import `Toaster` at the top of your `resources/js/app.js` bundle to start listening to incoming toasts:\n\n```js\nimport './bootstrap';\nimport '../../vendor/masmerise/livewire-toaster/resources/js'; // 👈\n\n// other app stuff...\n```\n\n### Tailwind styles\n\n> [!NOTE]\n> Skip this step if you're going to customize Toaster's default view.\n\nToaster provides a minimal view that utilizes Tailwind CSS defaults.\n\nIf the default toast appearances suffice your needs, you'll need to register it with Tailwind's purge list:\n\nFor Tailwind CSS Version < 4.x\n```js\nmodule.exports = {\n    content: [\n        './resources/**/*.blade.php',\n        './vendor/masmerise/livewire-toaster/resources/views/*.blade.php', // 👈\n    ],\n}\n```\n\nFor Tailwind CSS V >= 4+ you'll need to add this to your `app.css` file:\n\n> [!NOTE]\n> 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+.\n\n```css\n@import \"tailwindcss\";\n...\n@source '../../vendor/masmerise/livewire-toaster/resources/views/*.blade.php'; /* 👈 */\n```\nor into the file `tailwind.config.js`:\n\n> [!NOTE]\n> 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.\n\n```js\n/** @type {import('tailwindcss').Config} */\nexport default {\n    content: [\n        \"./resources/**/*.blade.php\",\n        \"./resources/**/*.js\",\n        \"./resources/**/*.vue\",\n        \"./vendor/masmerise/livewire-toaster/resources/views/*.blade.php\", // 👈\n    ],\n    plugins: [],\n};\n```\n\nOtherwise, please refer to [View customization](#view-customization).\n\n### RTL support\n\n> [!NOTE]\n> **LTR** will be assumed regardless of whether you apply the `ltr` attribute or not.\n\nIf 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:\n\n```html\n<!DOCTYPE html>\n<html dir=\"rtl\"> <!-- 👈 -->\n    ...\n</html>\n```\n\nThis will make sure the UI elements (such as the close button) are flipped and the text is properly aligned.\n\n## Usage\n\n### Sending toasts from the back-end\n\n> [!NOTE]\n> Toaster supports the dispatch of multiple toasts at once, you are not limited to dispatching a single toast.\n\n#### Toaster\n\nThe standard recommended way for dispatching toast messages is through the `Toaster` facade.\n\n```php\nuse Masmerise\\Toaster\\Toaster;\n\nfinal class RegistrationForm extends Component\n{\n    public function submit(): void\n    {\n        $this->validate();\n\n        User::create($this->form);\n\n        Toaster::success('User created!'); // 👈\n    }\n}\n```\n\nIf you need fine-grained control, you can always use the `PendingToast` class directly to which `Toaster` proxies its calls:\n\n```php\nuse Masmerise\\Toaster\\PendingToast;\n\nfinal class RegistrationForm extends Component\n{\n    public function submit(): void\n    {\n        $this->validate();\n\n        $user = User::create($this->form);\n\n        // 👇\n        PendingToast::create()\n            ->when($user->isAdmin(),\n                fn (PendingToast $toast) => $toast->message('Admin created')\n            )\n            ->unless($user->isAdmin(),\n                fn (PendingToast $toast) => $toast->message('User created')\n            )\n            ->success();\n    }\n}\n```\n\n#### Toastable\n\nYou can make any class `Toastable` to dispatch toasts from:\n\n```php\nuse Masmerise\\Toaster\\Toastable;\n\nfinal class ProductListing extends Component\n{\n    use Toastable; // 👈\n\n    public function check(): void\n    {\n        $result = Product::query()\n            ->tap(new Available())\n            ->count();\n\n        if ($result < 5) {\n            $this->warning('The quantity on hand is critically low.'); // 👈\n        }\n    }\n}\n```\n\n#### Redirects\n\nWhenever you return a `RedirectResponse` from anywhere in your app, you can chain any of the `Toaster` methods\nto dispatch a toast message:\n\n```php\nfinal class CompanyController extends Controller\n{\n    /** @throws ValidationException */\n    public function store(Request $request): RedirectResponse\n    {\n        $validator = Validator::make($request->all(), [...]);\n\n        if ($validator->fails()) {\n            return Redirect::back()\n                ->error('The form contains several errors'); // 👈\n        }\n\n        Company::create($validator->validate());\n\n        return Redirect::route('dashboard')\n            ->info('Company created!'); // 👈\n    }\n}\n```\n\nThis is, of course, **not** limited to `Controller`s as you can also redirect in Livewire `Component`s.\n\n#### Dependency injection\n\nIf you'd like to keep things \"pure\", you can also inject the `Collector` contract\nand use the `ToastBuilder` to dispatch your toasts:\n\n```php\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\ToasterConfig;\nuse Masmerise\\Toaster\\ToastBuilder;\n\nfinal readonly class SendEmailVerifiedNotification\n{\n    public function __construct(\n        private ToasterConfig $config,\n        private Collector $toasts,\n    ) {}\n\n    public function handle(Verified $event): void\n    {\n        $toast = ToastBuilder::create()\n            ->duration($this->config->duration)\n            ->success()\n            ->message(\"Thank you, {$event->user->name}!\")\n            ->get();\n\n        $this->toasts->collect($toast);\n    }\n}\n```\n\n### Sending toasts from the front-end\n\nYou can invoke the globally available `Toaster` instance to dispatch any toast message from anywhere:\n\n```html\n<button @click=\"Toaster.success('Form submitted!')\">\n    Submit\n</button>\n```\n\nAvailable methods: `error`, `info`, `warning` & `success`\n\n### Automatic translation of messages\n\n> [!NOTE]\n> The `translate` configuration value must be set to `true`.\n\nInstead of doing this:\n\n```php\nToaster::success(\n    Lang::get('path.to.translation', ['replacement' => 'value'])\n);\n```\n\nToaster makes it possible to do this:\n\n```php\nToaster::success('path.to.translation', ['replacement' => 'value']);\n```\n\nYou can mix and match without any problems:\n\n```php\nToaster::info('user.created', ['name' => $user->full_name]);\nToaster::info('You now have full access!');\n```\n\nYou can do whatever you want, whenever you want.\n\n### Accessibility\n\n> [!NOTE]\n> The `accessibility` configuration value must be set to `true`.\n\nToaster will add an additional second to a toast's on-screen duration for every 100th word.\nThis way, your users will have enough time to read toasts that are a tad larger than usual.\n\nSo, if your base duration value is `3 seconds` and your toast contains 223 words,\nthe total on-screen duration of the toast will be `3 + 2 = 5 seconds`\n\n### Replacing similar toasts\n\n> [!NOTE]\n> The `replace` configuration value must be set to `true`.\n\n> [!WARNING]\n> Takes precedence over `suppress`.\n\nToaster will dispose of any toast that is similar to the one being dispatched prior to displaying the new toast.\nA toast is considered similar if it has the same `duration`, `message`, and `type`.\n\n### Suppressing duplicate toasts\n\n> [!NOTE]\n> The `suppress` configuration value must be set to `true`.\n\nToaster will prevent the display of duplicate toast messages while another toast with the same message is still on-screen.\nA toast is considered a duplicate if it has the same `duration`, `message`, and `type`.\n\n### Unit testing\n\n> [!NOTE]\n> 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.\n> Otherwise, your tests are going to fail as the messages are not translated during unit testing.\n\nToaster provides a couple of testing capabilities in order for you to build a robust application:\n\n```php\nuse Masmerise\\Toaster\\Toaster;\n\nfinal class RegisterUserControllerTest extends TestCase\n{\n    #[Test]\n    public function users_can_register(): void\n    {\n        // Arrange\n        Toaster::fake();\n        Toaster::assertNothingDispatched();\n\n        // Act\n        $response = $this->post('users', [ ... ]);\n\n        // Assert\n        $response->assertRedirect('profile');\n        Toaster::assertDispatched('Welcome!');\n    }\n}\n```\n\n### Extending behavior\n\nImagine that you'd like to keep track of how many toasts are dispatched daily to display on an admin dashboard.\nFirst, create a new class that encapsulates this logic:\n\n```php\nfinal readonly class DailyCountingCollector implements Collector\n{\n    public function __construct(private Collector $next) {}\n\n    public function collect(Toast $toast): void\n    {\n        // increment the counter on durable storage\n\n        $this->next->collect($toast);\n    }\n\n    public function release(): array\n    {\n        return $this->next->release();\n    }\n}\n```\n\nAfter that, extend the behavior in your `AppServiceProvider`:\n\n```php\npublic function register(): void\n{\n    $this->app->extend(Collector::class,\n        static fn (Collector $next) => new DailyCountingCollector($next)\n    );\n}\n```\n\nThat's it!\n\n## View customization\n\nEven though the default toasts are pretty, they might not fit your design and you may want to customize them.\n\nYou can do so by publishing Toaster's views:\n\n```php\nphp artisan vendor:publish --tag=toaster-views\n```\n\nThe `hub.blade.php` view will be published to your application's `resources/views/vendor/toaster` directory.\nFeel free to modify anything to your liking.\n\n### Available `viewData`\n\n- `$alignment` - can be used to align the toast container vertically depending on the configuration\n- `$closeable` - whether the close button should be rendered by the Blade component\n- `$config` - default configuration values, used by the Alpine component\n- `$position` - can be used to position the toasts depending on the configuration\n- `$toasts` - toasts that were flashed to the session by Toaster, used by the Alpine component\n\n> [!WARNING]\n> You **must** keep the `x-data` and `x-init` directives and you **must** keep using the `x-for` loop.\n> Otherwise, the Alpine component that powers Toaster will start malfunctioning.\n\n\n## Testing\n\n```bash\ncomposer test\n```\n\n## Changelog\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\n## Security\n\nIf you discover any security related issues, please email support@muhammedsari.me instead of using the issue tracker.\n\n## Credits\n\n- [Muhammed Sari](https://github.com/masmerise)\n- [Greg Korba](https://github.com/wirone)\n- [All Contributors](../../contributors)\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n"
  },
  {
    "path": "UPGRADING.md",
    "content": "# Upgrade Guide\n\n## v1 → v2\n\n### Minimum versions\n\nThe following dependency versions have been updated:\n\n- The minimum Livewire version is now v3.0\n\n### Adjusting the `Toaster` import\n\n`Alpine` is now registered by Livewire automatically. Therefore, any direct `Alpine.plugin` or `Alpine.start` call will no longer work.\nYou should adjust your `app.js` JavaScript entry point to account for this change. An example:\n\n```diff\nimport './bootstrap';\n+ import '../../vendor/masmerise/livewire-toaster/resources/js';\n\n- import Alpine from 'alpinejs';\n- import Toaster from '../../vendor/masmerise/livewire-toaster/resources/js';\n\n- Alpine.plugin(Toaster);\n\n- window.Alpine = Alpine;\n- Alpine.start();\n```\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"masmerise/livewire-toaster\",\n    \"description\": \"Beautiful toast notifications for Laravel / Livewire.\",\n    \"license\": \"MIT\",\n    \"keywords\": [\n        \"alert\",\n        \"laravel\",\n        \"livewire\",\n        \"toast\",\n        \"toaster\"\n    ],\n    \"authors\": [\n        {\n            \"name\": \"Muhammed Sari\",\n            \"email\": \"support@muhammedsari.me\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"homepage\": \"https://github.com/masmerise/livewire-toaster\",\n    \"require\": {\n        \"php\": \"~8.2 || ~8.3 || ~8.4 || ~8.5\",\n        \"illuminate/contracts\": \"^11.0 || ^12.0 || ^13.0\",\n        \"illuminate/http\": \"^11.0 || ^12.0 || ^13.0\",\n        \"illuminate/routing\": \"^11.0 || ^12.0 || ^13.0\",\n        \"illuminate/support\": \"^11.0 || ^12.0 || ^13.0\",\n        \"illuminate/view\": \"^11.0 || ^12.0 || ^13.0\",\n        \"livewire/livewire\": \"^3.0 || ^4.0\"\n    },\n    \"require-dev\": {\n        \"larastan/larastan\": \"^2.0 || ^3.1\",\n        \"laravel/pint\": \"^1.0\",\n        \"orchestra/testbench\": \"^9.0 || ^10.0 || ^11.0\",\n        \"phpunit/phpunit\": \"^10.0 || ^11.5.3 || ^12.5.12\"\n    },\n    \"conflict\": {\n        \"stevebauman/unfinalize\": \"*\"\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"Masmerise\\\\Toaster\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Tests\\\\\": \"tests\"\n        }\n    },\n    \"config\": {\n        \"sort-packages\": true\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"aliases\": {\n                \"Toaster\": \"Masmerise\\\\Toaster\\\\Toaster\"\n            },\n            \"providers\": [\n                \"Masmerise\\\\Toaster\\\\ToasterServiceProvider\"\n            ]\n        }\n    },\n    \"scripts\": {\n        \"format\": \"vendor/bin/pint\",\n        \"larastan\": \"vendor/bin/phpstan analyse --memory-limit=2G\",\n        \"test\": \"vendor/bin/phpunit\",\n        \"verify\": [\n            \"@larastan\",\n            \"@test\"\n        ]\n    }\n}\n"
  },
  {
    "path": "config/toaster.php",
    "content": "<?php declare(strict_types=1);\n\nreturn [\n\n    /**\n     * Add an additional second for every 100th word of the toast messages.\n     *\n     * Supported: true | false\n     */\n    'accessibility' => true,\n\n    /**\n     * The vertical alignment of the toast container.\n     *\n     * Supported: \"bottom\", \"middle\" or \"top\"\n     */\n    'alignment' => 'bottom',\n\n    /**\n     * Allow users to close toast messages prematurely.\n     *\n     * Supported: true | false\n     */\n    'closeable' => true,\n\n    /**\n     * The on-screen duration of each toast.\n     *\n     * Minimum: 3000 (in milliseconds)\n     */\n    'duration' => 3000,\n\n    /**\n     * The horizontal position of each toast.\n     *\n     * Supported: \"center\", \"left\" or \"right\"\n     */\n    'position' => 'right',\n\n    /**\n     * New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.\n     * Takes precedence over the \"suppress\" option.\n     *\n     * Supported: true | false\n     */\n    'replace' => false,\n\n    /**\n     * Prevent the display of duplicate toast messages.\n     *\n     * Supported: true | false\n     */\n    'suppress' => false,\n\n    /**\n     * Whether messages passed as translation keys should be translated automatically.\n     *\n     * Supported: true | false\n     */\n    'translate' => true,\n];\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n\nparameters:\n    paths:\n        - src\n\n    level: 9\n\n    ignoreErrors:\n        - identifier: missingType.generics\n        - identifier: missingType.iterableValue\n        - '#Cannot access offset .* on Illuminate\\\\Contracts\\\\Foundation\\\\Application#'\n        - '#Parameter \\#.* of class Masmerise\\\\Toaster\\\\ToasterConfig constructor expects .*, mixed given.#'\n        - '#Trait Masmerise\\\\Toaster\\\\Toastable is used zero times and is not analysed.#'\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.0/phpunit.xsd\"\n    bootstrap=\"vendor/autoload.php\"\n    colors=\"true\"\n    executionOrder=\"random\"\n    failOnWarning=\"true\"\n    failOnRisky=\"true\"\n    failOnEmptyTestSuite=\"true\"\n    beStrictAboutOutputDuringTests=\"true\"\n    cacheDirectory=\".phpunit.cache\"\n>\n    <testsuites>\n        <testsuite name=\"Toaster Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\"/>\n        <env name=\"APP_KEY\" value=\"base64:TaUN5M30kEy4U+fj7RDG0+ppdVWwI9apTf1a0inKdP0=\"/>\n        <env name=\"SESSION_DRIVER\" value=\"array\"/>\n    </php>\n</phpunit>\n"
  },
  {
    "path": "pint.json",
    "content": "{\n    \"preset\": \"psr12\",\n    \"rules\": {\n        \"blank_line_after_opening_tag\": false,\n        \"blank_line_before_statement\": { \"statements\": [\"return\"] },\n        \"braces\": { \"allow_single_line_closure\": true },\n        \"concat_space\": { \"spacing\": \"one\" },\n        \"declare_strict_types\": true,\n        \"method_argument_space\": false,\n        \"no_extra_blank_lines\": { \"tokens\":  [\"curly_brace_block\", \"extra\", \"throw\", \"use\"] },\n        \"no_useless_else\": true,\n        \"not_operator_with_successor_space\": true,\n        \"ordered_imports\": { \"imports_order\": [\"class\", \"function\", \"const\"], \"sort_algorithm\": \"alpha\" },\n        \"single_line_empty_body\": true,\n        \"trailing_comma_in_multiline\": true\n    }\n}\n"
  },
  {
    "path": "resources/js/config.js",
    "content": "class Alignment {\n    static Top = 'top';\n\n    constructor(value) {\n        this.value = value;\n    }\n\n    isTop() {\n        return this.value === Alignment.Top;\n    }\n}\n\nexport class Config {\n    constructor(alignment, duration, replace, suppress) {\n        this.alignment = new Alignment(alignment);\n        this.duration = duration;\n        this.replace = replace;\n        this.suppress = suppress;\n    }\n\n    static fromJson(data) {\n        return new Config(data.alignment, data.duration, data.replace, data.suppress);\n    }\n}\n"
  },
  {
    "path": "resources/js/hub.js",
    "content": "import { Config } from './config';\nimport { Toast } from './toast';\n\nexport function Hub(Alpine) {\n    Alpine.data('toasterHub', (initialToasts, config) => {\n        config = Config.fromJson(config);\n\n        return {\n            _toasts: [],\n\n            get toasts() {\n                const toasts = this._toasts.filter(t => ! t.trashed);\n\n                if (this._toasts.length && ! toasts.length) {\n                    this.$nextTick(() => { this._toasts = []; });\n                }\n\n                return toasts;\n            },\n\n            init() {\n                document.addEventListener('toaster:received', event => {\n                    this.processToast(event);\n                });\n\n                initialToasts.map(Toast.fromJson).forEach(toast => this.show(toast));\n            },\n            \n            processToast(data) {\n                const toast = Toast.fromJson({ duration: config.duration, ...data.detail });\n                \n                if (config.replace) {\n                    this.toasts.filter(t => t.equals(toast)).forEach(t => t.dispose());\n                } else if (config.suppress && this.toasts.some(t => t.equals(toast))) {\n                    return;\n                }\n\n                this.show(toast);\n            },\n            \n            show(toast) {\n                toast = Alpine.reactive(toast);\n                toast.runAfterDuration(toast => toast.dispose());\n\n                if (config.alignment.isTop()) {\n                    this._toasts.unshift(toast);\n                } else {\n                    this._toasts.push(toast);\n                }\n            },\n        }\n    });\n}\n"
  },
  {
    "path": "resources/js/index.js",
    "content": "import { Hub } from './hub';\nimport * as Toaster from './toaster';\n\nwindow.Toaster = Toaster;\n\ndocument.addEventListener('alpine:init', () => {\n    window.Alpine.plugin(Hub);\n});\n"
  },
  {
    "path": "resources/js/toast.js",
    "content": "import { uuid41 } from './uuid41';\n\nexport class Toast {\n    constructor(duration, message, type) {\n        this.$el = null;\n        this.id = uuid41();\n        this.isVisible = false;\n        this.duration = duration;\n        this.message = message;\n        this.timeout = null;\n        this.trashed = false;\n        this.type = type;\n    }\n\n    static fromJson(data) {\n        return new Toast(data.duration, data.message, data.type);\n    }\n\n    dispose() {\n        if (this.timeout) {\n            clearTimeout(this.timeout);\n        }\n\n        this.isVisible = false;\n\n        if (this.$el) {\n            this.$el.addEventListener('transitioncancel', () => { this.trashed = true; })\n            this.$el.addEventListener('transitionend', () => { this.trashed = true; })\n        }\n    }\n\n    equals(other) {\n        return this.duration === other.duration\n            && this.message === other.message\n            && this.type === other.type;\n    }\n\n    runAfterDuration(callback) {\n        this.timeout = setTimeout(() => callback(this), this.duration);\n    }\n\n    select(config) {\n        return config[this.type];\n    }\n\n    show($el) {\n        this.$el = $el;\n        this.isVisible = true;\n    }\n}\n"
  },
  {
    "path": "resources/js/toaster.js",
    "content": "const event = (message, type) => {\n    document.dispatchEvent(new CustomEvent('toaster:received', { detail: { message, type }}));\n};\n\nconst error = message => event(message, 'error');\nconst info = message => event(message, 'info');\nconst success = message => event(message, 'success');\nconst warning = message => event(message, 'warning');\n\nexport { error, info, success, warning }\n"
  },
  {
    "path": "resources/js/uuid41.js",
    "content": "export function uuid41() {\n    let d = '';\n\n    while (d.length < 32) {\n        d += Math.random().toString(16).substring(2);\n    }\n\n    const vr = ((Number.parseInt(d.substring(16, 1), 16) & 0x3) | 0x8).toString(16);\n\n    return `${d.substring(0, 8)}-${d.substring(8, 4)}-4${d.substring(13, 3)}-${vr}${d.substring(17, 3)}-${d.substring(20, 12)}`;\n}\n"
  },
  {
    "path": "resources/views/close-button.blade.php",
    "content": "<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' }}\">\r\n    <svg class=\"h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\r\n        <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>\r\n    </svg>\r\n</button>"
  },
  {
    "path": "resources/views/hub.blade.php",
    "content": "<div role=\"status\" id=\"toaster\" x-data=\"toasterHub(@js($toasts), @js($config))\" @class([\n    'fixed z-50 p-4 w-full flex flex-col pointer-events-none sm:p-6',\n    'bottom-0' => $alignment->is('bottom'),\n    'top-1/2 -translate-y-1/2' => $alignment->is('middle'),\n    'top-0' => $alignment->is('top'),\n    'items-start rtl:items-end' => $position->is('left'),\n    'items-center' => $position->is('center'),\n    'items-end rtl:items-start' => $position->is('right'),\n ])>\n    <template x-for=\"toast in toasts\" :key=\"toast.id\">\n        <div x-show=\"toast.isVisible\"\n             x-init=\"$nextTick(() => toast.show($el))\"\n             @if($alignment->is('bottom'))\n             x-transition:enter-start=\"translate-y-12 opacity-0\"\n             x-transition:enter-end=\"translate-y-0 opacity-100\"\n             @elseif($alignment->is('top'))\n             x-transition:enter-start=\"-translate-y-12 opacity-0\"\n             x-transition:enter-end=\"translate-y-0 opacity-100\"\n             @else\n             x-transition:enter-start=\"opacity-0 scale-90\"\n             x-transition:enter-end=\"opacity-100 scale-100\"\n             @endif\n             x-transition:leave-end=\"opacity-0 scale-90\"\n             @class(['relative duration-300 transform transition ease-in-out max-w-xs w-full pointer-events-auto', 'text-center' => $position->is('center')])\n             :class=\"toast.select({ error: 'text-white', info: 'text-black', success: 'text-white', warning: 'text-white' })\"\n        >\n            <i x-text=\"toast.message\"\n               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' }}\"\n               :class=\"toast.select({ error: 'bg-red-500', info: 'bg-gray-200', success: 'bg-green-600', warning: 'bg-orange-500' })\"\n            ></i>\n\n            @includeWhen($closeable, 'toaster::close-button')\n        </div>\n    </template>\n</div>\n"
  },
  {
    "path": "src/AccessibleCollector.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nfinal readonly class AccessibleCollector implements Collector\n{\n    private const AMOUNT_OF_WORDS = 100;\n    private const ONE_SECOND = 1000;\n\n    public function __construct(private Collector $next) {}\n\n    public function collect(Toast $toast): void\n    {\n        $addend = (int) floor(str_word_count($toast->message->value) / self::AMOUNT_OF_WORDS);\n        $addend = $addend * self::ONE_SECOND;\n\n        if ($addend > 0) {\n            $toast = ToastBuilder::proto($toast)->duration($toast->duration->value + $addend)->get();\n        }\n\n        $this->next->collect($toast);\n    }\n\n    public function release(): array\n    {\n        return $this->next->release();\n    }\n}\n"
  },
  {
    "path": "src/Alignment.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum Alignment: string\n{\n    use Assertable;\n\n    case Bottom = 'bottom';\n    case Middle = 'middle';\n    case Top = 'top';\n}\n"
  },
  {
    "path": "src/Assertable.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @interal */\ntrait Assertable\n{\n    public function is(string $value): bool\n    {\n        return $value === $this->value;\n    }\n}\n"
  },
  {
    "path": "src/Collector.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\ninterface Collector\n{\n    public function collect(Toast $toast): void;\n\n    /** @internal */\n    public function release(): array;\n}\n"
  },
  {
    "path": "src/Duration.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse InvalidArgumentException;\n\n/** @internal */\nfinal readonly class Duration\n{\n    private const MINIMUM = 3000;\n\n    public int $value;\n\n    private function __construct(int $value)\n    {\n        if ($value < self::MINIMUM) {\n            throw new InvalidArgumentException('The duration value must be at least 3000 ms.');\n        }\n\n        $this->value = $value;\n    }\n\n    public static function fromMillis(int $value): self\n    {\n        return new self($value);\n    }\n}\n"
  },
  {
    "path": "src/LivewireRelay.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Livewire\\Component;\nuse Livewire\\Features\\SupportEvents\\Event;\nuse Livewire\\Livewire;\nuse Livewire\\Mechanisms\\HandleComponents\\ComponentContext;\n\nuse function Livewire\\store;\n\n/** @internal */\nfinal readonly class LivewireRelay\n{\n    public const EVENT = 'toaster:received';\n\n    public function __invoke(Component $component, ComponentContext $ctx): void\n    {\n        if (! Livewire::isLivewireRequest()) {\n            return;\n        }\n\n        $isRedirecting = store($component)->get('redirect');\n        $isRedirectingUsingNavigate = store($component)->get('redirectUsingNavigate');\n\n        if ($isRedirecting && ! $isRedirectingUsingNavigate) {\n            return;\n        }\n\n        if ($toasts = Toaster::release()) {\n            foreach ($toasts as $toast) {\n                $event = new Event(self::EVENT, $toast->toArray());\n                $ctx->pushEffect('dispatches', $event->serialize());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Message.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse InvalidArgumentException;\n\n/** @internal */\nfinal readonly class Message\n{\n    public array $replace;\n\n    public string $value;\n\n    private function __construct(string $value, array $replace = [])\n    {\n        if (empty($value = trim($value))) {\n            throw new InvalidArgumentException('The message value cannot be empty.');\n        }\n\n        $this->value = $value;\n        $this->replace = $replace;\n    }\n\n    public static function fromString(string $value): self\n    {\n        return new self($value);\n    }\n\n    public static function fromTranslatable(string $value, array $replace = []): self\n    {\n        return new self($value, $replace);\n    }\n\n    public function equals(Message|string $other): bool\n    {\n        if ($other instanceof Message) {\n            $other = $other->value;\n        }\n\n        return $other === $this->value;\n    }\n}\n"
  },
  {
    "path": "src/PendingToast.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Traits\\ForwardsCalls;\n\n/**\n * @method PendingToast duration(int $milliseconds)\n * @method PendingToast error()\n * @method PendingToast info()\n * @method PendingToast message(string $message, array $replace = [])\n * @method PendingToast success()\n * @method PendingToast type(string $type)\n * @method PendingToast unless($value = null, callable $callback = null, callable $default = null)\n * @method PendingToast warning()\n * @method PendingToast when($value = null, callable $callback = null, callable $default = null)\n */\nfinal class PendingToast\n{\n    use ForwardsCalls;\n\n    private ToastBuilder $builder;\n\n    private bool $dispatched = false;\n\n    private function __construct(int $duration)\n    {\n        $this->builder = ToastBuilder::create()->duration($duration);\n    }\n\n    public static function create(): self\n    {\n        return new self(Toaster::config()->duration);\n    }\n\n    public function dispatch(): void\n    {\n        $toast = $this->builder->get();\n\n        Toaster::collect($toast);\n\n        $this->dispatched = true;\n    }\n\n    public function __call(string $name, array $arguments): mixed\n    {\n        $result = $this->forwardCallTo($this->builder, $name, $arguments);\n\n        if ($result instanceof ToastBuilder) {\n            $this->builder = $result;\n\n            return $this;\n        }\n\n        return $result;\n    }\n\n    public function __destruct()\n    {\n        if (! $this->dispatched) {\n            $this->dispatch();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Position.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum Position: string\n{\n    use Assertable;\n\n    case Center = 'center';\n    case Left = 'left';\n    case Right = 'right';\n}\n"
  },
  {
    "path": "src/QueuingCollector.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nfinal class QueuingCollector implements Collector\n{\n    private array $toasts = [];\n\n    public function collect(Toast $toast): void\n    {\n        $this->toasts[] = $toast;\n    }\n\n    public function release(): array\n    {\n        $toasts = $this->toasts;\n        $this->toasts = [];\n\n        return $toasts;\n    }\n}\n"
  },
  {
    "path": "src/SessionRelay.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Closure;\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse Illuminate\\Contracts\\Session\\Session;\nuse Illuminate\\Http\\Request;\n\n/** @internal */\nfinal readonly class SessionRelay\n{\n    public const NAME = 'toasts';\n\n    public function __construct(private Application $app) {}\n\n    public function handle(Request $request, Closure $next): mixed\n    {\n        $response = $next($request);\n\n        if (! $this->app->resolved(Collector::class)) {\n            return $response;\n        }\n\n        if ($toasts = $this->app[Collector::class]->release()) {\n            $this->app[Session::class]->put(self::NAME, $this->serialize($toasts));\n        }\n\n        return $response;\n    }\n\n    private function serialize(array $toasts): array\n    {\n        return array_map(static fn (Toast $toast) => $toast->toArray(), $toasts);\n    }\n}\n"
  },
  {
    "path": "src/TestableCollector.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse PHPUnit\\Framework\\Assert as PHPUnit;\n\nfinal class TestableCollector implements Collector\n{\n    private array $toasts = [];\n\n    public function collect(Toast $toast): void\n    {\n        $this->toasts[] = $toast;\n    }\n\n    public function release(): array\n    {\n        $toasts = $this->toasts;\n        $this->toasts = [];\n\n        return $toasts;\n    }\n\n    public function assertDispatched(string $message): void\n    {\n        $toasts = array_filter($this->toasts, static fn (Toast $toast) => $toast->message->equals($message));\n\n        PHPUnit::assertNotEmpty($toasts, \"A toast with the message `{$message}` was not dispatched.\");\n    }\n\n    public function assertNothingDispatched(): void\n    {\n        $count = count($this->toasts);\n\n        PHPUnit::assertSame(0, $count, \"{$count} unexpected toasts were dispatched.\");\n    }\n}\n"
  },
  {
    "path": "src/Toast.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\n\nfinal readonly class Toast implements Arrayable\n{\n    public function __construct(\n        public Message $message,\n        public Duration $duration,\n        public ToastType $type,\n    ) {}\n\n    public function toArray(): array\n    {\n        return [\n            'duration' => $this->duration->value,\n            'message' => $this->message->value,\n            'type' => $this->type->value,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/ToastBuilder.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Traits\\Conditionable;\nuse UnexpectedValueException;\n\nfinal class ToastBuilder\n{\n    use Conditionable;\n\n    private ?Duration $duration = null;\n\n    private ?Message $message = null;\n\n    private ?ToastType $type = null;\n\n    public static function create(): self\n    {\n        return new self();\n    }\n\n    public static function proto(Toast $toast): self\n    {\n        $builder = self::create();\n        $builder->duration = $toast->duration;\n        $builder->message = $toast->message;\n        $builder->type = $toast->type;\n\n        return $builder;\n    }\n\n    public function duration(int $milliseconds): self\n    {\n        return $this->modify('duration', Duration::fromMillis($milliseconds));\n    }\n\n    public function error(): self\n    {\n        return $this->modify('type', ToastType::Error);\n    }\n\n    public function info(): self\n    {\n        return $this->modify('type', ToastType::Info);\n    }\n\n    public function message(string $message, array $replace = []): self\n    {\n        return $this->modify('message', Message::fromTranslatable($message, $replace));\n    }\n\n    public function success(): self\n    {\n        return $this->modify('type', ToastType::Success);\n    }\n\n    public function type(string $type): self\n    {\n        return $this->modify('type', ToastType::from($type));\n    }\n\n    public function warning(): self\n    {\n        return $this->modify('type', ToastType::Warning);\n    }\n\n    public function get(): Toast\n    {\n        if (! $this->duration instanceof Duration) {\n            throw new UnexpectedValueException('You must provide a valid duration.');\n        }\n\n        if (! $this->message instanceof Message) {\n            throw new UnexpectedValueException('You must provide a valid message.');\n        }\n\n        if (! $this->type instanceof ToastType) {\n            throw new UnexpectedValueException('You must choose a valid type.');\n        }\n\n        return new Toast($this->message, $this->duration, $this->type);\n    }\n\n    private function modify(string $property, mixed $value): self\n    {\n        $that = clone $this;\n        $that->{$property} = $value;\n\n        return $that;\n    }\n}\n"
  },
  {
    "path": "src/ToastType.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\n/** @internal */\nenum ToastType: string\n{\n    case Error = 'error';\n    case Info = 'info';\n    case Success = 'success';\n    case Warning = 'warning';\n}\n"
  },
  {
    "path": "src/Toastable.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\ntrait Toastable\n{\n    protected function error(string $message, array $replace = []): PendingToast\n    {\n        return Toaster::error($message, $replace);\n    }\n\n    protected function info(string $message, array $replace = []): PendingToast\n    {\n        return Toaster::info($message, $replace);\n    }\n\n    protected function success(string $message, array $replace = []): PendingToast\n    {\n        return Toaster::success($message, $replace);\n    }\n\n    protected function toast(): PendingToast\n    {\n        return Toaster::toast();\n    }\n\n    protected function warning(string $message, array $replace = []): PendingToast\n    {\n        return Toaster::warning($message, $replace);\n    }\n}\n"
  },
  {
    "path": "src/ToastableMacros.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Closure;\n\n/** @internal */\nfinal readonly class ToastableMacros\n{\n    protected function error(): Closure\n    {\n        return $this->macro('error');\n    }\n\n    protected function info(): Closure\n    {\n        return $this->macro('info');\n    }\n\n    protected function success(): Closure\n    {\n        return $this->macro('success');\n    }\n\n    protected function warning(): Closure\n    {\n        return $this->macro('warning');\n    }\n\n    private function macro(string $type): Closure\n    {\n        return function (string $message, array $replace = []) use ($type) {\n            Toaster::toast()->type($type)->message($message, $replace);\n\n            return $this;\n        };\n    }\n}\n"
  },
  {
    "path": "src/Toaster.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Facades\\Facade;\n\n/**\n * @method static void assertDispatched(string $message)\n * @method static void assertNothingDispatched()\n * @method static void collect(Toast $toast)\n * @method static array release()\n */\nfinal class Toaster extends Facade\n{\n    public static function config(): ToasterConfig\n    {\n        return self::$app[ToasterServiceProvider::CONFIG];\n    }\n\n    public static function error(string $message, array $replace = []): PendingToast\n    {\n        return self::toast()->message($message, $replace)->error();\n    }\n\n    public static function fake(): TestableCollector\n    {\n        self::swap($fake = new TestableCollector());\n\n        return $fake;\n    }\n\n    public static function info(string $message, array $replace = []): PendingToast\n    {\n        return self::toast()->message($message, $replace)->info();\n    }\n\n    public static function success(string $message, array $replace = []): PendingToast\n    {\n        return self::toast()->message($message, $replace)->success();\n    }\n\n    public static function toast(): PendingToast\n    {\n        return PendingToast::create();\n    }\n\n    public static function warning(string $message, array $replace = []): PendingToast\n    {\n        return self::toast()->message($message, $replace)->warning();\n    }\n\n    protected static function getFacadeAccessor(): string\n    {\n        return ToasterServiceProvider::NAME;\n    }\n\n    protected static function getMockableClass(): string\n    {\n        return Collector::class;\n    }\n}\n"
  },
  {
    "path": "src/ToasterConfig.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Support\\Arr;\n\n/** @internal */\nfinal readonly class ToasterConfig\n{\n    private function __construct(\n        public string $alignment,\n        public int $duration,\n        public string $position,\n        public bool $wantsAccessibility,\n        public bool $wantsCloseableToasts,\n        public bool $wantsReplacement,\n        public bool $wantsSuppression,\n        public bool $wantsTranslation,\n    ) {}\n\n    /**\n     * @param array{\n     *     alignment?: \"bottom\" | \"middle\" | \"top\",\n     *     duration?: int,\n     *     position?: \"center\" | \"left\" | \"right\",\n     *     accessibility?: bool,\n     *     closeable?: bool,\n     *     replace?: bool,\n     *     suppress?: bool,\n     *     translate?: bool,\n     * } $config\n     */\n    public static function fromArray(array $config): self\n    {\n        return new self(\n            Arr::get($config, 'alignment', 'bottom'),\n            Arr::get($config, 'duration', 3000),\n            Arr::get($config, 'position', 'right'),\n            Arr::get($config, 'accessibility', true),\n            Arr::get($config, 'closeable', true),\n            Arr::get($config, 'replace', false),\n            Arr::get($config, 'suppress', false),\n            Arr::get($config, 'translate', true),\n        );\n    }\n\n    public function alignment(): Alignment\n    {\n        return Alignment::from($this->alignment);\n    }\n\n    public function position(): Position\n    {\n        return Position::from($this->position);\n    }\n\n    public function toJavaScript(): array\n    {\n        return [\n            'alignment' => $this->alignment,\n            'duration' => $this->duration,\n            'replace' => $this->wantsReplacement,\n            'suppress' => $this->wantsSuppression,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/ToasterHub.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Session\\Session;\nuse Illuminate\\Contracts\\View\\View;\nuse Illuminate\\View\\Component;\n\n/** @internal */\nfinal class ToasterHub extends Component\n{\n    public const NAME = 'toaster-hub';\n\n    public function __construct(\n        private readonly ToasterConfig $config,\n        private readonly Session $session,\n        private readonly string $view = 'toaster::hub',\n    ) {}\n\n    public function render(): View\n    {\n        return $this->view($this->view, [\n            'alignment' => $this->config->alignment(),\n            'closeable' => $this->config->wantsCloseableToasts,\n            'config' => $this->config->toJavaScript(),\n            'position' => $this->config->position(),\n            'toasts' => $this->session->pull(SessionRelay::NAME, []),\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/ToasterServiceProvider.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Redirector;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Support\\AggregateServiceProvider;\nuse Illuminate\\View\\Compilers\\BladeCompiler;\nuse Livewire\\LivewireManager;\nuse Livewire\\LivewireServiceProvider;\n\nfinal class ToasterServiceProvider extends AggregateServiceProvider\n{\n    public const CONFIG = 'toaster.config';\n    public const NAME = 'toaster';\n\n    protected $providers = [LivewireServiceProvider::class];\n\n    public function boot(): void\n    {\n        $this->loadViewsFrom(__DIR__ . '/../resources/views', self::NAME);\n\n        if ($this->app->runningInConsole()) {\n            $this->registerPublishing();\n        }\n\n        $this->callAfterResolving(BladeCompiler::class, $this->aliasToasterHub(...));\n        $this->callAfterResolving(Collector::class, $this->relayToLivewire(...));\n        $this->callAfterResolving(Router::class, $this->relayToSession(...));\n\n        Redirector::mixin($macros = new ToastableMacros());\n        RedirectResponse::mixin($macros);\n    }\n\n    public function register(): void\n    {\n        $config = $this->configureService();\n\n        parent::register();\n\n        $this->app->scoped(Collector::class, QueuingCollector::class);\n        $this->app->alias(Collector::class, self::NAME);\n\n        if ($config->wantsAccessibility) {\n            $this->app->extend(Collector::class, static fn (Collector $next) => new AccessibleCollector($next));\n        }\n\n        if ($config->wantsTranslation) {\n            $this->app->extend(Collector::class, fn (Collector $next) => new TranslatingCollector($next, $this->app['translator']));\n        }\n    }\n\n    private function aliasToasterHub(BladeCompiler $blade): void\n    {\n        $blade->component(ToasterHub::NAME, ToasterHub::class);\n    }\n\n    private function configureService(): ToasterConfig\n    {\n        $this->mergeConfigFrom(__DIR__ . '/../config/toaster.php', self::NAME);\n\n        $config = ToasterConfig::fromArray($this->app['config'][self::NAME] ?? []);\n        $this->app->instance(ToasterConfig::class, $config);\n        $this->app->alias(ToasterConfig::class, self::CONFIG);\n\n        return $config;\n    }\n\n    private function registerPublishing(): void\n    {\n        $this->publishes([\n            __DIR__ . '/../config/toaster.php' => $this->app->configPath('toaster.php'),\n        ], 'toaster-config');\n\n        $this->publishes([\n            __DIR__ . '/../resources/views' => $this->app->resourcePath('views/vendor/toaster'),\n        ], 'toaster-views');\n    }\n\n    private function relayToLivewire(): void\n    {\n        $this->app[LivewireManager::class]->listen('dehydrate', new LivewireRelay());\n    }\n\n    private function relayToSession(Router $router): void\n    {\n        $router->aliasMiddleware(SessionRelay::NAME, SessionRelay::class);\n        $router->pushMiddlewareToGroup('web', SessionRelay::NAME);\n    }\n}\n"
  },
  {
    "path": "src/TranslatingCollector.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Masmerise\\Toaster;\n\nuse Illuminate\\Contracts\\Translation\\Translator;\n\n/** @internal */\nfinal readonly class TranslatingCollector implements Collector\n{\n    public function __construct(\n        private Collector $next,\n        private Translator $translator,\n    ) {}\n\n    public function collect(Toast $toast): void\n    {\n        $replacement = $this->translator->get($original = $toast->message->value, $toast->message->replace);\n\n        if (is_string($replacement) && $replacement !== $original) {\n            $toast = ToastBuilder::proto($toast)->message($replacement)->get();\n        }\n\n        $this->next->collect($toast);\n    }\n\n    public function release(): array\n    {\n        return $this->next->release();\n    }\n}\n"
  },
  {
    "path": "tests/AccessibleCollectorTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\AccessibleCollector;\nuse Masmerise\\Toaster\\Message;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class AccessibleCollectorTest extends TestCase\n{\n    use CollectorFactoryMethods;\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_adds_a_second_for_every_one_hundredth_word_floored(): void\n    {\n        $collector = new AccessibleCollector($this->aCollector());\n        $message = Message::fromTranslatable(str_repeat('word ', 223));\n\n        $collector->collect($this->aToast(message: $message));\n        [$toast] = $collector->release();\n\n        $this->assertSame(5000, $toast->duration->value);\n    }\n}\n"
  },
  {
    "path": "tests/CollectorFactoryMethods.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\QueuingCollector;\n\ntrait CollectorFactoryMethods\n{\n    private function aCollector(): QueuingCollector\n    {\n        return new QueuingCollector();\n    }\n}\n"
  },
  {
    "path": "tests/ControllerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\Toastable;\nuse Masmerise\\Toaster\\ToastBuilder;\nuse Masmerise\\Toaster\\Toaster;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class ControllerTest extends TestCase\n{\n    protected function defineRoutes($router): void\n    {\n        $router->get('inject', [ToastController::class, 'inject'])->middleware('web');\n        $router->get('multiple', [ToastController::class, 'multiple'])->middleware('web');\n    }\n\n    #[Test]\n    public function multiple_toasts_can_be_dispatched(): void\n    {\n        $this->get('multiple')->assertOk()->assertSessionHas('toasts', [\n            [\n                'duration' => 3000,\n                'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',\n                'type' => 'warning',\n            ],\n            [\n                'duration' => 3333,\n                'message' => 'Life is available only in the present moment. - Thich Nhat Hanh',\n                'type' => 'error',\n            ],\n        ]);\n    }\n\n    #[Test]\n    public function toast_is_flashed_to_the_session_using_dependency_injection(): void\n    {\n        $this->get('inject')->assertOk()->assertSessionHas('toasts', [[\n            'duration' => 4000,\n            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',\n            'type' => 'success',\n        ]]);\n    }\n}\n\nfinal class ToastController\n{\n    use Toastable;\n\n    public function inject(Collector $toasts): array\n    {\n        $toast = ToastBuilder::create()\n            ->success()\n            ->duration(4000)\n            ->message('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk')\n            ->get();\n\n        $toasts->collect($toast);\n\n        return ['message' => 'ok'];\n    }\n\n    public function multiple(): array\n    {\n        $this->warning('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk');\n\n        Toaster::error('Life is available only in the present moment. - Thich Nhat Hanh')\n            ->duration(3333)\n            ->error()\n            ->dispatch();\n\n        return ['message' => 'ok'];\n    }\n}\n"
  },
  {
    "path": "tests/DurationTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse InvalidArgumentException;\nuse Masmerise\\Toaster\\Duration;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class DurationTest extends TestCase\n{\n    #[Test]\n    public function it_requires_a_duration_to_be_at_least_1s(): void\n    {\n        $duration = Duration::fromMillis(3000);\n        $this->assertSame(3000, $duration->value);\n\n        $this->expectExceptionMessage(InvalidArgumentException::class);\n        $this->expectExceptionMessage('The duration value must be at least 3000 ms.');\n\n        Duration::fromMillis(999);\n    }\n}\n"
  },
  {
    "path": "tests/LivewireRelayTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Livewire;\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\LivewireRelay;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class LivewireRelayTest extends TestCase\n{\n    #[Test]\n    public function it_dispatches_events(): void\n    {\n        // mount => skip\n        $component = Livewire::test(TestComponent::class);\n        $component->assertNotDispatched(LivewireRelay::EVENT);\n\n        // redirect => skip\n        $component->call('redirectingAction');\n        $component->assertNotDispatched(LivewireRelay::EVENT);\n\n        // redirect using navigate => dispatch\n        $component->call('redirectingActionUsingNavigate');\n        $component->assertDispatched(LivewireRelay::EVENT,\n            duration: 3000,\n            message: 'Crispy toasts',\n            type: 'success',\n        );\n\n        // normal action => dispatch\n        $component->call('normalAction');\n        $component->assertDispatched(LivewireRelay::EVENT,\n            duration: 3000,\n            message: 'Crispy toasts',\n            type: 'success',\n        );\n    }\n}\n\nfinal class TestComponent extends Component\n{\n    use ToastFactoryMethods;\n\n    public function normalAction(): void\n    {\n        // noop\n    }\n\n    public function redirectingAction(): void\n    {\n        $this->redirect('https://localhost');\n    }\n\n    public function redirectingActionUsingNavigate(): void\n    {\n        $this->redirect('https://localhost', true);\n    }\n\n    public function render(Collector $toasts): string\n    {\n        $toasts->collect($this->aToast()); // trigger relay registration\n\n        return '<div></div>';\n    }\n}\n"
  },
  {
    "path": "tests/LivewireTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Features\\SupportTesting\\Testable;\nuse Livewire\\Livewire;\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\Toastable;\nuse Masmerise\\Toaster\\ToastBuilder;\nuse Masmerise\\Toaster\\Toaster;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class LivewireTest extends TestCase\n{\n    private Testable $component;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        $this->component = Livewire::test(ToastComponent::class);\n    }\n\n    #[Test]\n    public function multiple_toasts_can_be_dispatched(): void\n    {\n        $this->component->call('multiple');\n\n        $this->assertCount(2, $events = $this->component->effects['dispatches']);\n\n        [$eventA, $eventB] = $events;\n        $this->assertSame('toaster:received', $eventA['name']);\n        $this->assertSame('toaster:received', $eventB['name']);\n        $this->assertSame([\n            'duration' => 3000,\n            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',\n            'type' => 'warning',\n        ], $eventA['params']);\n        $this->assertSame([\n            'duration' => 3333,\n            'message' => 'Life is available only in the present moment. - Thich Nhat Hanh',\n            'type' => 'error',\n        ], $eventB['params']);\n    }\n\n    #[Test]\n    public function toast_is_dispatched_to_the_browser_using_dependency_injection(): void\n    {\n        $this->component->call('inject');\n\n        $this->assertCount(1, $events = $this->component->effects['dispatches']);\n\n        [$event] = $events;\n        $this->assertSame([\n            'duration' => 4000,\n            'message' => 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk',\n            'type' => 'success',\n        ], $event['params']);\n    }\n}\n\nfinal class ToastComponent extends Component\n{\n    use Toastable;\n\n    public function inject(Collector $toasts): void\n    {\n        $toast = ToastBuilder::create()\n            ->success()\n            ->duration(4000)\n            ->message('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk')\n            ->get();\n\n        $toasts->collect($toast);\n    }\n\n    public function multiple(): void\n    {\n        $this->warning('The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk');\n\n        Toaster::error('Life is available only in the present moment. - Thich Nhat Hanh')\n            ->duration(3333)\n            ->dispatch();\n    }\n\n    public function render(): string\n    {\n        return '<div></div>';\n    }\n}\n"
  },
  {
    "path": "tests/MessageTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse InvalidArgumentException;\nuse Masmerise\\Toaster\\Message;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class MessageTest extends TestCase\n{\n    #[Test]\n    public function it_requires_a_message_not_to_be_empty(): void\n    {\n        $message = Message::fromTranslatable('Clementine :name', ['name' => 'Tangerine']);\n        $this->assertSame('Clementine :name', $message->value);\n        $this->assertSame(['name' => 'Tangerine'], $message->replace);\n\n        $this->expectExceptionMessage(InvalidArgumentException::class);\n        $this->expectExceptionMessage('The message value cannot be empty.');\n\n        Message::fromString('     ');\n    }\n}\n"
  },
  {
    "path": "tests/PendingToastTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\PendingToast;\nuse Masmerise\\Toaster\\Toaster;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class PendingToastTest extends TestCase\n{\n    #[Test]\n    public function it_can_be_instantiated_through_static_factory_on_toaster(): void\n    {\n        $error = Toaster::error('validation.accepted', ['attribute' => 'terms'])->get();\n        $this->assertSame([\n            'duration' => 3000,\n            'message' => 'validation.accepted',\n            'type' => 'error',\n        ], $error->toArray());\n\n        $info = Toaster::info('Informational')->duration(3500)->get();\n        $this->assertSame([\n            'duration' => 3500,\n            'message' => 'Informational',\n            'type' => 'info',\n        ], $info->toArray());\n\n        $success = Toaster::success('Successful')->get();\n        $this->assertSame([\n            'duration' => 3000,\n            'message' => 'Successful',\n            'type' => 'success',\n        ], $success->toArray());\n\n        $warning = Toaster::warning('passwords.reset')->get();\n        $this->assertSame([\n            'duration' => 3000,\n            'message' => 'passwords.reset',\n            'type' => 'warning',\n        ], $warning->toArray());\n    }\n\n    #[Test]\n    public function it_can_be_instantiated_with_defaults(): void\n    {\n        $toast = PendingToast::create()->message('test')->success()->get();\n\n        $this->assertSame([\n            'duration' => 3000, // config default\n            'message' => 'test',\n            'type' => 'success',\n        ], $toast->toArray());\n    }\n\n    #[Test]\n    public function it_will_automatically_dispatch_the_toast_upon_destruction(): void\n    {\n        Toaster::shouldReceive('collect')->once();\n\n        PendingToast::create()\n            ->duration(4000)\n            ->error()\n            ->message('Uvuvuvwevwe Onyetenyevwe Ughemuhwem Osas');\n    }\n\n    #[Test]\n    public function it_will_only_dispatch_once(): void\n    {\n        Toaster::shouldReceive('collect')->once();\n\n        PendingToast::create()\n            ->duration(4000)\n            ->success()\n            ->message('Uvuvuvwevwe Onyetenyevwe Ughemuhwem Osas')\n            ->dispatch();\n    }\n}\n"
  },
  {
    "path": "tests/QueuingCollectorTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class QueuingCollectorTest extends TestCase\n{\n    use CollectorFactoryMethods;\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_can_add_and_flush_toasts(): void\n    {\n        $collector = $this->aCollector();\n        $collector->collect($toastA = $this->aToast());\n        $collector->collect($toastB = $this->aToast());\n\n        $toasts = $collector->release();\n\n        $this->assertCount(2, $toasts);\n        $this->assertSame($toastA, $toasts[0]);\n        $this->assertSame($toastB, $toasts[1]);\n        $this->assertEmpty($collector->release());\n    }\n}\n"
  },
  {
    "path": "tests/SessionRelayTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\Request;\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\SessionRelay;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class SessionRelayTest extends TestCase\n{\n    use CollectorFactoryMethods;\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_relays_toasts_to_the_session(): void\n    {\n        $session = $this->app['session.store'];\n        $relay = new SessionRelay($this->app);\n\n        $relay->handle(new Request(), function () {});\n\n        $this->assertFalse($session->exists(SessionRelay::NAME));\n\n        $collector = $this->app[Collector::class];\n        $collector->collect($this->aToast());\n        $collector->collect($this->aToast());\n\n        $relay->handle(new Request(), function () {});\n\n        $this->assertTrue($session->exists(SessionRelay::NAME));\n        $this->assertCount(2, $toasts = $session->get(SessionRelay::NAME));\n        $this->assertEmpty($collector->release());\n        $this->assertIsArray($toast = $toasts[0]);\n        $this->assertArrayHasKey('duration', $toast);\n        $this->assertArrayHasKey('message', $toast);\n        $this->assertArrayHasKey('type', $toast);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToasterServiceProvider;\nuse Orchestra\\Testbench\\TestCase as TestCaseBase;\n\nabstract class TestCase extends TestCaseBase\n{\n    protected function getPackageProviders($app): array\n    {\n        return [ToasterServiceProvider::class];\n    }\n}\n"
  },
  {
    "path": "tests/TestableCollectorTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\TestableCollector;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class TestableCollectorTest extends TestCase\n{\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_can_assert_if_toasts_were_dispatched(): void\n    {\n        $instance = new TestableCollector();\n\n        $instance->assertNothingDispatched();\n\n        $instance->collect($this->aToast());\n\n        $instance->assertDispatched('Crispy toasts');\n    }\n}\n"
  },
  {
    "path": "tests/ToastBuilderTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToastBuilder;\nuse Masmerise\\Toaster\\ToastType;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\nuse UnexpectedValueException;\n\nfinal class ToastBuilderTest extends TestCase\n{\n    #[Test]\n    public function it_can_fluently_build_and_return_a_toast(): void\n    {\n        $builderA = ToastBuilder::create();\n        $builderB = $builderA->message('Rice cooker');\n        $builderC = $builderB->success();\n        $builderD = $builderC->duration(4000);\n\n        $toast = $builderD->get();\n\n        $this->assertNotSame($builderA, $builderB);\n        $this->assertNotSame($builderB, $builderC);\n        $this->assertNotSame($builderC, $builderD);\n        $this->assertSame('Rice cooker', $toast->message->value);\n        $this->assertSame(ToastType::Success, $toast->type);\n        $this->assertSame(4000, $toast->duration->value);\n    }\n\n    #[Test]\n    public function it_throws_if_the_builder_data_is_incomplete_to_build_a_toast(): void\n    {\n        $this->expectException(UnexpectedValueException::class);\n        $this->expectExceptionMessage('You must provide a valid duration.');\n\n        ToastBuilder::create()->get();\n    }\n}\n"
  },
  {
    "path": "tests/ToastFactoryMethods.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Duration;\nuse Masmerise\\Toaster\\Message;\nuse Masmerise\\Toaster\\Toast;\nuse Masmerise\\Toaster\\ToastType;\n\ntrait ToastFactoryMethods\n{\n    private function aToast(...$values): Toast\n    {\n        return new Toast(...[\n            'message' => Message::fromString('Crispy toasts'),\n            'duration' => Duration::fromMillis(3000),\n            'type' => ToastType::Success,\n            ...$values,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/ToastTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ToastTest extends TestCase\n{\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_can_be_serialized_to_array(): void\n    {\n        $toast = $this->aToast();\n\n        $result = $toast->toArray();\n\n        $this->assertSame([\n            'duration' => 3000,\n            'message' => 'Crispy toasts',\n            'type' => 'success',\n        ], $result);\n    }\n}\n"
  },
  {
    "path": "tests/ToastableMacrosTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class ToastableMacrosTest extends TestCase\n{\n    protected function defineRoutes($router): void\n    {\n        $router->get('redirect', [ToastableMacroController::class, 'redirect'])->middleware('web');\n    }\n\n    #[Test]\n    public function redirect(): void\n    {\n        $this->get('redirect')->assertSessionHas('toasts');\n    }\n}\n\nfinal class ToastableMacroController\n{\n    public function redirect(): RedirectResponse\n    {\n        return redirect()\n            ->away('https://www.google.com')\n            ->success('Success Macro!');\n    }\n}\n"
  },
  {
    "path": "tests/ToastableTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Livewire\\Component;\nuse Livewire\\Livewire;\nuse Masmerise\\Toaster\\LivewireRelay;\nuse Masmerise\\Toaster\\SessionRelay;\nuse Masmerise\\Toaster\\Toastable;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class ToastableTest extends TestCase\n{\n    #[Test]\n    public function it_can_be_invoked_from_controllers(): void\n    {\n        $this->app['router']->get('test', [ToastableController::class, 'index'])->middleware('web');\n\n        $response = $this->get('test');\n\n        $response->assertOk()->assertSessionHas(SessionRelay::NAME, [[\n            'duration' => 3000,\n            'message' => 'I am a crispy toast, yummy!',\n            'type' => 'info',\n        ]]);\n    }\n\n    #[Test]\n    public function it_can_be_invoked_from_livewire_components(): void\n    {\n        $component = Livewire::test(ToastableComponent::class);\n\n        $component->call('bake');\n\n        $component->assertDispatched(LivewireRelay::EVENT,\n            duration: 3000,\n            message: 'I became a crispy toast, yummy!',\n            type: 'success',\n        );\n    }\n}\n\nfinal class ToastableComponent extends Component\n{\n    use Toastable;\n\n    public function bake(): void\n    {\n        $this->success('I became a crispy toast, yummy!');\n    }\n\n    public function render(): string\n    {\n        return '<div></div>';\n    }\n}\n\nfinal class ToastableController\n{\n    use Toastable;\n\n    public function index(): array\n    {\n        $this->info('I am a crispy toast, yummy!');\n\n        return ['message' => 'success'];\n    }\n}\n"
  },
  {
    "path": "tests/ToasterConfigTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\ToasterConfig;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ToasterConfigTest extends TestCase\n{\n    #[Test]\n    public function it_can_be_serialized_for_the_frontend(): void\n    {\n        $config = ToasterConfig::fromArray(require __DIR__ . '/../config/toaster.php');\n\n        $array = $config->toJavaScript();\n\n        $this->assertSame([\n            'alignment' => 'bottom',\n            'duration' => 3000,\n            'replace' => false,\n            'suppress' => false,\n        ], $array);\n    }\n}\n"
  },
  {
    "path": "tests/ToasterHubTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Testing\\Concerns\\InteractsWithViews;\nuse Masmerise\\Toaster\\Alignment;\nuse Masmerise\\Toaster\\Position;\nuse Masmerise\\Toaster\\ToasterConfig;\nuse Masmerise\\Toaster\\ToasterHub;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class ToasterHubTest extends TestCase\n{\n    use InteractsWithViews;\n\n    public static function configurations(): iterable\n    {\n        yield [['alignment' => Alignment::Bottom->value, 'closeable' => true, 'position' => Position::Right->value]];\n        yield [['alignment' => Alignment::Middle->value, 'closeable' => true, 'position' => Position::Right->value]];\n        yield [['alignment' => Alignment::Top->value, 'closeable' => true, 'position' => Position::Right->value]];\n        yield [['alignment' => Alignment::Bottom->value, 'closeable' => false, 'position' => Position::Left->value]];\n        yield [['alignment' => Alignment::Middle->value, 'closeable' => false, 'position' => Position::Left->value]];\n        yield [['alignment' => Alignment::Top->value, 'closeable' => false, 'position' => Position::Left->value]];\n        yield [['alignment' => Alignment::Bottom->value, 'closeable' => false, 'position' => Position::Center->value]];\n        yield [['alignment' => Alignment::Middle->value, 'closeable' => false, 'position' => Position::Center->value]];\n        yield [['alignment' => Alignment::Top->value, 'closeable' => false, 'position' => Position::Center->value]];\n    }\n\n    #[DataProvider('configurations')]\n    #[Test]\n    public function it_can_be_rendered(array $config): void\n    {\n        $component = $this->component(ToasterHub::class, ['config' => ToasterConfig::fromArray($config)]);\n\n        $component->assertSee('toaster');\n    }\n}\n"
  },
  {
    "path": "tests/ToasterServiceProviderTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Redirector;\nuse Masmerise\\Toaster\\AccessibleCollector;\nuse Masmerise\\Toaster\\Collector;\nuse Masmerise\\Toaster\\ToasterConfig;\nuse Masmerise\\Toaster\\ToasterServiceProvider;\nuse Masmerise\\Toaster\\TranslatingCollector;\nuse Orchestra\\Testbench\\TestCase;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class ToasterServiceProviderTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        $this->app->register(ToasterServiceProvider::class);\n    }\n\n    #[Test]\n    public function it_binds_the_service_as_a_singleton(): void\n    {\n        $this->assertTrue($this->app->isShared(Collector::class));\n        $this->assertTrue($this->app->isAlias(ToasterServiceProvider::NAME));\n    }\n\n    #[Test]\n    public function it_registers_the_translating_behaviour_only_if_enabled_in_the_config(): void\n    {\n        $this->assertInstanceOf(TranslatingCollector::class, $this->app[ToasterServiceProvider::NAME]);\n\n        $this->refreshApplication();\n        $this->app['config']->set('toaster.translate', false);\n        $this->app->register(ToasterServiceProvider::class);\n\n        $this->assertInstanceOf(AccessibleCollector::class, $this->app[ToasterServiceProvider::NAME]);\n    }\n\n    #[Test]\n    public function it_registers_macros(): void\n    {\n        $this->assertTrue(Redirector::hasMacro('error'));\n        $this->assertTrue(Redirector::hasMacro('info'));\n        $this->assertTrue(Redirector::hasMacro('success'));\n        $this->assertTrue(Redirector::hasMacro('warning'));\n\n        $this->assertTrue(RedirectResponse::hasMacro('error'));\n        $this->assertTrue(RedirectResponse::hasMacro('info'));\n        $this->assertTrue(RedirectResponse::hasMacro('success'));\n        $this->assertTrue(RedirectResponse::hasMacro('warning'));\n    }\n\n    #[Test]\n    public function it_registers_custom_config_object(): void\n    {\n        $this->assertTrue($this->app->bound(ToasterConfig::class));\n\n        $config = $this->app[ToasterConfig::class];\n\n        $this->assertSame(3000, $config->duration);\n        $this->assertTrue($config->wantsAccessibility);\n        $this->assertTrue($config->wantsCloseableToasts);\n        $this->assertTrue($config->wantsTranslation);\n        $this->assertSame('right', $config->position);\n    }\n}\n"
  },
  {
    "path": "tests/TranslatingCollectorTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Tests;\n\nuse Masmerise\\Toaster\\Message;\nuse Masmerise\\Toaster\\TranslatingCollector;\nuse PHPUnit\\Framework\\Attributes\\Test;\n\nfinal class TranslatingCollectorTest extends TestCase\n{\n    use CollectorFactoryMethods;\n    use ToastFactoryMethods;\n\n    #[Test]\n    public function it_can_translate_the_messages(): void\n    {\n        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);\n        $message = Message::fromTranslatable('auth.throttle', ['seconds' => 1337]);\n\n        $collector->collect($this->aToast(message: $message));\n        [$toast] = $collector->release();\n\n        $this->assertSame('Too many login attempts. Please try again in 1337 seconds.', $toast->message->value);\n    }\n\n    #[Test]\n    public function it_doesnt_replace_array_resolved_translations(): void\n    {\n        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);\n        $message = Message::fromTranslatable('validation.size');\n\n        $collector->collect($this->aToast(message: $message));\n        [$toast] = $collector->release();\n\n        $this->assertSame('validation.size', $toast->message->value);\n    }\n\n    #[Test]\n    public function it_doesnt_modify_regular_strings(): void\n    {\n        $collector = new TranslatingCollector($this->aCollector(), $this->app['translator']);\n\n        $collector->collect($this->aToast());\n        [$toast] = $collector->release();\n\n        $this->assertSame('Crispy toasts', $toast->message->value);\n    }\n}\n"
  }
]